no message
@@ -11,6 +11,7 @@
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ModernConversationAudioSlideToCancel@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
|
||||
|
Before Width: | Height: | Size: 111 B After Width: | Height: | Size: 496 B |
|
After Width: | Height: | Size: 645 B |
@@ -2,7 +2,17 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_attach.pdf"
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ModernConversationAttach@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ModernConversationAttach@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
BIN
Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ModernConversationAttach@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ModernConversationAttach@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
@@ -2,7 +2,17 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_voice.pdf"
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ModernConversationMicButton@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ModernConversationMicButton@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
BIN
Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 843 B |
BIN
Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
@@ -2,7 +2,17 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Send.pdf"
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ModernConversationSend@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ModernConversationSend@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
BIN
Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 682 B |
BIN
Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1019 B |
@@ -6,12 +6,12 @@
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewSettingsIcon@2x.png",
|
||||
"filename" : "RecordVideoIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewSettingsIcon@3x.png",
|
||||
"filename" : "RecordVideoIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
BIN
Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 651 B |
BIN
Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
22
Images.xcassets/Instant View/ActionIcon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_share@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_share@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Instant View/ActionIcon.imageset/ic_share@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Images.xcassets/Instant View/ActionIcon.imageset/ic_share@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 306 B |
@@ -6,11 +6,12 @@
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantPageBackArrow@2x.png",
|
||||
"filename" : "ic_more@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_more@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
BIN
Images.xcassets/Instant View/MoreIcon.imageset/ic_more@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 453 B |
BIN
Images.xcassets/Instant View/MoreIcon.imageset/ic_more@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 350 B |
22
Images.xcassets/Instant View/PanelCheck.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewCheck@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewCheck@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 359 B |
BIN
Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 502 B |
22
Images.xcassets/Instant View/SettingsArrow.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewRightCorner@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewRightCorner@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Instant View/SettingsArrow.imageset/InstantViewRightCorner@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Images.xcassets/Instant View/SettingsArrow.imageset/InstantViewRightCorner@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
22
Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewBrightnessMaxIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewBrightnessMaxIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 513 B |
BIN
Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 447 B |
22
Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewBrightnessMinIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewBrightnessMinIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/InstantViewBrightnessMinIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 398 B |
BIN
Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/InstantViewBrightnessMinIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 356 B |
22
Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewFontMaxIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewFontMaxIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/InstantViewFontMaxIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 508 B |
BIN
Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/InstantViewFontMaxIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 687 B |
22
Images.xcassets/Instant View/SettingsFontMinIcon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewFontMinIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "InstantViewFontMinIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Instant View/SettingsFontMinIcon.imageset/InstantViewFontMinIcon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 277 B |
BIN
Images.xcassets/Instant View/SettingsFontMinIcon.imageset/InstantViewFontMinIcon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 372 B |
|
Before Width: | Height: | Size: 829 B |
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -11,6 +11,10 @@
|
||||
D00ADFDB1EBA2EAF00873D2E /* OngoingCallContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00ADFDA1EBA2EAF00873D2E /* OngoingCallContext.swift */; };
|
||||
D00ADFDD1EBB73C200873D2E /* OverlayMediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00ADFDC1EBB73C200873D2E /* OverlayMediaManager.swift */; };
|
||||
D00BDA1F1EE5B69200C64C5E /* ChannelAdminController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00BDA1E1EE5B69200C64C5E /* ChannelAdminController.swift */; };
|
||||
D00FF2091F4E2414006FA332 /* InstantPageSettingsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00FF2081F4E2414006FA332 /* InstantPageSettingsNode.swift */; };
|
||||
D0104F281F47171F004E4881 /* InstantPageGalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0104F271F47171F004E4881 /* InstantPageGalleryController.swift */; };
|
||||
D0104F2A1F471DA6004E4881 /* InstantImageGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0104F291F471DA6004E4881 /* InstantImageGalleryItem.swift */; };
|
||||
D0104F2C1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0104F2B1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift */; };
|
||||
D01776B31F1D69A80044446D /* RadialStatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01776B21F1D69A80044446D /* RadialStatusNode.swift */; };
|
||||
D01776B51F1D6CCC0044446D /* RadialStatusContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01776B41F1D6CCC0044446D /* RadialStatusContentNode.swift */; };
|
||||
D01776B81F1D6FB30044446D /* RadialProgressContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01776B71F1D6FB30044446D /* RadialProgressContentNode.swift */; };
|
||||
@@ -28,6 +32,7 @@
|
||||
D01BAA241ECE173200295217 /* PresentationResourcesCallList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA231ECE173200295217 /* PresentationResourcesCallList.swift */; };
|
||||
D01BAA581ED3283D00295217 /* AddFormatToStringWithRanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA571ED3283D00295217 /* AddFormatToStringWithRanges.swift */; };
|
||||
D01C7F001EF9D45B008305F1 /* DeviceContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C7EFF1EF9D45B008305F1 /* DeviceContactsManager.swift */; };
|
||||
D01C99781F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C99771F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift */; };
|
||||
D02660941F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02660931F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift */; };
|
||||
D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033C60A1F0D306E0044EABA /* TelegramVideoNode.swift */; };
|
||||
D03E838F1EC10FE5001A6ED9 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D0FC40831D5B8E7400261D9D /* Info.plist */; };
|
||||
@@ -44,12 +49,21 @@
|
||||
D0471B601EFEB5A70074D609 /* BotPaymentTextItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B5F1EFEB5A70074D609 /* BotPaymentTextItemNode.swift */; };
|
||||
D0471B621EFEB5B70074D609 /* BotPaymentSwitchItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B611EFEB5B70074D609 /* BotPaymentSwitchItemNode.swift */; };
|
||||
D0471B641EFEB5CB0074D609 /* BotPaymentItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0471B631EFEB5CB0074D609 /* BotPaymentItemNode.swift */; };
|
||||
D048EA851F4F295300188713 /* InstantPageSettingsBacklightItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EA841F4F295300188713 /* InstantPageSettingsBacklightItemNode.swift */; };
|
||||
D048EA871F4F296400188713 /* InstantPageSettingsFontSizeItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EA861F4F296400188713 /* InstantPageSettingsFontSizeItemNode.swift */; };
|
||||
D048EA891F4F297500188713 /* InstantPageSettingsFontFamilyItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EA881F4F297500188713 /* InstantPageSettingsFontFamilyItemNode.swift */; };
|
||||
D048EA8B1F4F298A00188713 /* InstantPageSettingsThemeItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EA8A1F4F298A00188713 /* InstantPageSettingsThemeItemNode.swift */; };
|
||||
D048EA8D1F4F299A00188713 /* InstantPageSettingsSwitchItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EA8C1F4F299A00188713 /* InstantPageSettingsSwitchItemNode.swift */; };
|
||||
D048EA8F1F4F2A9C00188713 /* InstantPageSettingsItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EA8E1F4F2A9C00188713 /* InstantPageSettingsItemNode.swift */; };
|
||||
D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D101EEA04D400711AF6 /* MapResources.swift */; };
|
||||
D04B4D131EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D121EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift */; };
|
||||
D04B4D661EEA993A00711AF6 /* LegacyLocationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D651EEA993A00711AF6 /* LegacyLocationController.swift */; };
|
||||
D053B4351F19299100E2D58A /* ChatMessageItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D053B4341F19299000E2D58A /* ChatMessageItemContent.swift */; };
|
||||
D053B4371F1A9CA000E2D58A /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D053B4361F1A9CA000E2D58A /* WebKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
D05677511F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05677501F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift */; };
|
||||
D05677531F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05677521F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift */; };
|
||||
D0642EFC1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0642EFB1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift */; };
|
||||
D06BB8821F58994B0084FC30 /* LegacyInstantVideoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BB8811F58994B0084FC30 /* LegacyInstantVideoController.swift */; };
|
||||
D0754D1E1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D1D1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift */; };
|
||||
D0754D201EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D1F1EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift */; };
|
||||
D0754D221EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D211EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift */; };
|
||||
@@ -61,6 +75,8 @@
|
||||
D079FCE11F05C9380038FADE /* BotReceiptControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D079FCE01F05C9380038FADE /* BotReceiptControllerNode.swift */; };
|
||||
D079FCE91F06A76C0038FADE /* Notices.swift in Sources */ = {isa = PBXBuildFile; fileRef = D079FCE81F06A76C0038FADE /* Notices.swift */; };
|
||||
D07BCBFE1F2B792300ED97AA /* LegacyComponents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D07BCBFD1F2B792300ED97AA /* LegacyComponents.framework */; };
|
||||
D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */; };
|
||||
D089F78A1F4E0C14000E934D /* InstantPagePresentationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */; };
|
||||
D099D74D1EEFEE1500A3128C /* GameController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D74C1EEFEE1500A3128C /* GameController.swift */; };
|
||||
D099D74F1EEFEE6A00A3128C /* GameControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D74E1EEFEE6A00A3128C /* GameControllerNode.swift */; };
|
||||
D099D7511EEFF91E00A3128C /* GameControllerTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D7501EEFF91E00A3128C /* GameControllerTitleView.swift */; };
|
||||
@@ -75,6 +91,8 @@
|
||||
D0ACCB1C1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ACCB1B1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift */; };
|
||||
D0AF7C461ED84BC500CD8E0F /* LanguageSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF7C451ED84BC500CD8E0F /* LanguageSelectionController.swift */; };
|
||||
D0AF7C4A1ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF7C491ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift */; };
|
||||
D0AFCC791F4C8D2C000720C6 /* InstantPageSlideshowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AFCC781F4C8D2C000720C6 /* InstantPageSlideshowItem.swift */; };
|
||||
D0AFCC7B1F4C8D39000720C6 /* InstantPageSlideshowItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AFCC7A1F4C8D39000720C6 /* InstantPageSlideshowItemNode.swift */; };
|
||||
D0B4AF861EC111FA00D51FF6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */; };
|
||||
D0B4AF881EC112EE00D51FF6 /* CallKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0B4AF871EC112ED00D51FF6 /* CallKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
D0B4AF8B1EC1133600D51FF6 /* CallKitIntergation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B4AF8A1EC1133600D51FF6 /* CallKitIntergation.swift */; };
|
||||
@@ -85,6 +103,8 @@
|
||||
D0C0B5B11EE1C421000F4D2C /* ChatDateSelectionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B5B01EE1C421000F4D2C /* ChatDateSelectionSheet.swift */; };
|
||||
D0C0B5B71EE1DEF1000F4D2C /* ThemeGridControllerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C0B5B61EE1DEF1000F4D2C /* ThemeGridControllerItem.swift */; };
|
||||
D0C12A1D1F33A85600B3F66D /* ChatWallpaperBuiltin0.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D0C12A1B1F33964900B3F66D /* ChatWallpaperBuiltin0.jpg */; };
|
||||
D0C27B3B1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C27B3A1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift */; };
|
||||
D0C27B3D1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C27B3C1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift */; };
|
||||
D0E9B9E81EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9B9E71EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift */; };
|
||||
D0E9B9EA1F00853C00F079A4 /* PhoneCountries.txt in Resources */ = {isa = PBXBuildFile; fileRef = D0E9B9E91F00853C00F079A4 /* PhoneCountries.txt */; };
|
||||
D0E9B9F41F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9B9F31F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift */; };
|
||||
@@ -578,7 +598,7 @@
|
||||
D0EC6DDA1EB9F58900EBF1C3 /* HorizontalListContextResultsChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA201DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift */; };
|
||||
D0EC6DDB1EB9F58900EBF1C3 /* ChatInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */; };
|
||||
D0EC6DDC1EB9F58900EBF1C3 /* ChatTextInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E401D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift */; };
|
||||
D0EC6DDD1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F66121DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift */; };
|
||||
D0EC6DDD1EB9F58900EBF1C3 /* ChatTextInputMediaRecordingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F66121DE8903300345CBE /* ChatTextInputMediaRecordingButton.swift */; };
|
||||
D0EC6DDE1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */; };
|
||||
D0EC6DDF1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingTimeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */; };
|
||||
D0EC6DE01EB9F58900EBF1C3 /* ChatTextInputAudioRecordingCancelIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */; };
|
||||
@@ -639,8 +659,8 @@
|
||||
D0EC6E171EB9F58900EBF1C3 /* InstantPageTextStyleStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0215D431E0413FB001A0B1E /* InstantPageTextStyleStack.swift */; };
|
||||
D0EC6E181EB9F58900EBF1C3 /* InstantPageTextItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0215D451E041851001A0B1E /* InstantPageTextItem.swift */; };
|
||||
D0EC6E191EB9F58900EBF1C3 /* InstantPageAnchorItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0215D471E041B90001A0B1E /* InstantPageAnchorItem.swift */; };
|
||||
D0EC6E1A1EB9F58900EBF1C3 /* InstantPageMediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0215D491E041CAF001A0B1E /* InstantPageMediaItem.swift */; };
|
||||
D0EC6E1B1EB9F58900EBF1C3 /* InstantPageMediaNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0215D4B1E041D5E001A0B1E /* InstantPageMediaNode.swift */; };
|
||||
D0EC6E1A1EB9F58900EBF1C3 /* InstantPageImageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0215D491E041CAF001A0B1E /* InstantPageImageItem.swift */; };
|
||||
D0EC6E1B1EB9F58900EBF1C3 /* InstantPageImageNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0215D4B1E041D5E001A0B1E /* InstantPageImageNode.swift */; };
|
||||
D0EC6E1C1EB9F58900EBF1C3 /* InstantPageWebEmbedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0215D4F1E0422C7001A0B1E /* InstantPageWebEmbedItem.swift */; };
|
||||
D0EC6E1D1EB9F58900EBF1C3 /* InstantPageWebEmbedNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0215D4D1E042164001A0B1E /* InstantPageWebEmbedNode.swift */; };
|
||||
D0EC6E1E1EB9F58900EBF1C3 /* InstantPageShapeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0215D511E0423EE001A0B1E /* InstantPageShapeItem.swift */; };
|
||||
@@ -883,6 +903,9 @@
|
||||
D0FE4DE01F0ACA8300E8A0B3 /* InstantVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE4DDF1F0ACA8300E8A0B3 /* InstantVideoNode.swift */; };
|
||||
D0FE4DE41F0AEBB900E8A0B3 /* SharedVideoContextManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE4DE31F0AEBB900E8A0B3 /* SharedVideoContextManager.swift */; };
|
||||
D0FE4DE61F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE4DE51F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift */; };
|
||||
D0FFF7F61F55B82500BEBC01 /* InstantPageAudioItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FFF7F51F55B82500BEBC01 /* InstantPageAudioItem.swift */; };
|
||||
D0FFF7F81F55B83600BEBC01 /* InstantPageAudioNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FFF7F71F55B83600BEBC01 /* InstantPageAudioNode.swift */; };
|
||||
D0FFF7FF1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FFF7FE1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -919,6 +942,10 @@
|
||||
D00DE69B1E8E8E97003F0D76 /* ShareControllerPeerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareControllerPeerGridItem.swift; sourceTree = "<group>"; };
|
||||
D00DE6AC1E8EB2D4003F0D76 /* ShareActionButtonNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareActionButtonNode.swift; sourceTree = "<group>"; };
|
||||
D00E15251DDBD4E700ACF65C /* LegacyCamera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCamera.swift; sourceTree = "<group>"; };
|
||||
D00FF2081F4E2414006FA332 /* InstantPageSettingsNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsNode.swift; sourceTree = "<group>"; };
|
||||
D0104F271F47171F004E4881 /* InstantPageGalleryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageGalleryController.swift; sourceTree = "<group>"; };
|
||||
D0104F291F471DA6004E4881 /* InstantImageGalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantImageGalleryItem.swift; sourceTree = "<group>"; };
|
||||
D0104F2B1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageGalleryFooterContentNode.swift; sourceTree = "<group>"; };
|
||||
D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatChannelSubscriberInputPanelNode.swift; sourceTree = "<group>"; };
|
||||
D010C2C91EA7A59F00F41B96 /* PresentationThemeSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationThemeSettings.swift; sourceTree = "<group>"; };
|
||||
D010C2CB1EA7D74800F41B96 /* DefaultPresentationTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultPresentationTheme.swift; sourceTree = "<group>"; };
|
||||
@@ -963,8 +990,9 @@
|
||||
D01C2AAA1E75E010001F6F9A /* TwoStepVerificationUnlockController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationUnlockController.swift; sourceTree = "<group>"; };
|
||||
D01C2AAC1E768404001F6F9A /* Markdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Markdown.swift; sourceTree = "<group>"; };
|
||||
D01C7EFF1EF9D45B008305F1 /* DeviceContactsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceContactsManager.swift; sourceTree = "<group>"; };
|
||||
D01C99771F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsItemTheme.swift; sourceTree = "<group>"; };
|
||||
D01D6BFB1E42AB3C006151C6 /* EmojiUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiUtils.swift; sourceTree = "<group>"; };
|
||||
D01F66121DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingButton.swift; sourceTree = "<group>"; };
|
||||
D01F66121DE8903300345CBE /* ChatTextInputMediaRecordingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputMediaRecordingButton.swift; sourceTree = "<group>"; };
|
||||
D0215D371E040F53001A0B1E /* InstantPageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageNode.swift; sourceTree = "<group>"; };
|
||||
D0215D391E041003001A0B1E /* InstantPageLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageLayout.swift; sourceTree = "<group>"; };
|
||||
D0215D3B1E041014001A0B1E /* InstantPageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageItem.swift; sourceTree = "<group>"; };
|
||||
@@ -974,8 +1002,8 @@
|
||||
D0215D431E0413FB001A0B1E /* InstantPageTextStyleStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageTextStyleStack.swift; sourceTree = "<group>"; };
|
||||
D0215D451E041851001A0B1E /* InstantPageTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageTextItem.swift; sourceTree = "<group>"; };
|
||||
D0215D471E041B90001A0B1E /* InstantPageAnchorItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageAnchorItem.swift; sourceTree = "<group>"; };
|
||||
D0215D491E041CAF001A0B1E /* InstantPageMediaItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageMediaItem.swift; sourceTree = "<group>"; };
|
||||
D0215D4B1E041D5E001A0B1E /* InstantPageMediaNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageMediaNode.swift; sourceTree = "<group>"; };
|
||||
D0215D491E041CAF001A0B1E /* InstantPageImageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageImageItem.swift; sourceTree = "<group>"; };
|
||||
D0215D4B1E041D5E001A0B1E /* InstantPageImageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageImageNode.swift; sourceTree = "<group>"; };
|
||||
D0215D4D1E042164001A0B1E /* InstantPageWebEmbedNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageWebEmbedNode.swift; sourceTree = "<group>"; };
|
||||
D0215D4F1E0422C7001A0B1E /* InstantPageWebEmbedItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageWebEmbedItem.swift; sourceTree = "<group>"; };
|
||||
D0215D511E0423EE001A0B1E /* InstantPageShapeItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageShapeItem.swift; sourceTree = "<group>"; };
|
||||
@@ -1045,6 +1073,12 @@
|
||||
D0471B631EFEB5CB0074D609 /* BotPaymentItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotPaymentItemNode.swift; sourceTree = "<group>"; };
|
||||
D04791661E79A22000F18979 /* ItemListStickerPackItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListStickerPackItem.swift; sourceTree = "<group>"; };
|
||||
D0486F091E523C8500091F0C /* GroupInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInfoController.swift; sourceTree = "<group>"; };
|
||||
D048EA841F4F295300188713 /* InstantPageSettingsBacklightItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsBacklightItemNode.swift; sourceTree = "<group>"; };
|
||||
D048EA861F4F296400188713 /* InstantPageSettingsFontSizeItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsFontSizeItemNode.swift; sourceTree = "<group>"; };
|
||||
D048EA881F4F297500188713 /* InstantPageSettingsFontFamilyItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsFontFamilyItemNode.swift; sourceTree = "<group>"; };
|
||||
D048EA8A1F4F298A00188713 /* InstantPageSettingsThemeItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsThemeItemNode.swift; sourceTree = "<group>"; };
|
||||
D048EA8C1F4F299A00188713 /* InstantPageSettingsSwitchItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsSwitchItemNode.swift; sourceTree = "<group>"; };
|
||||
D048EA8E1F4F2A9C00188713 /* InstantPageSettingsItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsItemNode.swift; sourceTree = "<group>"; };
|
||||
D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalStickersChatContextPanelNode.swift; sourceTree = "<group>"; };
|
||||
D049EAE31E44949F00A2CD3A /* HorizontalStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalStickerGridItem.swift; sourceTree = "<group>"; };
|
||||
D049EAE51E44AD5600A2CD3A /* ChatMediaInputMetaSectionItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputMetaSectionItemNode.swift; sourceTree = "<group>"; };
|
||||
@@ -1145,6 +1179,8 @@
|
||||
D0561DE01E57153000E6B9E9 /* ItemListActivityTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListActivityTextItem.swift; sourceTree = "<group>"; };
|
||||
D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListMultilineTextItem.swift; sourceTree = "<group>"; };
|
||||
D0561DE71E574C3200E6B9E9 /* ChannelAdminsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelAdminsController.swift; sourceTree = "<group>"; };
|
||||
D05677501F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePeerReferenceItem.swift; sourceTree = "<group>"; };
|
||||
D05677521F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePeerReferenceNode.swift; sourceTree = "<group>"; };
|
||||
D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveformNode.swift; sourceTree = "<group>"; };
|
||||
D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = "<group>"; };
|
||||
D0575AEA1E9FD579006F2541 /* ChatListTitleLockView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListTitleLockView.swift; sourceTree = "<group>"; };
|
||||
@@ -1172,6 +1208,7 @@
|
||||
D0642EFB1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryNavigationButtons.swift; sourceTree = "<group>"; };
|
||||
D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = "<group>"; };
|
||||
D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = "<group>"; };
|
||||
D06BB8811F58994B0084FC30 /* LegacyInstantVideoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyInstantVideoController.swift; sourceTree = "<group>"; };
|
||||
D06E4AC31E84806300627D1D /* FetchPhotoLibraryImageResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchPhotoLibraryImageResource.swift; sourceTree = "<group>"; };
|
||||
D06FFBA71EAFAC4F00CB53D4 /* PresentationThemeEssentialGraphics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationThemeEssentialGraphics.swift; sourceTree = "<group>"; };
|
||||
D06FFBA91EAFAD2500CB53D4 /* PresentationResourcesChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationResourcesChat.swift; sourceTree = "<group>"; };
|
||||
@@ -1213,6 +1250,7 @@
|
||||
D07CFF7C1DCA273400761F81 /* ChatListViewTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListViewTransition.swift; sourceTree = "<group>"; };
|
||||
D07CFF7E1DCA308500761F81 /* ChatListNodeLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListNodeLocation.swift; sourceTree = "<group>"; };
|
||||
D07CFF861DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForwardAccessoryPanelNode.swift; sourceTree = "<group>"; };
|
||||
D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageManagedMediaId.swift; sourceTree = "<group>"; };
|
||||
D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListEditableDeleteControlNode.swift; sourceTree = "<group>"; };
|
||||
D08774F91E3E2A5600A97350 /* ItemListCheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListCheckboxItem.swift; sourceTree = "<group>"; };
|
||||
D08775081E3E59DE00A97350 /* PeerNotificationSoundStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerNotificationSoundStrings.swift; sourceTree = "<group>"; };
|
||||
@@ -1224,6 +1262,7 @@
|
||||
D087751B1E3F542500A97350 /* ContactMultiselectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactMultiselectionControllerNode.swift; sourceTree = "<group>"; };
|
||||
D087751D1E3F579300A97350 /* CounterContollerTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CounterContollerTitleView.swift; sourceTree = "<group>"; };
|
||||
D087751F1E3F595000A97350 /* ContactListActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactListActionItem.swift; sourceTree = "<group>"; };
|
||||
D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePresentationSettings.swift; sourceTree = "<group>"; };
|
||||
D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputPanelEntries.swift; sourceTree = "<group>"; };
|
||||
D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputGridEntries.swift; sourceTree = "<group>"; };
|
||||
D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputStickerGridItem.swift; sourceTree = "<group>"; };
|
||||
@@ -1267,6 +1306,8 @@
|
||||
D0ACCB1B1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageCallBubbleContentNode.swift; sourceTree = "<group>"; };
|
||||
D0AF7C451ED84BC500CD8E0F /* LanguageSelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageSelectionController.swift; sourceTree = "<group>"; };
|
||||
D0AF7C491ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageSelectionControllerNode.swift; sourceTree = "<group>"; };
|
||||
D0AFCC781F4C8D2C000720C6 /* InstantPageSlideshowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSlideshowItem.swift; sourceTree = "<group>"; };
|
||||
D0AFCC7A1F4C8D39000720C6 /* InstantPageSlideshowItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSlideshowItemNode.swift; sourceTree = "<group>"; };
|
||||
D0B417C21D7DE54E004562A4 /* ChatPresentationInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPresentationInterfaceState.swift; sourceTree = "<group>"; };
|
||||
D0B4AF871EC112ED00D51FF6 /* CallKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CallKit.framework; path = System/Library/Frameworks/CallKit.framework; sourceTree = SDKROOT; };
|
||||
D0B4AF8A1EC1133600D51FF6 /* CallKitIntergation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitIntergation.swift; sourceTree = "<group>"; };
|
||||
@@ -1295,6 +1336,8 @@
|
||||
D0C0B5B01EE1C421000F4D2C /* ChatDateSelectionSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatDateSelectionSheet.swift; sourceTree = "<group>"; };
|
||||
D0C0B5B61EE1DEF1000F4D2C /* ThemeGridControllerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeGridControllerItem.swift; sourceTree = "<group>"; };
|
||||
D0C12A1B1F33964900B3F66D /* ChatWallpaperBuiltin0.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = ChatWallpaperBuiltin0.jpg; path = TelegramUI/Resources/ChatWallpaperBuiltin0.jpg; sourceTree = "<group>"; };
|
||||
D0C27B3A1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPagePlayableVideoItem.swift; sourceTree = "<group>"; };
|
||||
D0C27B3C1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPagePlayableVideoNode.swift; sourceTree = "<group>"; };
|
||||
D0C48F431E81D5110075317D /* ChatEmptyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatEmptyItem.swift; sourceTree = "<group>"; };
|
||||
D0C50DE81E93A07900F62E39 /* libtgvoip.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = libtgvoip.framework; path = "../libtgvoip/build/Debug-iphoneos/libtgvoip.framework"; sourceTree = "<group>"; };
|
||||
D0C50E281E93A33700F62E39 /* VoipDynamic.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VoipDynamic.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphoneos/VoipDynamic.framework"; sourceTree = "<group>"; };
|
||||
@@ -1984,6 +2027,9 @@
|
||||
D0FE4DDF1F0ACA8300E8A0B3 /* InstantVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantVideoNode.swift; sourceTree = "<group>"; };
|
||||
D0FE4DE31F0AEBB900E8A0B3 /* SharedVideoContextManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedVideoContextManager.swift; sourceTree = "<group>"; };
|
||||
D0FE4DE51F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayMediaItemNode.swift; sourceTree = "<group>"; };
|
||||
D0FFF7F51F55B82500BEBC01 /* InstantPageAudioItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageAudioItem.swift; sourceTree = "<group>"; };
|
||||
D0FFF7F71F55B83600BEBC01 /* InstantPageAudioNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageAudioNode.swift; sourceTree = "<group>"; };
|
||||
D0FFF7FE1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageMediaAudioPlaylist.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -2046,6 +2092,14 @@
|
||||
name = Share;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0104F261F471702004E4881 /* Instant Page Gallery */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0104F271F47171F004E4881 /* InstantPageGalleryController.swift */,
|
||||
);
|
||||
name = "Instant Page Gallery";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D017494F1E1067C00057C89A /* Hashtag Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -2499,6 +2553,7 @@
|
||||
D023ED311DDB60CF00BD496D /* LegacyNavigationController.swift */,
|
||||
D0EFD8951DDE8249009E508A /* LegacyLocationPicker.swift */,
|
||||
D04B4D651EEA993A00711AF6 /* LegacyLocationController.swift */,
|
||||
D06BB8811F58994B0084FC30 /* LegacyInstantVideoController.swift */,
|
||||
D0EB41F21F2FEAB800838FE6 /* LegacyComponentsStickers.swift */,
|
||||
D0EB41F41F30D26A00838FE6 /* LegacySuggestionContext.swift */,
|
||||
D0EB41F61F30D4A800838FE6 /* LegacyMediaLocations.swift */,
|
||||
@@ -2525,8 +2580,16 @@
|
||||
D0215D431E0413FB001A0B1E /* InstantPageTextStyleStack.swift */,
|
||||
D0215D451E041851001A0B1E /* InstantPageTextItem.swift */,
|
||||
D0215D471E041B90001A0B1E /* InstantPageAnchorItem.swift */,
|
||||
D0215D491E041CAF001A0B1E /* InstantPageMediaItem.swift */,
|
||||
D0215D4B1E041D5E001A0B1E /* InstantPageMediaNode.swift */,
|
||||
D0215D491E041CAF001A0B1E /* InstantPageImageItem.swift */,
|
||||
D0215D4B1E041D5E001A0B1E /* InstantPageImageNode.swift */,
|
||||
D0C27B3A1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift */,
|
||||
D0C27B3C1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift */,
|
||||
D0AFCC781F4C8D2C000720C6 /* InstantPageSlideshowItem.swift */,
|
||||
D0AFCC7A1F4C8D39000720C6 /* InstantPageSlideshowItemNode.swift */,
|
||||
D05677501F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift */,
|
||||
D05677521F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift */,
|
||||
D0FFF7F51F55B82500BEBC01 /* InstantPageAudioItem.swift */,
|
||||
D0FFF7F71F55B83600BEBC01 /* InstantPageAudioNode.swift */,
|
||||
D0215D4F1E0422C7001A0B1E /* InstantPageWebEmbedItem.swift */,
|
||||
D0215D4D1E042164001A0B1E /* InstantPageWebEmbedNode.swift */,
|
||||
D0215D511E0423EE001A0B1E /* InstantPageShapeItem.swift */,
|
||||
@@ -2536,6 +2599,14 @@
|
||||
D0215D551E043020001A0B1E /* InstantPageControllerNode.swift */,
|
||||
D01A21AE1F39EA2E00DDA104 /* InstantPageTheme.swift */,
|
||||
D01A21B01F3A050E00DDA104 /* InstantPageNavigationBar.swift */,
|
||||
D00FF2081F4E2414006FA332 /* InstantPageSettingsNode.swift */,
|
||||
D01C99771F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift */,
|
||||
D048EA8E1F4F2A9C00188713 /* InstantPageSettingsItemNode.swift */,
|
||||
D048EA841F4F295300188713 /* InstantPageSettingsBacklightItemNode.swift */,
|
||||
D048EA861F4F296400188713 /* InstantPageSettingsFontSizeItemNode.swift */,
|
||||
D048EA881F4F297500188713 /* InstantPageSettingsFontFamilyItemNode.swift */,
|
||||
D048EA8A1F4F298A00188713 /* InstantPageSettingsThemeItemNode.swift */,
|
||||
D048EA8C1F4F299A00188713 /* InstantPageSettingsSwitchItemNode.swift */,
|
||||
);
|
||||
name = "Instant Page";
|
||||
sourceTree = "<group>";
|
||||
@@ -2563,6 +2634,7 @@
|
||||
D0223A911EA5420C00211D94 /* GeneratedMediaStoreSettings.swift */,
|
||||
D0223A931EA5442C00211D94 /* VoiceCallSettings.swift */,
|
||||
D010C2C91EA7A59F00F41B96 /* PresentationThemeSettings.swift */,
|
||||
D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */,
|
||||
);
|
||||
name = Settings;
|
||||
sourceTree = "<group>";
|
||||
@@ -2680,6 +2752,7 @@
|
||||
D09E637B1F0E7C28003444CD /* SharedMediaPlayer.swift */,
|
||||
D09E637E1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift */,
|
||||
D0736F221DF496D000F2C02A /* PeerMediaAudioPlaylist.swift */,
|
||||
D0FFF7FE1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift */,
|
||||
);
|
||||
name = "Shared Media Player";
|
||||
sourceTree = "<group>";
|
||||
@@ -3883,6 +3956,7 @@
|
||||
children = (
|
||||
D099EA261DE765DB001AF5A8 /* ManagedMediaId.swift */,
|
||||
D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */,
|
||||
D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */,
|
||||
D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */,
|
||||
D0F02CD81E97ED080065DEE2 /* RecentGifManagedMediaId.swift */,
|
||||
D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */,
|
||||
@@ -4147,7 +4221,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0F69E401D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift */,
|
||||
D01F66121DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift */,
|
||||
D01F66121DE8903300345CBE /* ChatTextInputMediaRecordingButton.swift */,
|
||||
D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */,
|
||||
D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */,
|
||||
D039EB091DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift */,
|
||||
@@ -4178,8 +4252,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0B7F8DF1D8A17D20045D939 /* Collection */,
|
||||
D0575AF81EA0FD94006F2541 /* Avatar Gallery */,
|
||||
D0F69E4F1D6B8BC40046BCD6 /* Gallery */,
|
||||
D0575AF81EA0FD94006F2541 /* Avatar Gallery */,
|
||||
D0104F261F471702004E4881 /* Instant Page Gallery */,
|
||||
D0F69E671D6B8C030046BCD6 /* Map Input */,
|
||||
D07827CC1E03F32C00071108 /* Instant Page */,
|
||||
D0D748041E7AF62000F4B1F6 /* Stickers */,
|
||||
@@ -4217,6 +4292,8 @@
|
||||
D042C6891E8DAAB000C863B0 /* ChatItemGalleryFooterContentNode.swift */,
|
||||
D042C68B1E8DAB7100C863B0 /* ChatItemGalleryItemNode.swift */,
|
||||
D0575AFB1EA104A6006F2541 /* PeerAvatarImageGalleryItem.swift */,
|
||||
D0104F291F471DA6004E4881 /* InstantImageGalleryItem.swift */,
|
||||
D0104F2B1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift */,
|
||||
);
|
||||
name = Items;
|
||||
sourceTree = "<group>";
|
||||
@@ -4659,8 +4736,10 @@
|
||||
D0754D1E1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift in Sources */,
|
||||
D0E9BAC71F05738600F079A4 /* STPAPIClient.m in Sources */,
|
||||
D0EC6FEC1EBA182B00EBF1C3 /* aec_core_sse2.cc in Sources */,
|
||||
D089F78A1F4E0C14000E934D /* InstantPagePresentationSettings.swift in Sources */,
|
||||
D01776B51F1D6CCC0044446D /* RadialStatusContentNode.swift in Sources */,
|
||||
D0EC6FC81EBA135100EBF1C3 /* cross_correlation.c in Sources */,
|
||||
D01C99781F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift in Sources */,
|
||||
D0EC6FB11EBA112600EBF1C3 /* ooura_fft_neon.cc in Sources */,
|
||||
D0EC6CC21EB9F58800EBF1C3 /* LegacyEmptyController.swift in Sources */,
|
||||
D0EC6CC31EB9F58800EBF1C3 /* LegacyNavigationController.swift in Sources */,
|
||||
@@ -4669,6 +4748,7 @@
|
||||
D0EC6FA41EBA10EA00EBF1C3 /* stringutils.cc in Sources */,
|
||||
D0EC6CC61EB9F58800EBF1C3 /* PresenceStrings.swift in Sources */,
|
||||
D0EC6CC71EB9F58800EBF1C3 /* PeerNotificationSoundStrings.swift in Sources */,
|
||||
D0104F281F47171F004E4881 /* InstantPageGalleryController.swift in Sources */,
|
||||
D0EC6CC81EB9F58800EBF1C3 /* ProgressiveImage.swift in Sources */,
|
||||
D0EC6CC91EB9F58800EBF1C3 /* WebP.swift in Sources */,
|
||||
D0EC6CCA1EB9F58800EBF1C3 /* PeerPresenceStatusManager.swift in Sources */,
|
||||
@@ -4685,6 +4765,7 @@
|
||||
D0EC6EA71EBA0FB000EBF1C3 /* BlockingQueue.cpp in Sources */,
|
||||
D0EC6CD01EB9F58800EBF1C3 /* PerformanceSpinner.swift in Sources */,
|
||||
D0471B5C1EFEB4F30074D609 /* BotPaymentFieldItemNode.swift in Sources */,
|
||||
D0C27B3D1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift in Sources */,
|
||||
D0EC6CD11EB9F58800EBF1C3 /* UrlHandling.swift in Sources */,
|
||||
D0EC6FE31EBA135100EBF1C3 /* spl_sqrt.c in Sources */,
|
||||
D0E9BAD21F0573C000F079A4 /* STPToken.m in Sources */,
|
||||
@@ -4725,6 +4806,7 @@
|
||||
D0EC6CE91EB9F58800EBF1C3 /* DefaultDarkPresentationTheme.swift in Sources */,
|
||||
D0EC6FB01EBA112600EBF1C3 /* ooura_fft.cc in Sources */,
|
||||
D0EC6CEA1EB9F58800EBF1C3 /* DefaultPresentationStrings.swift in Sources */,
|
||||
D0C27B3B1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift in Sources */,
|
||||
D0EC6CEB1EB9F58800EBF1C3 /* Wallpapers.swift in Sources */,
|
||||
D0EC6CEC1EB9F58800EBF1C3 /* PresentationThemeEssentialGraphics.swift in Sources */,
|
||||
D01BAA1E1ECC931D00295217 /* CallListNodeEntries.swift in Sources */,
|
||||
@@ -4773,6 +4855,7 @@
|
||||
D0EC6D0A1EB9F58800EBF1C3 /* internal.c in Sources */,
|
||||
D0EC6D0B1EB9F58800EBF1C3 /* opusfile.c in Sources */,
|
||||
D0B4AF8B1EC1133600D51FF6 /* CallKitIntergation.swift in Sources */,
|
||||
D0FFF7F61F55B82500BEBC01 /* InstantPageAudioItem.swift in Sources */,
|
||||
D0EC6D0C1EB9F58800EBF1C3 /* stream.c in Sources */,
|
||||
D0EC6D0D1EB9F58800EBF1C3 /* MediaFrameSource.swift in Sources */,
|
||||
D0EC6FA71EBA111500EBF1C3 /* ring_buffer.c in Sources */,
|
||||
@@ -4807,20 +4890,24 @@
|
||||
D0EC6D221EB9F58800EBF1C3 /* PhotoResources.swift in Sources */,
|
||||
D0EC6FC11EBA132B00EBF1C3 /* nsx_core_neon.c in Sources */,
|
||||
D0EC6FA81EBA111500EBF1C3 /* sparse_fir_filter.cc in Sources */,
|
||||
D048EA871F4F296400188713 /* InstantPageSettingsFontSizeItemNode.swift in Sources */,
|
||||
D0EC6D231EB9F58800EBF1C3 /* StickerResources.swift in Sources */,
|
||||
D0EC6D241EB9F58800EBF1C3 /* CachedResourceRepresentations.swift in Sources */,
|
||||
D01BAA201ECC9A2500295217 /* CallListNodeLocation.swift in Sources */,
|
||||
D0EC6D251EB9F58800EBF1C3 /* FetchCachedRepresentations.swift in Sources */,
|
||||
D0EC6D261EB9F58800EBF1C3 /* TransformOutgoingMessageMedia.swift in Sources */,
|
||||
D0EC6D271EB9F58800EBF1C3 /* FetchResource.swift in Sources */,
|
||||
D048EA8F1F4F2A9C00188713 /* InstantPageSettingsItemNode.swift in Sources */,
|
||||
D0EC6FDE1EBA135100EBF1C3 /* resample_by_2.c in Sources */,
|
||||
D0EC6D281EB9F58800EBF1C3 /* MediaResources.swift in Sources */,
|
||||
D0E9BA671F055B5500F079A4 /* BotCheckoutNativeCardEntryControllerNode.swift in Sources */,
|
||||
D0EC6D291EB9F58800EBF1C3 /* FetchVideoMediaResource.swift in Sources */,
|
||||
D0AFCC791F4C8D2C000720C6 /* InstantPageSlideshowItem.swift in Sources */,
|
||||
D0EC6FA51EBA111500EBF1C3 /* audio_util.cc in Sources */,
|
||||
D02660941F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift in Sources */,
|
||||
D0EC6FFD1EBA1F2400EBF1C3 /* OngoingCallThreadLocalContext.mm in Sources */,
|
||||
D0E9BAE21F0574D800F079A4 /* STPBankAccount.m in Sources */,
|
||||
D0104F2A1F471DA6004E4881 /* InstantImageGalleryItem.swift in Sources */,
|
||||
D0F67FF41EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift in Sources */,
|
||||
D0E9BA471F0559A500F079A4 /* NSDictionary+Stripe.m in Sources */,
|
||||
D0EC6D2A1EB9F58800EBF1C3 /* FetchPhotoLibraryImageResource.swift in Sources */,
|
||||
@@ -4849,6 +4936,7 @@
|
||||
D0EC6D3B1EB9F58800EBF1C3 /* EditableTokenListNode.swift in Sources */,
|
||||
D0EC6D3C1EB9F58800EBF1C3 /* PhoneInputNode.swift in Sources */,
|
||||
D0EC6D3D1EB9F58800EBF1C3 /* ProgressNavigationButtonNode.swift in Sources */,
|
||||
D0FFF7FF1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift in Sources */,
|
||||
D01BAA581ED3283D00295217 /* AddFormatToStringWithRanges.swift in Sources */,
|
||||
D0EC6D3E1EB9F58800EBF1C3 /* TelegramController.swift in Sources */,
|
||||
D0EB42011F30ED4F00838FE6 /* LegacyImageProcessors.m in Sources */,
|
||||
@@ -4867,6 +4955,7 @@
|
||||
D0EC6D511EB9F58800EBF1C3 /* ChatListViewTransition.swift in Sources */,
|
||||
D0EC6D521EB9F58800EBF1C3 /* ChatListNodeLocation.swift in Sources */,
|
||||
D0EC6D531EB9F58800EBF1C3 /* ChatHistoryViewForLocation.swift in Sources */,
|
||||
D06BB8821F58994B0084FC30 /* LegacyInstantVideoController.swift in Sources */,
|
||||
D0EC6D541EB9F58800EBF1C3 /* ChatHistoryEntriesForView.swift in Sources */,
|
||||
D0EC6D551EB9F58800EBF1C3 /* PreparedChatHistoryViewTransition.swift in Sources */,
|
||||
D0EB41FB1F30E75000838FE6 /* LegacyImageDownloadActor.swift in Sources */,
|
||||
@@ -4885,6 +4974,7 @@
|
||||
D0EC6D5E1EB9F58800EBF1C3 /* ListMessageHoleItem.swift in Sources */,
|
||||
D0EC6EB21EBA0FBB00EBF1C3 /* OpusEncoder.cpp in Sources */,
|
||||
D0EC6D5F1EB9F58800EBF1C3 /* GridMessageItem.swift in Sources */,
|
||||
D048EA851F4F295300188713 /* InstantPageSettingsBacklightItemNode.swift in Sources */,
|
||||
D0EC6D601EB9F58800EBF1C3 /* GridHoleItem.swift in Sources */,
|
||||
D0EC6D611EB9F58800EBF1C3 /* GridMessageSelectionNode.swift in Sources */,
|
||||
D0754D201EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift in Sources */,
|
||||
@@ -4923,6 +5013,7 @@
|
||||
D0EC6D761EB9F58800EBF1C3 /* ChatListController.swift in Sources */,
|
||||
D0EC6D771EB9F58800EBF1C3 /* ChatListControllerNode.swift in Sources */,
|
||||
D0EC6D781EB9F58800EBF1C3 /* NetworkStatusTitleView.swift in Sources */,
|
||||
D048EA8D1F4F299A00188713 /* InstantPageSettingsSwitchItemNode.swift in Sources */,
|
||||
D0EC6FAC1EBA112600EBF1C3 /* three_band_filter_bank.cc in Sources */,
|
||||
D0E9B9F41F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift in Sources */,
|
||||
D0F6800A1EE750EE000E5906 /* ChannelBannedMemberController.swift in Sources */,
|
||||
@@ -4937,6 +5028,7 @@
|
||||
D0EC6D7F1EB9F58800EBF1C3 /* HashtagSearchController.swift in Sources */,
|
||||
D0EC6D801EB9F58800EBF1C3 /* HashtagSearchControllerNode.swift in Sources */,
|
||||
D0EC6D811EB9F58800EBF1C3 /* ChatController.swift in Sources */,
|
||||
D0FFF7F81F55B83600BEBC01 /* InstantPageAudioNode.swift in Sources */,
|
||||
D0EC6D821EB9F58800EBF1C3 /* ChatControllerInteraction.swift in Sources */,
|
||||
D0EC6D831EB9F58800EBF1C3 /* ChatControllerNode.swift in Sources */,
|
||||
D0E9BA231F05577700F079A4 /* STPCard.m in Sources */,
|
||||
@@ -4960,6 +5052,7 @@
|
||||
D0EC6D921EB9F58900EBF1C3 /* ChatMessageDateAndStatusNode.swift in Sources */,
|
||||
D0EC6D931EB9F58900EBF1C3 /* ChatMessageFileBubbleContentNode.swift in Sources */,
|
||||
D0EC6D941EB9F58900EBF1C3 /* ChatMessageForwardInfoNode.swift in Sources */,
|
||||
D0104F2C1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift in Sources */,
|
||||
D0754D241EEE0F4100884F6E /* ChatMessageInteractiveMediaLabelNode.swift in Sources */,
|
||||
D04B4D131EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift in Sources */,
|
||||
D0EC6FB81EBA114200EBF1C3 /* aecm_core_neon.cc in Sources */,
|
||||
@@ -4997,6 +5090,7 @@
|
||||
D0EC6DA81EB9F58900EBF1C3 /* ChatInterfaceState.swift in Sources */,
|
||||
D0EC6DA91EB9F58900EBF1C3 /* ChatPresentationInterfaceState.swift in Sources */,
|
||||
D0EC6DAA1EB9F58900EBF1C3 /* ChatPanelInterfaceInteraction.swift in Sources */,
|
||||
D00FF2091F4E2414006FA332 /* InstantPageSettingsNode.swift in Sources */,
|
||||
D0EC6DAB1EB9F58900EBF1C3 /* ChatInterfaceStateAccessoryPanels.swift in Sources */,
|
||||
D0EC6DAC1EB9F58900EBF1C3 /* ChatInterfaceStateInputPanels.swift in Sources */,
|
||||
D0EB41F31F2FEAB800838FE6 /* LegacyComponentsStickers.swift in Sources */,
|
||||
@@ -5069,7 +5163,7 @@
|
||||
D0E9BA081F0446A300F079A4 /* BotCheckoutPaymentShippingOptionSheetController.swift in Sources */,
|
||||
D0EC6DDC1EB9F58900EBF1C3 /* ChatTextInputPanelNode.swift in Sources */,
|
||||
D0EB41F51F30D26A00838FE6 /* LegacySuggestionContext.swift in Sources */,
|
||||
D0EC6DDD1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingButton.swift in Sources */,
|
||||
D0EC6DDD1EB9F58900EBF1C3 /* ChatTextInputMediaRecordingButton.swift in Sources */,
|
||||
D0F0AAE61EC21B68005EE2A5 /* CallControllerButton.swift in Sources */,
|
||||
D0EC6DDE1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */,
|
||||
D0E9BAC91F05738600F079A4 /* STPAPIClient+ApplePay.m in Sources */,
|
||||
@@ -5107,6 +5201,7 @@
|
||||
D0EC6DF81EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceState.swift in Sources */,
|
||||
D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */,
|
||||
D0EC6DF91EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceStateButtons.swift in Sources */,
|
||||
D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */,
|
||||
D0EC6DFA1EB9F58900EBF1C3 /* PeerMediaCollectionModeSelectionNode.swift in Sources */,
|
||||
D0EC6DFB1EB9F58900EBF1C3 /* AvatarGalleryController.swift in Sources */,
|
||||
D0EC6DFC1EB9F58900EBF1C3 /* GalleryController.swift in Sources */,
|
||||
@@ -5116,6 +5211,7 @@
|
||||
D0EC6FC71EBA135100EBF1C3 /* copy_set_operations.c in Sources */,
|
||||
D0EC6DFF1EB9F58900EBF1C3 /* GalleryItem.swift in Sources */,
|
||||
D0EC6E001EB9F58900EBF1C3 /* GalleryItemNode.swift in Sources */,
|
||||
D048EA8B1F4F298A00188713 /* InstantPageSettingsThemeItemNode.swift in Sources */,
|
||||
D0EC6E011EB9F58900EBF1C3 /* GalleryPagerNode.swift in Sources */,
|
||||
D0EC6E021EB9F58900EBF1C3 /* GalleryFooterNode.swift in Sources */,
|
||||
D0EC6E031EB9F58900EBF1C3 /* GalleryFooterContentNode.swift in Sources */,
|
||||
@@ -5125,6 +5221,7 @@
|
||||
D0EC6E061EB9F58900EBF1C3 /* ChatDocumentGalleryItem.swift in Sources */,
|
||||
D0EC6E071EB9F58900EBF1C3 /* ChatHoleGalleryItem.swift in Sources */,
|
||||
D0EC6E081EB9F58900EBF1C3 /* ChatImageGalleryItem.swift in Sources */,
|
||||
D048EA891F4F297500188713 /* InstantPageSettingsFontFamilyItemNode.swift in Sources */,
|
||||
D0EC6E091EB9F58900EBF1C3 /* ChatVideoGalleryItem.swift in Sources */,
|
||||
D0EC6E0A1EB9F58900EBF1C3 /* ChatVideoGalleryItemScrubberView.swift in Sources */,
|
||||
D0EC6FC31EBA135100EBF1C3 /* auto_correlation.c in Sources */,
|
||||
@@ -5138,6 +5235,7 @@
|
||||
D0EC6E0F1EB9F58900EBF1C3 /* MapInputController.swift in Sources */,
|
||||
D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */,
|
||||
D0EC6E101EB9F58900EBF1C3 /* MapInputControllerNode.swift in Sources */,
|
||||
D0AFCC7B1F4C8D39000720C6 /* InstantPageSlideshowItemNode.swift in Sources */,
|
||||
D0E9BA211F05577700F079A4 /* STPCardParams.m in Sources */,
|
||||
D0EC6E111EB9F58900EBF1C3 /* InstantPageNode.swift in Sources */,
|
||||
D0EC6E121EB9F58900EBF1C3 /* InstantPageLayout.swift in Sources */,
|
||||
@@ -5150,10 +5248,11 @@
|
||||
D0EC6E171EB9F58900EBF1C3 /* InstantPageTextStyleStack.swift in Sources */,
|
||||
D0EC6E181EB9F58900EBF1C3 /* InstantPageTextItem.swift in Sources */,
|
||||
D0EC6E191EB9F58900EBF1C3 /* InstantPageAnchorItem.swift in Sources */,
|
||||
D0EC6E1A1EB9F58900EBF1C3 /* InstantPageMediaItem.swift in Sources */,
|
||||
D05677531F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift in Sources */,
|
||||
D0EC6E1A1EB9F58900EBF1C3 /* InstantPageImageItem.swift in Sources */,
|
||||
D0EC6FD81EBA135100EBF1C3 /* min_max_operations_neon.c in Sources */,
|
||||
D0EC6FB71EBA114200EBF1C3 /* aecm_core_c.cc in Sources */,
|
||||
D0EC6E1B1EB9F58900EBF1C3 /* InstantPageMediaNode.swift in Sources */,
|
||||
D0EC6E1B1EB9F58900EBF1C3 /* InstantPageImageNode.swift in Sources */,
|
||||
D0EC6E1C1EB9F58900EBF1C3 /* InstantPageWebEmbedItem.swift in Sources */,
|
||||
D0EC6E1D1EB9F58900EBF1C3 /* InstantPageWebEmbedNode.swift in Sources */,
|
||||
D0EC6E1E1EB9F58900EBF1C3 /* InstantPageShapeItem.swift in Sources */,
|
||||
@@ -5301,6 +5400,7 @@
|
||||
D0EC6E861EB9F58900EBF1C3 /* UIImage+WebP.m in Sources */,
|
||||
D0EC6E871EB9F58900EBF1C3 /* FastBlur.m in Sources */,
|
||||
D0ACCB1C1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift in Sources */,
|
||||
D05677511F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift in Sources */,
|
||||
D0EC6E881EB9F58900EBF1C3 /* FFMpegSwResample.m in Sources */,
|
||||
D0EC6E891EB9F58900EBF1C3 /* FrameworkBundle.swift in Sources */,
|
||||
D0EC6E8B1EB9F58900EBF1C3 /* RingBuffer.m in Sources */,
|
||||
@@ -5462,8 +5562,10 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
COPY_PHASE_STRIP = YES;
|
||||
DEVELOPMENT_TEAM = X834Q8SBVP;
|
||||
HEADERMAP_USES_VFS = YES;
|
||||
INFOPLIST_FILE = TelegramUITests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 3.1;
|
||||
@@ -5539,8 +5641,10 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
DEVELOPMENT_TEAM = X834Q8SBVP;
|
||||
HEADERMAP_USES_VFS = YES;
|
||||
INFOPLIST_FILE = TelegramUITests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 3.1;
|
||||
@@ -5574,6 +5678,7 @@
|
||||
"-DWEBRTC_POSIX",
|
||||
);
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
OTHER_SWIFT_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI;
|
||||
PRODUCT_NAME = TelegramUI;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -5610,6 +5715,7 @@
|
||||
"-DWEBRTC_POSIX",
|
||||
);
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
OTHER_SWIFT_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI;
|
||||
PRODUCT_NAME = TelegramUI;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -5645,6 +5751,7 @@
|
||||
"-DWEBRTC_POSIX",
|
||||
);
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
OTHER_SWIFT_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI;
|
||||
PRODUCT_NAME = TelegramUI;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -5678,6 +5785,7 @@
|
||||
"-DWEBRTC_POSIX",
|
||||
);
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
OTHER_SWIFT_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI;
|
||||
PRODUCT_NAME = TelegramUI;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -5812,8 +5920,10 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
DEVELOPMENT_TEAM = X834Q8SBVP;
|
||||
HEADERMAP_USES_VFS = YES;
|
||||
INFOPLIST_FILE = TelegramUITests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 3.1;
|
||||
@@ -5827,8 +5937,10 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
COPY_PHASE_STRIP = YES;
|
||||
DEVELOPMENT_TEAM = X834Q8SBVP;
|
||||
HEADERMAP_USES_VFS = YES;
|
||||
INFOPLIST_FILE = TelegramUITests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 3.1;
|
||||
|
||||
@@ -236,7 +236,7 @@ public final class AuthorizationSequenceController: NavigationController {
|
||||
return controller
|
||||
}
|
||||
|
||||
private func updateState(state: Coding?) {
|
||||
private func updateState(state: PostboxCoding?) {
|
||||
if let state = state as? UnauthorizedAccountState {
|
||||
switch state.contents {
|
||||
case .empty:
|
||||
|
||||
@@ -3,7 +3,7 @@ import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
public struct AutomaticMediaDownloadCategoryPeers: Coding, Equatable {
|
||||
public struct AutomaticMediaDownloadCategoryPeers: PostboxCoding, Equatable {
|
||||
public let privateChats: Bool
|
||||
public let groupsAndChannels: Bool
|
||||
|
||||
@@ -12,12 +12,12 @@ public struct AutomaticMediaDownloadCategoryPeers: Coding, Equatable {
|
||||
self.groupsAndChannels = groupsAndChannels
|
||||
}
|
||||
|
||||
public init(decoder: Decoder) {
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.privateChats = decoder.decodeInt32ForKey("p", orElse: 0) != 0
|
||||
self.groupsAndChannels = decoder.decodeInt32ForKey("g", orElse: 0) != 0
|
||||
}
|
||||
|
||||
public func encode(_ encoder: Encoder) {
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.privateChats ? 1 : 0, forKey: "p")
|
||||
encoder.encodeInt32(self.groupsAndChannels ? 1 : 0, forKey: "g")
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public struct AutomaticMediaDownloadCategoryPeers: Coding, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct AutomaticMediaDownloadCategories: Coding, Equatable {
|
||||
public struct AutomaticMediaDownloadCategories: PostboxCoding, Equatable {
|
||||
public let photo: AutomaticMediaDownloadCategoryPeers
|
||||
public let voice: AutomaticMediaDownloadCategoryPeers
|
||||
public let instantVideo: AutomaticMediaDownloadCategoryPeers
|
||||
@@ -86,14 +86,14 @@ public struct AutomaticMediaDownloadCategories: Coding, Equatable {
|
||||
self.gif = gif
|
||||
}
|
||||
|
||||
public init(decoder: Decoder) {
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.photo = decoder.decodeObjectForKey("p", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers
|
||||
self.voice = decoder.decodeObjectForKey("v", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers
|
||||
self.instantVideo = decoder.decodeObjectForKey("iv", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers
|
||||
self.gif = decoder.decodeObjectForKey("g", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers
|
||||
}
|
||||
|
||||
public func encode(_ encoder: Encoder) {
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObject(self.photo, forKey: "p")
|
||||
encoder.encodeObject(self.voice, forKey: "v")
|
||||
encoder.encodeObject(self.instantVideo, forKey: "iv")
|
||||
@@ -150,12 +150,12 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable {
|
||||
self.saveIncomingPhotos = saveIncomingPhotos
|
||||
}
|
||||
|
||||
public init(decoder: Decoder) {
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.categories = decoder.decodeObjectForKey("c", decoder: { AutomaticMediaDownloadCategories(decoder: $0) }) as! AutomaticMediaDownloadCategories
|
||||
self.saveIncomingPhotos = decoder.decodeInt32ForKey("siph", orElse: 0) != 0
|
||||
}
|
||||
|
||||
public func encode(_ encoder: Encoder) {
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObject(self.categories, forKey: "c")
|
||||
encoder.encodeInt32(self.saveIncomingPhotos ? 1 : 0, forKey: "siph")
|
||||
}
|
||||
|
||||
@@ -41,6 +41,18 @@ private final class BotCheckoutInfoAddressItems {
|
||||
private final class BotCheckoutInfoControllerScrollerNodeView: UIScrollView {
|
||||
var ignoreUpdateBounds = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override var bounds: CGRect {
|
||||
get {
|
||||
return super.bounds
|
||||
@@ -64,7 +76,7 @@ private final class BotCheckoutInfoControllerScrollerNode: ASDisplayNode {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return BotCheckoutInfoControllerScrollerNodeView()
|
||||
return BotCheckoutInfoControllerScrollerNodeView(frame: CGRect())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,9 +372,9 @@ final class CallListControllerNode: ASDisplayNode {
|
||||
|
||||
func scrollToLatest() {
|
||||
if let view = self.callListView?.originalView, view.later == nil {
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
} else {
|
||||
let location: CallListNodeLocation = .scroll(index: MessageIndex.absoluteUpperBound(), sourceIndex: MessageIndex.absoluteLowerBound(), scrollPosition: .Top, animated: true)
|
||||
let location: CallListNodeLocation = .scroll(index: MessageIndex.absoluteUpperBound(), sourceIndex: MessageIndex.absoluteLowerBound(), scrollPosition: .top(0.0), animated: true)
|
||||
self.currentLocationAndType = CallListNodeLocationAndType(location: location, type: self.currentLocationAndType.type)
|
||||
self.callListLocationAndType.set(self.currentLocationAndType)
|
||||
}
|
||||
|
||||
@@ -58,6 +58,14 @@ final class ChatButtonKeyboardInputNode: ChatInputNode {
|
||||
self.addSubnode(self.separatorNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
}
|
||||
|
||||
private func heightForWidth(width: CGFloat) -> CGFloat {
|
||||
return defaultPortraitPanelHeight
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import SafariServices
|
||||
public class ChatController: TelegramController {
|
||||
private var containerLayout = ContainerViewLayout()
|
||||
|
||||
public let v = 1
|
||||
|
||||
private let account: Account
|
||||
public let peerId: PeerId
|
||||
private let messageId: MessageId?
|
||||
@@ -66,6 +68,11 @@ public class ChatController: TelegramController {
|
||||
private var audioRecorder = Promise<ManagedAudioRecorder?>()
|
||||
private var audioRecorderDisposable: Disposable?
|
||||
|
||||
private var videoRecorderValue: InstantVideoController?
|
||||
private var tempVideoRecorderValue: InstantVideoController?
|
||||
private var videoRecorder = Promise<InstantVideoController?>()
|
||||
private var videoRecorderDisposable: Disposable?
|
||||
|
||||
private var buttonKeyboardMessageDisposable: Disposable?
|
||||
private var cachedDataDisposable: Disposable?
|
||||
private var chatUnreadCountDisposable: Disposable?
|
||||
@@ -742,11 +749,11 @@ public class ChatController: TelegramController {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
$0.updatedInputTextPanelState { panelState in
|
||||
if let audioRecorder = audioRecorder {
|
||||
if panelState.audioRecordingState == nil {
|
||||
return panelState.withUpdatedAudioRecordingState(ChatTextInputPanelAudioRecordingState(recorder: audioRecorder))
|
||||
if panelState.mediaRecordingState == nil {
|
||||
return panelState.withUpdatedMediaRecordingState(.audio(recorder: audioRecorder, isLocked: false))
|
||||
}
|
||||
} else {
|
||||
return panelState.withUpdatedAudioRecordingState(nil)
|
||||
return panelState.withUpdatedMediaRecordingState(nil)
|
||||
}
|
||||
return panelState
|
||||
}
|
||||
@@ -759,6 +766,45 @@ public class ChatController: TelegramController {
|
||||
}
|
||||
})
|
||||
|
||||
self.videoRecorderDisposable = (self.videoRecorder.get() |> deliverOnMainQueue).start(next: { [weak self] videoRecorder in
|
||||
if let strongSelf = self {
|
||||
if strongSelf.videoRecorderValue !== videoRecorder {
|
||||
let previousVideoRecorderValue = strongSelf.videoRecorderValue
|
||||
strongSelf.videoRecorderValue = videoRecorder
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
$0.updatedInputTextPanelState { panelState in
|
||||
if let videoRecorder = videoRecorder {
|
||||
if panelState.mediaRecordingState == nil {
|
||||
return panelState.withUpdatedMediaRecordingState(.video(status: .recording(videoRecorder.audioStatus), isLocked: false))
|
||||
}
|
||||
} else {
|
||||
return panelState.withUpdatedMediaRecordingState(nil)
|
||||
}
|
||||
return panelState
|
||||
}
|
||||
})
|
||||
|
||||
if let videoRecorder = videoRecorder {
|
||||
videoRecorder.onDismiss = {
|
||||
if let strongSelf = self {
|
||||
strongSelf.videoRecorder.set(.single(nil))
|
||||
}
|
||||
}
|
||||
strongSelf.present(videoRecorder, in: .window(.root))
|
||||
}
|
||||
|
||||
if let previousVideoRecorderValue = previousVideoRecorderValue {
|
||||
previousVideoRecorderValue.dismissVideo()
|
||||
}
|
||||
|
||||
/*if let videoRecorder = videoRecorder {
|
||||
videoRecorder.start()
|
||||
}*/
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if let botStart = botStart, case .automatic = botStart.behavior {
|
||||
self.startBot(botStart.payload)
|
||||
}
|
||||
@@ -817,6 +863,7 @@ public class ChatController: TelegramController {
|
||||
self.contextQueryState?.1.dispose()
|
||||
self.urlPreviewQueryState?.1.dispose()
|
||||
self.audioRecorderDisposable?.dispose()
|
||||
self.videoRecorderDisposable?.dispose()
|
||||
self.buttonKeyboardMessageDisposable?.dispose()
|
||||
self.cachedDataDisposable?.dispose()
|
||||
self.resolveUrlDisposable?.dispose()
|
||||
@@ -898,6 +945,14 @@ public class ChatController: TelegramController {
|
||||
}
|
||||
})
|
||||
})
|
||||
if let readStateData = combinedInitialData.readStateData {
|
||||
let globalRemainingUnreadCount = readStateData.totalUnreadCount - readStateData.unreadCount
|
||||
if globalRemainingUnreadCount > 0 {
|
||||
strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)"
|
||||
} else {
|
||||
strongSelf.navigationItem.badge = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1064,14 +1119,14 @@ public class ChatController: TelegramController {
|
||||
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(duration: 0.4), directionHint: .Up)
|
||||
let scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: 0.4), directionHint: .Up)
|
||||
|
||||
var stationaryItemRange: (Int, Int)?
|
||||
if let maxInsertedItem = maxInsertedItem {
|
||||
stationaryItemRange = (maxInsertedItem + 1, Int.max)
|
||||
}
|
||||
|
||||
mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, scrolledToIndex: transition.scrolledToIndex), updateSizeAndInsets)
|
||||
mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex), updateSizeAndInsets)
|
||||
})
|
||||
|
||||
if let mappedTransition = mappedTransition {
|
||||
@@ -1193,10 +1248,17 @@ public class ChatController: TelegramController {
|
||||
|
||||
self.chatDisplayNode.navigateButtons.mentionsPressed = { [weak self] in
|
||||
if let strongSelf = self, strongSelf.isNodeLoaded {
|
||||
let signal = earliestUnseenPersonalMentionMessage(postbox: strongSelf.account.postbox, peerId: strongSelf.peerId)
|
||||
strongSelf.navigationActionDisposable.set((signal |> take(1) |> deliverOnMainQueue).start(next: { messageId in
|
||||
if let strongSelf = self, let messageId = messageId {
|
||||
strongSelf.navigateToMessage(from: nil, to: messageId)
|
||||
let signal = earliestUnseenPersonalMentionMessage(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: strongSelf.peerId)
|
||||
strongSelf.navigationActionDisposable.set((signal |> deliverOnMainQueue).start(next: { result in
|
||||
if let strongSelf = self {
|
||||
switch result {
|
||||
case let .result(messageId):
|
||||
if let messageId = messageId {
|
||||
strongSelf.navigateToMessage(from: nil, to: messageId)
|
||||
}
|
||||
case .loading:
|
||||
break
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -1522,10 +1584,33 @@ public class ChatController: TelegramController {
|
||||
if let strongSelf = self {
|
||||
strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .automatic(returnToPeerId: strongSelf.peerId))), fromMessageId: nil)
|
||||
}
|
||||
}, beginAudioRecording: { [weak self] in
|
||||
self?.requestAudioRecorder()
|
||||
}, finishAudioRecording: { [weak self] sendAudio in
|
||||
self?.dismissAudioRecorder(sendAudio: sendAudio)
|
||||
}, beginMediaRecording: { [weak self] isVideo in
|
||||
if let strongSelf = self {
|
||||
if isVideo {
|
||||
strongSelf.requestVideoRecorder()
|
||||
} else {
|
||||
strongSelf.requestAudioRecorder()
|
||||
}
|
||||
}
|
||||
}, finishMediaRecording: { [weak self] sendMedia in
|
||||
self?.dismissMediaRecorder(sendMedia: sendMedia)
|
||||
}, stopMediaRecording: { [weak self] in
|
||||
self?.stopMediaRecorder()
|
||||
}, lockMediaRecording: { [weak self] in
|
||||
self?.lockMediaRecorder()
|
||||
}, switchMediaRecordingMode: { [weak self] in
|
||||
self?.updateChatPresentationInterfaceState(interactive: true, {
|
||||
return $0.updatedInterfaceState { current in
|
||||
let mode: ChatTextInputMediaRecordingButtonMode
|
||||
switch current.mediaRecordingMode {
|
||||
case .audio:
|
||||
mode = .video
|
||||
case .video:
|
||||
mode = .audio
|
||||
}
|
||||
return current.withUpdatedMediaRecordingMode(mode)
|
||||
}
|
||||
})
|
||||
}, setupMessageAutoremoveTimeout: { [weak self] in
|
||||
if let strongSelf = self, strongSelf.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
@@ -1645,16 +1730,29 @@ public class ChatController: TelegramController {
|
||||
} |> switchToLatest).start()
|
||||
}
|
||||
}
|
||||
}, presentController: { [weak self] controller in
|
||||
self?.present(controller, in: .window(.root))
|
||||
}, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get()))
|
||||
|
||||
self.chatUnreadCountDisposable = (self.account.postbox.unreadMessageCountsView(items: [.peer(self.peerId)]) |> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
self.chatUnreadCountDisposable = (self.account.postbox.unreadMessageCountsView(items: [.peer(self.peerId), .total]) |> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
if let strongSelf = self {
|
||||
var unreadCount: Int32 = 0
|
||||
if let count = items.count(for: .peer(strongSelf.peerId)) {
|
||||
unreadCount = count
|
||||
}
|
||||
var totalCount: Int32 = 0
|
||||
if let count = items.count(for: .total) {
|
||||
totalCount = count
|
||||
}
|
||||
|
||||
strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount
|
||||
|
||||
let globalRemainingUnreadCount = totalCount - unreadCount
|
||||
if globalRemainingUnreadCount > 0 {
|
||||
strongSelf.navigationItem.badge = "\(globalRemainingUnreadCount)"
|
||||
} else {
|
||||
strongSelf.navigationItem.badge = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1737,7 +1835,8 @@ public class ChatController: TelegramController {
|
||||
|
||||
self.chatDisplayNode.historyNode.canReadHistory.set(.single(false))
|
||||
let timestamp = Int32(Date().timeIntervalSince1970)
|
||||
let interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp)
|
||||
let scrollState = self.chatDisplayNode.historyNode.immediateScrollState()
|
||||
let interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp).withUpdatedHistoryScrollState(scrollState)
|
||||
let _ = updatePeerChatInterfaceState(account: account, peerId: self.peerId, state: interfaceState).start()
|
||||
}
|
||||
|
||||
@@ -2044,10 +2143,18 @@ public class ChatController: TelegramController {
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissAudioRecorder(sendAudio: Bool) {
|
||||
private func requestVideoRecorder() {
|
||||
if self.videoRecorderValue == nil {
|
||||
if let currentInputPanelFrame = self.chatDisplayNode.currentInputPanelFrame() {
|
||||
self.videoRecorder.set(.single(legacyInstantVideoController(theme: self.presentationData.theme, panelFrame: currentInputPanelFrame, account: self.account, peerId: self.peerId)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissMediaRecorder(sendMedia: Bool) {
|
||||
if let audioRecorderValue = self.audioRecorderValue {
|
||||
audioRecorderValue.stop()
|
||||
if sendAudio {
|
||||
if sendMedia {
|
||||
let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self] data in
|
||||
if let strongSelf = self, let data = data {
|
||||
if data.duration < 0.5 {
|
||||
@@ -2074,8 +2181,45 @@ public class ChatController: TelegramController {
|
||||
}
|
||||
})
|
||||
}
|
||||
self.audioRecorder.set(.single(nil))
|
||||
} else if let videoRecorderValue = self.videoRecorderValue {
|
||||
if sendMedia {
|
||||
videoRecorderValue.completeVideo()
|
||||
self.tempVideoRecorderValue = videoRecorderValue
|
||||
self.videoRecorder.set(.single(nil))
|
||||
} else {
|
||||
self.videoRecorder.set(.single(nil))
|
||||
}
|
||||
}
|
||||
self.audioRecorder.set(.single(nil))
|
||||
}
|
||||
|
||||
private func stopMediaRecorder() {
|
||||
if let audioRecorderValue = self.audioRecorderValue {
|
||||
audioRecorderValue.stop()
|
||||
self.audioRecorder.set(.single(nil))
|
||||
} else if let videoRecorderValue = self.videoRecorderValue {
|
||||
if videoRecorderValue.stopVideo() {
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
$0.updatedInputTextPanelState { panelState in
|
||||
return panelState.withUpdatedMediaRecordingState(.video(status: .editing, isLocked: false))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.videoRecorder.set(.single(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func lockMediaRecorder() {
|
||||
if self.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil {
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedInputTextPanelState { panelState in
|
||||
return panelState.withUpdatedMediaRecordingState(panelState.mediaRecordingState?.withLocked(true))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.videoRecorderValue?.lockVideo()
|
||||
}
|
||||
|
||||
private func navigateToMessage(from fromId: MessageId?, to toId: MessageId, rememberInStack: Bool = true) {
|
||||
|
||||
@@ -139,7 +139,9 @@ class ChatControllerNode: ASDisplayNode {
|
||||
|
||||
self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
|
||||
self.textInputPanelNode = ChatTextInputPanelNode()
|
||||
self.textInputPanelNode = ChatTextInputPanelNode(theme: chatPresentationInterfaceState.theme, presentController: { [weak self] controller in
|
||||
self?.interfaceInteraction?.presentController(controller)
|
||||
})
|
||||
self.textInputPanelNode?.updateHeight = { [weak self] in
|
||||
if let strongSelf = self, let _ = strongSelf.inputPanelNode as? ChatTextInputPanelNode, !strongSelf.ignoreUpdateHeight {
|
||||
strongSelf.requestLayout(.animated(duration: 0.1, curve: .easeInOut))
|
||||
@@ -724,4 +726,8 @@ class ChatControllerNode: ASDisplayNode {
|
||||
let _ = inputNode.updateLayout(width: self.bounds.size.width, transition: .immediate, interfaceState: self.chatPresentationInterfaceState)
|
||||
}
|
||||
}
|
||||
|
||||
func currentInputPanelFrame() -> CGRect? {
|
||||
return self.inputPanelNode?.frame
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,11 @@ private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerI
|
||||
if let scrollToItem = transition.scrollToItem {
|
||||
let mappedPosition: GridNodeScrollToItemPosition
|
||||
switch scrollToItem.position {
|
||||
case .Top:
|
||||
case .top:
|
||||
mappedPosition = .top
|
||||
case .Center:
|
||||
case .center:
|
||||
mappedPosition = .center
|
||||
case .Bottom:
|
||||
case .bottom:
|
||||
mappedPosition = .bottom
|
||||
}
|
||||
let scrollTransition: ContainedViewLayoutTransition
|
||||
@@ -236,7 +236,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode {
|
||||
let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, theme: themeAndStrings.0, strings: themeAndStrings.1))
|
||||
let previous = previousView.swap(processedView)
|
||||
|
||||
return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous, theme: themeAndStrings.0, strings: themeAndStrings.1) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue)
|
||||
return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil, readStateData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous, theme: themeAndStrings.0, strings: themeAndStrings.1) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,15 +303,15 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode {
|
||||
}
|
||||
|
||||
public func scrollToStartOfHistory() {
|
||||
self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .Bottom, animated: true))
|
||||
self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .bottom(0.0), animated: true))
|
||||
}
|
||||
|
||||
public func scrollToEndOfHistory() {
|
||||
self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .Top, animated: true))
|
||||
self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .top(0.0), animated: true))
|
||||
}
|
||||
|
||||
public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex) {
|
||||
self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true))
|
||||
self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .center(.bottom), animated: true))
|
||||
}
|
||||
|
||||
public func messageInCurrentHistoryView(_ id: MessageId) -> Message? {
|
||||
|
||||
@@ -11,8 +11,9 @@ public enum ChatHistoryListMode {
|
||||
}
|
||||
|
||||
enum ChatHistoryViewScrollPosition {
|
||||
case Unread(index: MessageIndex)
|
||||
case Index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool)
|
||||
case unread(index: MessageIndex)
|
||||
case positionRestoration(index: MessageIndex, relativeOffset: CGFloat)
|
||||
case index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool)
|
||||
}
|
||||
|
||||
enum ChatHistoryViewUpdateType {
|
||||
@@ -20,10 +21,16 @@ enum ChatHistoryViewUpdateType {
|
||||
case Generic(type: ViewUpdateType)
|
||||
}
|
||||
|
||||
public struct ChatHistoryCombinedInitialReadStateData {
|
||||
public let unreadCount: Int32
|
||||
public let totalUnreadCount: Int32
|
||||
}
|
||||
|
||||
public struct ChatHistoryCombinedInitialData {
|
||||
let initialData: InitialMessageHistoryData?
|
||||
let buttonKeyboardMessage: Message?
|
||||
let cachedData: CachedPeerData?
|
||||
let readStateData: ChatHistoryCombinedInitialReadStateData?
|
||||
}
|
||||
|
||||
enum ChatHistoryViewUpdate {
|
||||
@@ -68,6 +75,7 @@ struct ChatHistoryViewTransition {
|
||||
let initialData: InitialMessageHistoryData?
|
||||
let keyboardButtonsMessage: Message?
|
||||
let cachedData: CachedPeerData?
|
||||
let readStateData: ChatHistoryCombinedInitialReadStateData?
|
||||
let scrolledToIndex: MessageIndex?
|
||||
}
|
||||
|
||||
@@ -82,6 +90,7 @@ struct ChatHistoryListViewTransition {
|
||||
let initialData: InitialMessageHistoryData?
|
||||
let keyboardButtonsMessage: Message?
|
||||
let cachedData: CachedPeerData?
|
||||
let readStateData: ChatHistoryCombinedInitialReadStateData?
|
||||
let scrolledToIndex: MessageIndex?
|
||||
}
|
||||
|
||||
@@ -163,7 +172,7 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt
|
||||
}
|
||||
|
||||
private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition {
|
||||
return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, scrolledToIndex: transition.scrolledToIndex)
|
||||
return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex)
|
||||
}
|
||||
|
||||
private final class ChatHistoryTransactionOpaqueState {
|
||||
@@ -282,6 +291,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
|
||||
var additionalData: [AdditionalMessageHistoryViewData] = []
|
||||
additionalData.append(.cachedPeerData(peerId))
|
||||
additionalData.append(.totalUnreadCount)
|
||||
|
||||
let historyViewUpdate = self.chatHistoryLocation
|
||||
|> distinctUntilChanged
|
||||
@@ -302,7 +312,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
let initialData: ChatHistoryCombinedInitialData?
|
||||
switch update {
|
||||
case let .Loading(data):
|
||||
let combinedInitialData = ChatHistoryCombinedInitialData(initialData: data, buttonKeyboardMessage: nil, cachedData: nil)
|
||||
let combinedInitialData = ChatHistoryCombinedInitialData(initialData: data, buttonKeyboardMessage: nil, cachedData: nil, readStateData: nil)
|
||||
initialData = combinedInitialData
|
||||
Queue.mainQueue().async { [weak self] in
|
||||
if let strongSelf = self {
|
||||
@@ -345,7 +355,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles, includeChatInfoEntry: true, theme: themeAndStrings.0, strings: themeAndStrings.1))
|
||||
let previous = previousView.swap(processedView)
|
||||
|
||||
return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue)
|
||||
return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, readStateData: initialData?.readStateData) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,19 +416,25 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
var messageIdsWithUnseenPersonalMention: [MessageId] = []
|
||||
for i in (indexRange.0 ... indexRange.1) {
|
||||
if case let .MessageEntry(message, _, _, _, _) = historyView.filteredEntries[i] {
|
||||
var hasUnconsumedMention = false
|
||||
var hasUnsonsumedContent = false
|
||||
if message.tags.contains(.unseenPersonalMessage) {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.pending {
|
||||
messageIdsWithUnseenPersonalMention.append(message.id)
|
||||
hasUnconsumedMention = true
|
||||
}
|
||||
}
|
||||
}
|
||||
inner: for attribute in message.attributes {
|
||||
for attribute in message.attributes {
|
||||
if attribute is ViewCountMessageAttribute {
|
||||
messageIdsWithViewCount.append(message.id)
|
||||
break inner
|
||||
} else if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed {
|
||||
hasUnsonsumedContent = true
|
||||
}
|
||||
}
|
||||
if hasUnconsumedMention && !hasUnsonsumedContent {
|
||||
messageIdsWithUnseenPersonalMention.append(message.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,15 +520,15 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
}
|
||||
|
||||
public func scrollToStartOfHistory() {
|
||||
self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .Bottom, animated: true))
|
||||
self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .bottom(0.0), animated: true))
|
||||
}
|
||||
|
||||
public func scrollToEndOfHistory() {
|
||||
self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .Top, animated: true))
|
||||
self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .top(0.0), animated: true))
|
||||
}
|
||||
|
||||
public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex) {
|
||||
self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true))
|
||||
self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .center(.bottom), animated: true))
|
||||
}
|
||||
|
||||
public func anchorMessageInCurrentHistoryView() -> Message? {
|
||||
@@ -558,7 +574,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
|
||||
if !strongSelf.didSetInitialData {
|
||||
strongSelf.didSetInitialData = true
|
||||
strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData)))
|
||||
strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, readStateData: transition.readStateData)))
|
||||
}
|
||||
|
||||
strongSelf.enqueuedHistoryViewTransition = (transition, {
|
||||
@@ -573,10 +589,6 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.dequeueHistoryViewTransition()
|
||||
} else {
|
||||
/*if !strongSelf.didSetInitialData {
|
||||
strongSelf.didSetInitialData = true
|
||||
strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData)))
|
||||
}*/
|
||||
strongSelf._cachedPeerData.set(.single(transition.cachedData))
|
||||
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty)
|
||||
if strongSelf.currentHistoryState != historyState {
|
||||
@@ -612,7 +624,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
}
|
||||
if !strongSelf.didSetInitialData {
|
||||
strongSelf.didSetInitialData = true
|
||||
strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData)))
|
||||
strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, readStateData: transition.readStateData)))
|
||||
}
|
||||
strongSelf._cachedPeerData.set(.single(transition.cachedData))
|
||||
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty)
|
||||
@@ -675,4 +687,37 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func immediateScrollState() -> ChatInterfaceHistoryScrollState? {
|
||||
var currentMessage: Message?
|
||||
if let historyView = self.historyView {
|
||||
if let visibleRange = self.displayedItemRange.visibleRange {
|
||||
var index = 0
|
||||
loop: for entry in historyView.filteredEntries.reversed() {
|
||||
if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex {
|
||||
if case let .MessageEntry(message, _, _, _, _) = entry {
|
||||
if index != 0 || historyView.originalView.laterId != nil {
|
||||
currentMessage = message
|
||||
}
|
||||
break loop
|
||||
}
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let message = currentMessage {
|
||||
var relativeOffset: CGFloat = 0.0
|
||||
self.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == message.id {
|
||||
if let offsetValue = self.itemNodeRelativeOffset(itemNode) {
|
||||
relativeOffset = offsetValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return ChatInterfaceHistoryScrollState(messageIndex: MessageIndex(message), relativeOffset: Double(relativeOffset))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,34 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun
|
||||
if let tagMask = tagMask {
|
||||
signal = account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: MessageIndex.upperBound(peerId: peerId), count: count, anchorIndex: MessageIndex.upperBound(peerId: peerId), fixedCombinedReadState: nil, tagMask: tagMask, orderStatistics: orderStatistics)
|
||||
} else {
|
||||
signal = account.viewTracker.aroundUnreadMessageHistoryViewForPeerId(peerId, count: count, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData)
|
||||
signal = account.viewTracker.aroundMessageOfInterestHistoryViewForPeerId(peerId, count: count, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData)
|
||||
}
|
||||
return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in
|
||||
var cachedData: CachedPeerData?
|
||||
var readStateData: ChatHistoryCombinedInitialReadStateData?
|
||||
for data in view.additionalData {
|
||||
if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId {
|
||||
cachedData = value
|
||||
break
|
||||
switch data {
|
||||
case let .cachedPeerData(peerIdValue, value):
|
||||
if peerIdValue == peerId {
|
||||
cachedData = value
|
||||
}
|
||||
case let .totalUnreadCount(totalUnreadCount):
|
||||
if let readState = view.combinedReadState {
|
||||
readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if preloaded {
|
||||
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData))
|
||||
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData))
|
||||
} else {
|
||||
var scrollPosition: ChatHistoryViewScrollPosition?
|
||||
|
||||
if let maxReadIndex = view.maxReadIndex, tagMask == nil {
|
||||
let aroundIndex = maxReadIndex
|
||||
scrollPosition = .Unread(index: maxReadIndex)
|
||||
scrollPosition = .unread(index: maxReadIndex)
|
||||
|
||||
var targetIndex = 0
|
||||
for i in 0 ..< view.entries.count {
|
||||
@@ -64,6 +74,8 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let historyScrollState = (initialData?.chatInterfaceState as? ChatInterfaceState)?.historyScrollState {
|
||||
scrollPosition = .positionRestoration(index: historyScrollState.messageIndex, relativeOffset: CGFloat(historyScrollState.relativeOffset))
|
||||
} else {
|
||||
var messageCount = 0
|
||||
for entry in view.entries.reversed() {
|
||||
@@ -80,7 +92,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun
|
||||
}
|
||||
|
||||
preloaded = true
|
||||
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData))
|
||||
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData))
|
||||
}
|
||||
}
|
||||
case let .InitialSearch(searchLocation, count):
|
||||
@@ -97,14 +109,24 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun
|
||||
|
||||
return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in
|
||||
var cachedData: CachedPeerData?
|
||||
var readStateData: ChatHistoryCombinedInitialReadStateData?
|
||||
for data in view.additionalData {
|
||||
if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId {
|
||||
cachedData = value
|
||||
break
|
||||
switch data {
|
||||
case let .cachedPeerData(peerIdValue, value):
|
||||
if peerIdValue == peerId {
|
||||
cachedData = value
|
||||
}
|
||||
case let .totalUnreadCount(totalUnreadCount):
|
||||
if let readState = view.combinedReadState {
|
||||
readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if preloaded {
|
||||
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData))
|
||||
return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData))
|
||||
} else {
|
||||
let anchorIndex = view.anchorIndex
|
||||
|
||||
@@ -127,17 +149,26 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun
|
||||
}
|
||||
|
||||
preloaded = true
|
||||
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Index(index: anchorIndex, position: .Center(.Bottom), directionHint: .Down, animated: false), initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData))
|
||||
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .index(index: anchorIndex, position: .center(.bottom), directionHint: .Down, animated: false), initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData))
|
||||
}
|
||||
}
|
||||
case let .Navigation(index, anchorIndex):
|
||||
var first = true
|
||||
return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in
|
||||
var cachedData: CachedPeerData?
|
||||
var readStateData: ChatHistoryCombinedInitialReadStateData?
|
||||
for data in view.additionalData {
|
||||
if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId {
|
||||
cachedData = value
|
||||
break
|
||||
switch data {
|
||||
case let .cachedPeerData(peerIdValue, value):
|
||||
if peerIdValue == peerId {
|
||||
cachedData = value
|
||||
}
|
||||
case let .totalUnreadCount(totalUnreadCount):
|
||||
if let readState = view.combinedReadState {
|
||||
readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,18 +179,27 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun
|
||||
} else {
|
||||
genericType = updateType
|
||||
}
|
||||
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData))
|
||||
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData))
|
||||
}
|
||||
case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated):
|
||||
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
|
||||
let chatScrollPosition = ChatHistoryViewScrollPosition.Index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated)
|
||||
let chatScrollPosition = ChatHistoryViewScrollPosition.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, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in
|
||||
var cachedData: CachedPeerData?
|
||||
var readStateData: ChatHistoryCombinedInitialReadStateData?
|
||||
for data in view.additionalData {
|
||||
if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId {
|
||||
cachedData = value
|
||||
break
|
||||
switch data {
|
||||
case let .cachedPeerData(peerIdValue, value):
|
||||
if peerIdValue == peerId {
|
||||
cachedData = value
|
||||
}
|
||||
case let .totalUnreadCount(totalUnreadCount):
|
||||
if let readState = view.combinedReadState {
|
||||
readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +211,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun
|
||||
} else {
|
||||
genericType = updateType
|
||||
}
|
||||
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData))
|
||||
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,10 +167,10 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
|
||||
}
|
||||
switch chatPresentationInterfaceState.inputMode {
|
||||
case .media, .inputButtons:
|
||||
return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState)
|
||||
return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState)
|
||||
case .none, .text:
|
||||
if let _ = chatPresentationInterfaceState.interfaceState.editMessage {
|
||||
return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState)
|
||||
return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState)
|
||||
} else {
|
||||
if chatPresentationInterfaceState.interfaceState.composeInputState.inputText.isEmpty {
|
||||
var accessoryItems: [ChatTextInputAccessoryItem] = []
|
||||
@@ -181,9 +181,9 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
|
||||
if let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup {
|
||||
accessoryItems.append(.inputButtons)
|
||||
}
|
||||
return ChatTextInputPanelState(accessoryItems: accessoryItems, contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState)
|
||||
return ChatTextInputPanelState(accessoryItems: accessoryItems, contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState)
|
||||
} else {
|
||||
return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState)
|
||||
return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, mediaRecordingState: chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
struct ChatInterfaceSelectionState: Coding, Equatable {
|
||||
struct ChatInterfaceSelectionState: PostboxCoding, Equatable {
|
||||
let selectedIds: Set<MessageId>
|
||||
|
||||
static func ==(lhs: ChatInterfaceSelectionState, rhs: ChatInterfaceSelectionState) -> Bool {
|
||||
@@ -13,7 +13,7 @@ struct ChatInterfaceSelectionState: Coding, Equatable {
|
||||
self.selectedIds = selectedIds
|
||||
}
|
||||
|
||||
init(decoder: Decoder) {
|
||||
init(decoder: PostboxDecoder) {
|
||||
if let data = decoder.decodeBytesForKeyNoCopy("i") {
|
||||
self.selectedIds = Set(MessageId.decodeArrayFromBuffer(data))
|
||||
} else {
|
||||
@@ -21,14 +21,14 @@ struct ChatInterfaceSelectionState: Coding, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
func encode(_ encoder: Encoder) {
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
let buffer = WriteBuffer()
|
||||
MessageId.encodeArrayToBuffer(Array(selectedIds), buffer: buffer)
|
||||
encoder.encodeBytes(buffer, forKey: "i")
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatTextInputState: Coding, Equatable {
|
||||
public struct ChatTextInputState: PostboxCoding, Equatable {
|
||||
let inputText: String
|
||||
let selectionRange: Range<Int>
|
||||
|
||||
@@ -52,19 +52,19 @@ public struct ChatTextInputState: Coding, Equatable {
|
||||
self.selectionRange = length ..< length
|
||||
}
|
||||
|
||||
public init(decoder: Decoder) {
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.inputText = decoder.decodeStringForKey("t", orElse: "")
|
||||
self.selectionRange = Int(decoder.decodeInt32ForKey("s0", orElse: 0)) ..< Int(decoder.decodeInt32ForKey("s1", orElse: 0))
|
||||
}
|
||||
|
||||
public func encode(_ encoder: Encoder) {
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeString(self.inputText, forKey: "t")
|
||||
encoder.encodeInt32(Int32(self.selectionRange.lowerBound), forKey: "s0")
|
||||
encoder.encodeInt32(Int32(self.selectionRange.upperBound), forKey: "s1")
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatEditMessageState: Coding, Equatable {
|
||||
struct ChatEditMessageState: PostboxCoding, Equatable {
|
||||
let messageId: MessageId
|
||||
let inputState: ChatTextInputState
|
||||
|
||||
@@ -73,7 +73,7 @@ struct ChatEditMessageState: Coding, Equatable {
|
||||
self.inputState = inputState
|
||||
}
|
||||
|
||||
init(decoder: Decoder) {
|
||||
init(decoder: PostboxDecoder) {
|
||||
self.messageId = MessageId(peerId: PeerId(decoder.decodeInt64ForKey("mp", orElse: 0)), namespace: decoder.decodeInt32ForKey("mn", orElse: 0), id: decoder.decodeInt32ForKey("mi", orElse: 0))
|
||||
if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState {
|
||||
self.inputState = inputState
|
||||
@@ -82,7 +82,7 @@ struct ChatEditMessageState: Coding, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
func encode(_ encoder: Encoder) {
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt64(self.messageId.peerId.toInt64(), forKey: "mp")
|
||||
encoder.encodeInt32(self.messageId.namespace, forKey: "mn")
|
||||
encoder.encodeInt32(self.messageId.id, forKey: "mi")
|
||||
@@ -107,12 +107,12 @@ final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceState {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
init(decoder: Decoder) {
|
||||
init(decoder: PostboxDecoder) {
|
||||
self.timestamp = decoder.decodeInt32ForKey("d", orElse: 0)
|
||||
self.text = decoder.decodeStringForKey("t", orElse: "")
|
||||
}
|
||||
|
||||
func encode(_ encoder: Encoder) {
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.timestamp, forKey: "d")
|
||||
encoder.encodeString(self.text, forKey: "t")
|
||||
}
|
||||
@@ -126,7 +126,7 @@ final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceState {
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatInterfaceMessageActionsState: Coding, Equatable {
|
||||
struct ChatInterfaceMessageActionsState: PostboxCoding, Equatable {
|
||||
let closedButtonKeyboardMessageId: MessageId?
|
||||
let processedSetupReplyMessageId: MessageId?
|
||||
let closedPinnedMessageId: MessageId?
|
||||
@@ -147,7 +147,7 @@ struct ChatInterfaceMessageActionsState: Coding, Equatable {
|
||||
self.closedPinnedMessageId = closedPinnedMessageId
|
||||
}
|
||||
|
||||
init(decoder: Decoder) {
|
||||
init(decoder: PostboxDecoder) {
|
||||
if let closedMessageIdPeerId = decoder.decodeOptionalInt64ForKey("cb.p"), let closedMessageIdNamespace = decoder.decodeOptionalInt32ForKey("cb.n"), let closedMessageIdId = decoder.decodeOptionalInt32ForKey("cb.i") {
|
||||
self.closedButtonKeyboardMessageId = MessageId(peerId: PeerId(closedMessageIdPeerId), namespace: closedMessageIdNamespace, id: closedMessageIdId)
|
||||
} else {
|
||||
@@ -167,7 +167,7 @@ struct ChatInterfaceMessageActionsState: Coding, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
func encode(_ encoder: Encoder) {
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
if let closedButtonKeyboardMessageId = self.closedButtonKeyboardMessageId {
|
||||
encoder.encodeInt64(closedButtonKeyboardMessageId.peerId.toInt64(), forKey: "cb.p")
|
||||
encoder.encodeInt32(closedButtonKeyboardMessageId.namespace, forKey: "cb.n")
|
||||
@@ -216,6 +216,39 @@ struct ChatInterfaceMessageActionsState: Coding, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatInterfaceHistoryScrollState: PostboxCoding, Equatable {
|
||||
let messageIndex: MessageIndex
|
||||
let relativeOffset: Double
|
||||
|
||||
init(messageIndex: MessageIndex, relativeOffset: Double) {
|
||||
self.messageIndex = messageIndex
|
||||
self.relativeOffset = relativeOffset
|
||||
}
|
||||
|
||||
init(decoder: PostboxDecoder) {
|
||||
self.messageIndex = MessageIndex(id: MessageId(peerId: PeerId(decoder.decodeInt64ForKey("m.p", orElse: 0)), namespace: decoder.decodeInt32ForKey("m.n", orElse: 0), id: decoder.decodeInt32ForKey("m.i", orElse: 0)), timestamp: decoder.decodeInt32ForKey("m.t", orElse: 0))
|
||||
self.relativeOffset = decoder.decodeDoubleForKey("ro", orElse: 0.0)
|
||||
}
|
||||
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.messageIndex.timestamp, forKey: "m.t")
|
||||
encoder.encodeInt64(self.messageIndex.id.peerId.toInt64(), forKey: "m.p")
|
||||
encoder.encodeInt32(self.messageIndex.id.namespace, forKey: "m.n")
|
||||
encoder.encodeInt32(self.messageIndex.id.id, forKey: "m.i")
|
||||
encoder.encodeDouble(self.relativeOffset, forKey: "ro")
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatInterfaceHistoryScrollState, rhs: ChatInterfaceHistoryScrollState) -> Bool {
|
||||
if lhs.messageIndex != rhs.messageIndex {
|
||||
return false
|
||||
}
|
||||
if !lhs.relativeOffset.isEqual(to: rhs.relativeOffset) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable {
|
||||
let timestamp: Int32
|
||||
let composeInputState: ChatTextInputState
|
||||
@@ -225,6 +258,8 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable {
|
||||
let editMessage: ChatEditMessageState?
|
||||
let selectionState: ChatInterfaceSelectionState?
|
||||
let messageActionsState: ChatInterfaceMessageActionsState
|
||||
let historyScrollState: ChatInterfaceHistoryScrollState?
|
||||
let mediaRecordingMode: ChatTextInputMediaRecordingButtonMode
|
||||
|
||||
var chatListEmbeddedState: PeerChatListEmbeddedInterfaceState? {
|
||||
if !self.composeInputState.inputText.isEmpty && self.timestamp != 0 {
|
||||
@@ -242,6 +277,10 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
var historyScrollMessageIndex: MessageIndex? {
|
||||
return self.historyScrollState?.messageIndex
|
||||
}
|
||||
|
||||
func withUpdatedSynchronizeableInputState(_ state: SynchronizeableChatInputState?) -> SynchronizeableChatInterfaceState {
|
||||
return self.withUpdatedComposeInputState(ChatTextInputState(inputText: state?.text ?? "")).withUpdatedReplyMessageId(state?.replyToMessageId)
|
||||
}
|
||||
@@ -263,9 +302,11 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable {
|
||||
self.editMessage = nil
|
||||
self.selectionState = nil
|
||||
self.messageActionsState = ChatInterfaceMessageActionsState()
|
||||
self.historyScrollState = nil
|
||||
self.mediaRecordingMode = .audio
|
||||
}
|
||||
|
||||
init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreview: String?, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState) {
|
||||
init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreview: String?, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode) {
|
||||
self.timestamp = timestamp
|
||||
self.composeInputState = composeInputState
|
||||
self.composeDisableUrlPreview = composeDisableUrlPreview
|
||||
@@ -274,9 +315,11 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable {
|
||||
self.editMessage = editMessage
|
||||
self.selectionState = selectionState
|
||||
self.messageActionsState = messageActionsState
|
||||
self.historyScrollState = historyScrollState
|
||||
self.mediaRecordingMode = mediaRecordingMode
|
||||
}
|
||||
|
||||
init(decoder: Decoder) {
|
||||
init(decoder: PostboxDecoder) {
|
||||
self.timestamp = decoder.decodeInt32ForKey("ts", orElse: 0)
|
||||
if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState {
|
||||
self.composeInputState = inputState
|
||||
@@ -317,9 +360,13 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable {
|
||||
} else {
|
||||
self.messageActionsState = ChatInterfaceMessageActionsState()
|
||||
}
|
||||
|
||||
self.historyScrollState = decoder.decodeObjectForKey("hss", decoder: { ChatInterfaceHistoryScrollState(decoder: $0) }) as? ChatInterfaceHistoryScrollState
|
||||
|
||||
self.mediaRecordingMode = ChatTextInputMediaRecordingButtonMode(rawValue: decoder.decodeInt32ForKey("mrm", orElse: 0))!
|
||||
}
|
||||
|
||||
func encode(_ encoder: Encoder) {
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.timestamp, forKey: "ts")
|
||||
encoder.encodeObject(self.composeInputState, forKey: "is")
|
||||
if let composeDisableUrlPreview = self.composeDisableUrlPreview {
|
||||
@@ -358,6 +405,12 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable {
|
||||
} else {
|
||||
encoder.encodeObject(self.messageActionsState, forKey: "as")
|
||||
}
|
||||
if let historyScrollState = self.historyScrollState {
|
||||
encoder.encodeObject(historyScrollState, forKey: "hss")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "hss")
|
||||
}
|
||||
encoder.encodeInt32(self.mediaRecordingMode.rawValue, forKey: "mrm")
|
||||
}
|
||||
|
||||
func isEqual(to: PeerChatInterfaceState) -> Bool {
|
||||
@@ -382,17 +435,23 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable {
|
||||
if lhs.messageActionsState != rhs.messageActionsState {
|
||||
return false
|
||||
}
|
||||
if lhs.historyScrollState != rhs.historyScrollState {
|
||||
return false
|
||||
}
|
||||
if lhs.mediaRecordingMode != rhs.mediaRecordingMode {
|
||||
return false
|
||||
}
|
||||
return lhs.composeInputState == rhs.composeInputState && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState && lhs.editMessage == rhs.editMessage
|
||||
}
|
||||
|
||||
func withUpdatedComposeInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState {
|
||||
let updatedComposeInputState = inputState
|
||||
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState)
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode)
|
||||
}
|
||||
|
||||
func withUpdatedComposeDisableUrlPreview(_ disableUrlPreview: String?) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: disableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState)
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: disableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode)
|
||||
}
|
||||
|
||||
func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState {
|
||||
@@ -404,15 +463,15 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable {
|
||||
updatedComposeInputState = inputState
|
||||
}
|
||||
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState)
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode)
|
||||
}
|
||||
|
||||
func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState)
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode)
|
||||
}
|
||||
|
||||
func withUpdatedForwardMessageIds(_ forwardMessageIds: [MessageId]?) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState)
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode)
|
||||
}
|
||||
|
||||
func withUpdatedSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState {
|
||||
@@ -421,7 +480,7 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable {
|
||||
selectedIds.formUnion(selectionState.selectedIds)
|
||||
}
|
||||
selectedIds.insert(messageId)
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState)
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode)
|
||||
}
|
||||
|
||||
func withToggledSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState {
|
||||
@@ -434,22 +493,30 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable {
|
||||
} else {
|
||||
selectedIds.insert(messageId)
|
||||
}
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState)
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode)
|
||||
}
|
||||
|
||||
func withoutSelectionState() -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState)
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode)
|
||||
}
|
||||
|
||||
func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState)
|
||||
return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode)
|
||||
}
|
||||
|
||||
func withUpdatedEditMessage(_ editMessage: ChatEditMessageState?) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState)
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode)
|
||||
}
|
||||
|
||||
func withUpdatedMessageActionsState(_ f: (ChatInterfaceMessageActionsState) -> ChatInterfaceMessageActionsState) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState))
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode)
|
||||
}
|
||||
|
||||
func withUpdatedHistoryScrollState(_ historyScrollState: ChatInterfaceHistoryScrollState?) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode)
|
||||
}
|
||||
|
||||
func withUpdatedMediaRecordingMode(_ mediaRecordingMode: ChatTextInputMediaRecordingButtonMode) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,9 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
|
||||
textInputPanelNode.account = account
|
||||
return textInputPanelNode
|
||||
} else {
|
||||
let panel = ChatTextInputPanelNode()
|
||||
let panel = ChatTextInputPanelNode(theme: chatPresentationInterfaceState.theme, presentController: { [weak interfaceInteraction] controller in
|
||||
interfaceInteraction?.presentController(controller)
|
||||
})
|
||||
panel.interfaceInteraction = interfaceInteraction
|
||||
panel.account = account
|
||||
return panel
|
||||
|
||||
@@ -547,9 +547,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
if let combinedReadState = combinedReadState {
|
||||
let unreadCount = combinedReadState.count
|
||||
if unreadCount != 0 {
|
||||
if let unreadCount = combinedReadState?.count, unreadCount > 0 {
|
||||
if let message = message, message.tags.contains(.unseenPersonalMessage), unreadCount == 1 {
|
||||
} else {
|
||||
let badgeTextColor: UIColor
|
||||
if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings {
|
||||
if case .unmuted = notificationSettings.muteState {
|
||||
|
||||
@@ -382,10 +382,10 @@ final class ChatListNode: ListView {
|
||||
|
||||
func scrollToLatest() {
|
||||
if let view = self.chatListView?.originalView, view.laterIndex == nil {
|
||||
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
} else {
|
||||
let location: ChatListNodeLocation = .scroll(index: ChatListIndex.absoluteUpperBound, sourceIndex: ChatListIndex.absoluteLowerBound
|
||||
, scrollPosition: .Top, animated: true)
|
||||
, scrollPosition: .top(0.0), animated: true)
|
||||
self.currentLocation = location
|
||||
self.chatListLocation.set(location)
|
||||
}
|
||||
|
||||
@@ -510,7 +510,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
let firstVisibleIndex = currentView.collectionInfos.index(where: { id, _, _ in return id == firstVisibleCollectionId })
|
||||
if let targetIndex = targetIndex, let firstVisibleIndex = firstVisibleIndex {
|
||||
let toRight = targetIndex > firstVisibleIndex
|
||||
self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .Bottom : .Top, animated: true, curve: .Default, directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil)
|
||||
self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .bottom(0.0) : .top(0.0), animated: true, curve: .Default, directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
var hostedVideoNode: InstantVideoNode?
|
||||
var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
private var statusNode: RadialStatusNode?
|
||||
private var videoFrame: CGRect?
|
||||
|
||||
private var selectionNode: ChatMessageSelectionNode?
|
||||
|
||||
private var appliedItem: ChatMessageItem?
|
||||
@@ -93,6 +96,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
updatedMuteIconImage = PresentationResourcesChat.chatInstantMessageMuteIconImage(item.theme)
|
||||
}
|
||||
|
||||
let theme = item.theme
|
||||
let isSecretMedia = item.message.containsSecretMedia
|
||||
|
||||
let incoming = item.message.effectivelyIncoming
|
||||
let imageSize = displaySize
|
||||
|
||||
@@ -111,7 +117,14 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
|
||||
var updatedPlaybackStatus: Signal<FileMediaResourceStatus, NoError>?
|
||||
if let updatedFile = updatedFile, updatedMedia {
|
||||
updatedPlaybackStatus = fileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message)
|
||||
updatedPlaybackStatus = combineLatest(fileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message), item.account.pendingMessageManager.pendingMessageStatus(item.message.id))
|
||||
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in
|
||||
if let pendingStatus = pendingStatus {
|
||||
return .fetchStatus(.Fetching(progress: pendingStatus.progress))
|
||||
} else {
|
||||
return resourceStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let avatarInset: CGFloat
|
||||
@@ -207,6 +220,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: imageSize.height), insets: layoutInsets), { [weak self] animation in
|
||||
if let strongSelf = self {
|
||||
strongSelf.appliedItem = item
|
||||
strongSelf.videoFrame = videoFrame
|
||||
|
||||
if let updatedMuteIconImage = updatedMuteIconImage {
|
||||
strongSelf.muteIconNode.image = updatedMuteIconImage
|
||||
@@ -220,7 +234,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
|
||||
if let updatedPlaybackStatus = updatedPlaybackStatus {
|
||||
strongSelf.playbackStatusDisposable.set((updatedPlaybackStatus |> deliverOnMainQueue).start(next: { status in
|
||||
if let strongSelf = self {
|
||||
if let strongSelf = self, let videoFrame = strongSelf.videoFrame {
|
||||
let displayMute: Bool
|
||||
switch status {
|
||||
case let .fetchStatus(fetchStatus):
|
||||
@@ -244,6 +258,68 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
strongSelf.muteIconNode.layer.animateScale(from: 1.0, to: 0.4, duration: 0.15)
|
||||
}
|
||||
}
|
||||
|
||||
var progressRequired = false
|
||||
if case let .fetchStatus(fetchStatus) = status {
|
||||
if case .Local = fetchStatus {
|
||||
if let file = updatedFile, file.isVideo {
|
||||
progressRequired = true
|
||||
} else if isSecretMedia {
|
||||
progressRequired = true
|
||||
}
|
||||
} else {
|
||||
progressRequired = true
|
||||
}
|
||||
}
|
||||
|
||||
if progressRequired {
|
||||
if strongSelf.statusNode == nil {
|
||||
let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.bubble.mediaOverlayControlBackgroundColor)
|
||||
statusNode.frame = CGRect(origin: CGPoint(x: videoFrame.origin.x + floor((videoFrame.size.width - 50.0) / 2.0), y: videoFrame.origin.y + floor((videoFrame.size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0))
|
||||
strongSelf.statusNode = statusNode
|
||||
strongSelf.addSubnode(statusNode)
|
||||
} else if let _ = updatedTheme {
|
||||
|
||||
//strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil))
|
||||
}
|
||||
} else {
|
||||
if let statusNode = strongSelf.statusNode {
|
||||
statusNode.transitionToState(.none, completion: { [weak statusNode] in
|
||||
statusNode?.removeFromSupernode()
|
||||
})
|
||||
strongSelf.statusNode = nil
|
||||
}
|
||||
}
|
||||
|
||||
var state: RadialStatusNodeState
|
||||
let bubbleTheme = theme.chat.bubble
|
||||
switch status {
|
||||
case let .fetchStatus(fetchStatus):
|
||||
switch fetchStatus {
|
||||
case let .Fetching(progress):
|
||||
state = .progress(color: bubbleTheme.mediaOverlayControlForegroundColor, value: CGFloat(progress), cancelEnabled: true)
|
||||
case .Local:
|
||||
state = .none
|
||||
/*if isSecretMedia && secretProgressIcon != nil {
|
||||
state = .customIcon(secretProgressIcon!)
|
||||
} else */
|
||||
case .Remote:
|
||||
state = .download(bubbleTheme.mediaOverlayControlForegroundColor)
|
||||
}
|
||||
default:
|
||||
state = .none
|
||||
break
|
||||
}
|
||||
if let statusNode = strongSelf.statusNode {
|
||||
if state == .none {
|
||||
strongSelf.statusNode = nil
|
||||
}
|
||||
statusNode.transitionToState(state, completion: { [weak statusNode] in
|
||||
if state == .none {
|
||||
statusNode?.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Display
|
||||
|
||||
final class ChatPanelInterfaceInteractionStatuses {
|
||||
let editingMessage: Signal<Bool, NoError>
|
||||
@@ -45,8 +46,11 @@ final class ChatPanelInterfaceInteraction {
|
||||
let sendBotCommand: (Peer, String) -> Void
|
||||
let sendBotStart: (String?) -> Void
|
||||
let botSwitchChatWithPayload: (PeerId, String) -> Void
|
||||
let beginAudioRecording: () -> Void
|
||||
let finishAudioRecording: (Bool) -> Void
|
||||
let beginMediaRecording: (Bool) -> Void
|
||||
let finishMediaRecording: (Bool) -> Void
|
||||
let stopMediaRecording: () -> Void
|
||||
let lockMediaRecording: () -> Void
|
||||
let switchMediaRecordingMode: () -> Void
|
||||
let setupMessageAutoremoveTimeout: () -> Void
|
||||
let sendSticker: (TelegramMediaFile) -> Void
|
||||
let unblockPeer: () -> Void
|
||||
@@ -57,9 +61,10 @@ final class ChatPanelInterfaceInteraction {
|
||||
let deleteChat: () -> Void
|
||||
let beginCall: () -> Void
|
||||
let toggleMessageStickerStarred: (MessageId) -> Void
|
||||
let presentController: (ViewController) -> Void
|
||||
let statuses: ChatPanelInterfaceInteractionStatuses?
|
||||
|
||||
init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) {
|
||||
init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (Bool) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) {
|
||||
self.setupReplyMessage = setupReplyMessage
|
||||
self.setupEditMessage = setupEditMessage
|
||||
self.beginMessageSelection = beginMessageSelection
|
||||
@@ -80,8 +85,11 @@ final class ChatPanelInterfaceInteraction {
|
||||
self.sendBotCommand = sendBotCommand
|
||||
self.sendBotStart = sendBotStart
|
||||
self.botSwitchChatWithPayload = botSwitchChatWithPayload
|
||||
self.beginAudioRecording = beginAudioRecording
|
||||
self.finishAudioRecording = finishAudioRecording
|
||||
self.beginMediaRecording = beginMediaRecording
|
||||
self.finishMediaRecording = finishMediaRecording
|
||||
self.stopMediaRecording = stopMediaRecording
|
||||
self.lockMediaRecording = lockMediaRecording
|
||||
self.switchMediaRecordingMode = switchMediaRecordingMode
|
||||
self.setupMessageAutoremoveTimeout = setupMessageAutoremoveTimeout
|
||||
self.sendSticker = sendSticker
|
||||
self.unblockPeer = unblockPeer
|
||||
@@ -92,6 +100,7 @@ final class ChatPanelInterfaceInteraction {
|
||||
self.deleteChat = deleteChat
|
||||
self.beginCall = beginCall
|
||||
self.toggleMessageStickerStarred = toggleMessageStickerStarred
|
||||
self.presentController = presentController
|
||||
self.statuses = statuses
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
private let offsetThreshold: CGFloat = 10.0
|
||||
private let dismissOffsetThreshold: CGFloat = 70.0
|
||||
|
||||
final class ChatTextInputAudioRecordingButton: UIButton {
|
||||
var account: Account?
|
||||
var beginRecording: () -> Void = { }
|
||||
var endRecording: (Bool) -> Void = { _ in }
|
||||
var offsetRecordingControls: () -> Void = { }
|
||||
|
||||
private var recordingOverlay: ChatTextInputAudioRecordingOverlay?
|
||||
private var startTouchLocation: CGPoint?
|
||||
private(set) var controlsOffset: CGFloat = 0.0
|
||||
|
||||
private var micLevelDisposable: MetaDisposable?
|
||||
|
||||
var audioRecorder: ManagedAudioRecorder? {
|
||||
didSet {
|
||||
if self.audioRecorder !== oldValue {
|
||||
if self.micLevelDisposable == nil {
|
||||
micLevelDisposable = MetaDisposable()
|
||||
}
|
||||
if let audioRecorder = self.audioRecorder {
|
||||
self.micLevelDisposable?.set(audioRecorder.micLevel.start(next: { [weak self] level in
|
||||
Queue.mainQueue().async {
|
||||
self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level))
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
self.micLevelDisposable?.set(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.isExclusiveTouch = true
|
||||
self.adjustsImageWhenHighlighted = false
|
||||
self.adjustsImageWhenDisabled = false
|
||||
self.disablesInteractiveTransitionGestureRecognizer = true
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updateTheme(theme: PresentationTheme) {
|
||||
self.setImage(PresentationResourcesChat.chatInputPanelVoiceButtonImage(theme), for: [])
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let micLevelDisposable = self.micLevelDisposable {
|
||||
micLevelDisposable.dispose()
|
||||
}
|
||||
if let recordingOverlay = self.recordingOverlay {
|
||||
recordingOverlay.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
func cancelRecording() {
|
||||
self.isEnabled = false
|
||||
self.isEnabled = true
|
||||
}
|
||||
|
||||
override func beginTracking(_ touch: UITouch, with touchEvent: UIEvent?) -> Bool {
|
||||
if super.beginTracking(touch, with: touchEvent) {
|
||||
self.startTouchLocation = touch.location(in: self)
|
||||
|
||||
self.controlsOffset = 0.0
|
||||
self.beginRecording()
|
||||
let recordingOverlay: ChatTextInputAudioRecordingOverlay
|
||||
if let currentRecordingOverlay = self.recordingOverlay {
|
||||
recordingOverlay = currentRecordingOverlay
|
||||
} else {
|
||||
recordingOverlay = ChatTextInputAudioRecordingOverlay(anchorView: self)
|
||||
self.recordingOverlay = recordingOverlay
|
||||
}
|
||||
if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext, let topWindow = applicationContext.applicationBindings.getTopWindow() {
|
||||
recordingOverlay.present(in: topWindow)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func endTracking(_ touch: UITouch?, with touchEvent: UIEvent?) {
|
||||
super.endTracking(touch, with: touchEvent)
|
||||
|
||||
self.endRecording(self.controlsOffset < 40.0)
|
||||
self.dismissRecordingOverlay()
|
||||
}
|
||||
|
||||
override func cancelTracking(with event: UIEvent?) {
|
||||
super.cancelTracking(with: event)
|
||||
|
||||
self.endRecording(false)
|
||||
self.dismissRecordingOverlay()
|
||||
}
|
||||
|
||||
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
if super.continueTracking(touch, with: event) {
|
||||
if let startTouchLocation = self.startTouchLocation {
|
||||
let horiontalOffset = startTouchLocation.x - touch.location(in: self).x
|
||||
let controlsOffset = max(0.0, horiontalOffset - offsetThreshold)
|
||||
if !controlsOffset.isEqual(to: self.controlsOffset) {
|
||||
self.recordingOverlay?.dismissFactor = 1.0 - controlsOffset / dismissOffsetThreshold
|
||||
self.controlsOffset = controlsOffset
|
||||
self.offsetRecordingControls()
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissRecordingOverlay() {
|
||||
if let recordingOverlay = self.recordingOverlay {
|
||||
self.recordingOverlay = nil
|
||||
recordingOverlay.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,20 @@ import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
private let cancelFont = Font.regular(17.0)
|
||||
|
||||
final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
|
||||
private let cancel: () -> Void
|
||||
|
||||
private let arrowNode: ASImageNode
|
||||
private let labelNode: TextNode
|
||||
private let cancelButton: HighlightableButtonNode
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
private var isDisplayingCancel = false
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) {
|
||||
self.cancel = cancel
|
||||
|
||||
self.arrowNode = ASImageNode()
|
||||
self.arrowNode.isLayerBacked = true
|
||||
self.arrowNode.displayWithoutProcessing = true
|
||||
@@ -16,10 +25,15 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.isLayerBacked = true
|
||||
|
||||
self.cancelButton = HighlightableButtonNode()
|
||||
self.cancelButton.setTitle(strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: [])
|
||||
self.cancelButton.alpha = 0.0
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.arrowNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
self.addSubnode(self.cancelButton)
|
||||
|
||||
let makeLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.panelControlColor), nil, 1, .end, CGSize(width: 200.0, height: 100.0), .natural, nil, UIEdgeInsets())
|
||||
@@ -30,9 +44,52 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode {
|
||||
self.frame = CGRect(origin: CGPoint(), size: CGSize(width: arrowSize.width + 12.0 + labelLayout.size.width, height: height))
|
||||
self.arrowNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - arrowSize.height) / 2.0)), size: arrowSize)
|
||||
self.labelNode.frame = CGRect(origin: CGPoint(x: arrowSize.width + 6.0, y: floor((height - labelLayout.size.height) / 2.0) - UIScreenPixel), size: labelLayout.size)
|
||||
|
||||
let cancelSize = self.cancelButton.measure(CGSize(width: 200.0, height: 100.0))
|
||||
self.cancelButton.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - cancelSize.width) / 2.0), y: floor((height - cancelSize.height) / 2.0)), size: cancelSize)
|
||||
|
||||
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func updateTheme(theme: PresentationTheme) {
|
||||
self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)
|
||||
}
|
||||
|
||||
func updateIsDisplayingCancel(_ isDisplayingCancel: Bool, animated: Bool) {
|
||||
if self.isDisplayingCancel != isDisplayingCancel {
|
||||
self.isDisplayingCancel = isDisplayingCancel
|
||||
if isDisplayingCancel {
|
||||
self.arrowNode.alpha = 0.0
|
||||
self.labelNode.alpha = 0.0
|
||||
self.cancelButton.alpha = 1.0
|
||||
|
||||
if animated {
|
||||
self.arrowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
||||
self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
||||
self.cancelButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
} else {
|
||||
self.arrowNode.alpha = 1.0
|
||||
self.labelNode.alpha = 1.0
|
||||
self.cancelButton.alpha = 0.0
|
||||
|
||||
if animated {
|
||||
self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
self.cancelButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.cancelButton.alpha.isZero, self.cancelButton.frame.insetBy(dx: -5.0, dy: -5.0).contains(point) {
|
||||
return self.cancelButton.view
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
@objc func cancelPressed() {
|
||||
self.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
386
TelegramUI/ChatTextInputMediaRecordingButton.swift
Normal file
@@ -0,0 +1,386 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
import LegacyComponents
|
||||
|
||||
private let offsetThreshold: CGFloat = 10.0
|
||||
private let dismissOffsetThreshold: CGFloat = 70.0
|
||||
|
||||
enum ChatTextInputMediaRecordingButtonMode: Int32 {
|
||||
case audio = 0
|
||||
case video = 1
|
||||
}
|
||||
|
||||
private final class ChatTextInputMediaRecordingButtonPresenterContainer: UIView {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
for subview in self.subviews {
|
||||
if let result = subview.hitTest(point.offsetBy(dx: -subview.frame.minX, dy: -subview.frame.minY), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatTextInputMediaRecordingButtonPresenterController: ViewController {
|
||||
override func loadDisplayNode() {
|
||||
self.displayNode = ChatTextInputMediaRecordingButtonPresenterControllerNode()
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatTextInputMediaRecordingButtonPresenterControllerNode: ViewControllerTracingNode {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatTextInputMediaRecordingButtonPresenter : NSObject, TGModernConversationInputMicButtonPresentation {
|
||||
private let account: Account?
|
||||
private let presentController: (ViewController) -> Void
|
||||
private let container: ChatTextInputMediaRecordingButtonPresenterContainer
|
||||
private var presentationController: ViewController?
|
||||
|
||||
init(account: Account, presentController: @escaping (ViewController) -> Void) {
|
||||
self.account = account
|
||||
self.presentController = presentController
|
||||
self.container = ChatTextInputMediaRecordingButtonPresenterContainer()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.container.removeFromSuperview()
|
||||
if let presentationController = self.presentationController {
|
||||
presentationController.presentingViewController?.dismiss(animated: false, completion: {})
|
||||
self.presentationController = nil
|
||||
}
|
||||
}
|
||||
|
||||
func view() -> UIView! {
|
||||
return self.container
|
||||
}
|
||||
|
||||
func setUserInteractionEnabled(_ enabled: Bool) {
|
||||
self.container.isUserInteractionEnabled = enabled
|
||||
}
|
||||
|
||||
func present() {
|
||||
if let keyboardWindow = LegacyComponentsGlobals.provider().applicationKeyboardWindow(), !keyboardWindow.isHidden {
|
||||
keyboardWindow.addSubview(self.container)
|
||||
} else {
|
||||
var presentNow = false
|
||||
if self.presentationController == nil {
|
||||
let presentationController = ChatTextInputMediaRecordingButtonPresenterController(navigationBarTheme: nil)
|
||||
presentationController.statusBar.statusBarStyle = .Ignore
|
||||
self.presentationController = presentationController
|
||||
presentNow = true
|
||||
}
|
||||
|
||||
self.presentationController?.displayNode.view.addSubview(self.container)
|
||||
if let presentationController = self.presentationController, presentNow {
|
||||
self.presentController(presentationController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
self.container.removeFromSuperview()
|
||||
if let presentationController = self.presentationController {
|
||||
presentationController.presentingViewController?.dismiss(animated: false, completion: {})
|
||||
self.presentationController = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButton, TGModernConversationInputMicButtonDelegate {
|
||||
private var theme: PresentationTheme
|
||||
|
||||
var mode: ChatTextInputMediaRecordingButtonMode = .audio
|
||||
var account: Account?
|
||||
let presentController: (ViewController) -> Void
|
||||
var beginRecording: () -> Void = { }
|
||||
var endRecording: (Bool) -> Void = { _ in }
|
||||
var stopRecording: () -> Void = { _ in }
|
||||
var offsetRecordingControls: () -> Void = { }
|
||||
var switchMode: () -> Void = { }
|
||||
var updateLocked: (Bool) -> Void = { _ in }
|
||||
|
||||
private var modeTimeoutTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private let innerIconView: UIImageView
|
||||
|
||||
private var recordingOverlay: ChatTextInputAudioRecordingOverlay?
|
||||
private var startTouchLocation: CGPoint?
|
||||
private(set) var controlsOffset: CGFloat = 0.0
|
||||
|
||||
private var micLevelDisposable: MetaDisposable?
|
||||
|
||||
var audioRecorder: ManagedAudioRecorder? {
|
||||
didSet {
|
||||
if self.audioRecorder !== oldValue {
|
||||
if self.micLevelDisposable == nil {
|
||||
micLevelDisposable = MetaDisposable()
|
||||
}
|
||||
if let audioRecorder = self.audioRecorder {
|
||||
self.micLevelDisposable?.set(audioRecorder.micLevel.start(next: { [weak self] level in
|
||||
Queue.mainQueue().async {
|
||||
//self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level))
|
||||
self?.addMicLevel(CGFloat(level))
|
||||
}
|
||||
}))
|
||||
} else if self.videoRecordingStatus == nil {
|
||||
self.micLevelDisposable?.set(nil)
|
||||
}
|
||||
|
||||
self.hasRecorder = self.audioRecorder != nil || self.videoRecordingStatus != nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var videoRecordingStatus: InstantVideoControllerRecordingStatus? {
|
||||
didSet {
|
||||
if self.videoRecordingStatus !== oldValue {
|
||||
if self.micLevelDisposable == nil {
|
||||
micLevelDisposable = MetaDisposable()
|
||||
}
|
||||
|
||||
if let videoRecordingStatus = self.videoRecordingStatus {
|
||||
self.micLevelDisposable?.set(videoRecordingStatus.micLevel.start(next: { [weak self] level in
|
||||
Queue.mainQueue().async {
|
||||
//self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level))
|
||||
self?.addMicLevel(CGFloat(level))
|
||||
}
|
||||
}))
|
||||
} else if self.audioRecorder == nil {
|
||||
self.micLevelDisposable?.set(nil)
|
||||
}
|
||||
|
||||
self.hasRecorder = self.audioRecorder != nil || self.videoRecordingStatus != nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var hasRecorder: Bool = false {
|
||||
didSet {
|
||||
if self.hasRecorder != oldValue {
|
||||
if self.hasRecorder {
|
||||
self.animateIn()
|
||||
} else {
|
||||
self.animateOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(theme: PresentationTheme, presentController: @escaping (ViewController) -> Void) {
|
||||
self.theme = theme
|
||||
self.innerIconView = UIImageView()
|
||||
self.presentController = presentController
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.insertSubview(self.innerIconView, at: 0)
|
||||
|
||||
self.isExclusiveTouch = true
|
||||
self.adjustsImageWhenHighlighted = false
|
||||
self.adjustsImageWhenDisabled = false
|
||||
self.disablesInteractiveTransitionGestureRecognizer = true
|
||||
|
||||
self.updateMode(mode: self.mode, animated: false, force: true)
|
||||
|
||||
self.delegate = self
|
||||
|
||||
self.centerOffset = CGPoint(x: 0.0, y: -1.0 + UIScreenPixel)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updateMode(mode: ChatTextInputMediaRecordingButtonMode, animated: Bool) {
|
||||
self.updateMode(mode: mode, animated: animated, force: false)
|
||||
}
|
||||
|
||||
private func updateMode(mode: ChatTextInputMediaRecordingButtonMode, animated: Bool, force: Bool) {
|
||||
if mode != self.mode || force {
|
||||
self.mode = mode
|
||||
|
||||
if animated {
|
||||
let previousView = UIImageView(image: self.innerIconView.image)
|
||||
previousView.frame = self.innerIconView.frame
|
||||
self.addSubview(previousView)
|
||||
previousView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
previousView.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false, completion: { [weak previousView] _ in
|
||||
previousView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
switch self.mode {
|
||||
case .audio:
|
||||
self.icon = PresentationResourcesChat.chatInputPanelVoiceActiveButtonImage(self.theme)
|
||||
self.innerIconView.image = PresentationResourcesChat.chatInputPanelVoiceButtonImage(self.theme)
|
||||
case .video:
|
||||
self.icon = PresentationResourcesChat.chatInputPanelVideoActiveButtonImage(self.theme)
|
||||
self.innerIconView.image = PresentationResourcesChat.chatInputPanelVideoButtonImage(self.theme)
|
||||
}
|
||||
if let image = self.innerIconView.image {
|
||||
let size = self.bounds.size
|
||||
let iconSize = image.size
|
||||
self.innerIconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
}
|
||||
|
||||
if animated {
|
||||
self.innerIconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, removeOnCompletion: false)
|
||||
self.innerIconView.layer.animateSpring(from: 0.4 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateTheme(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
switch self.mode {
|
||||
case .audio:
|
||||
self.icon = PresentationResourcesChat.chatInputPanelVoiceActiveButtonImage(self.theme)
|
||||
self.innerIconView.image = PresentationResourcesChat.chatInputPanelVoiceButtonImage(self.theme)
|
||||
case .video:
|
||||
self.icon = PresentationResourcesChat.chatInputPanelVideoActiveButtonImage(self.theme)
|
||||
self.innerIconView.image = PresentationResourcesChat.chatInputPanelVideoButtonImage(self.theme)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let micLevelDisposable = self.micLevelDisposable {
|
||||
micLevelDisposable.dispose()
|
||||
}
|
||||
if let recordingOverlay = self.recordingOverlay {
|
||||
recordingOverlay.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
func cancelRecording() {
|
||||
self.isEnabled = false
|
||||
self.isEnabled = true
|
||||
}
|
||||
|
||||
/*override func beginTracking(_ touch: UITouch, with touchEvent: UIEvent?) -> Bool {
|
||||
if super.beginTracking(touch, with: touchEvent) {
|
||||
self.startTouchLocation = touch.location(in: self)
|
||||
|
||||
self.controlsOffset = 0.0
|
||||
self.beginRecording()
|
||||
let recordingOverlay: ChatTextInputAudioRecordingOverlay
|
||||
if let currentRecordingOverlay = self.recordingOverlay {
|
||||
recordingOverlay = currentRecordingOverlay
|
||||
} else {
|
||||
recordingOverlay = ChatTextInputAudioRecordingOverlay(anchorView: self)
|
||||
self.recordingOverlay = recordingOverlay
|
||||
}
|
||||
if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext, let topWindow = applicationContext.applicationBindings.getTopWindow() {
|
||||
recordingOverlay.present(in: topWindow)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func endTracking(_ touch: UITouch?, with touchEvent: UIEvent?) {
|
||||
super.endTracking(touch, with: touchEvent)
|
||||
|
||||
self.endRecording(self.controlsOffset < 40.0)
|
||||
self.dismissRecordingOverlay()
|
||||
}
|
||||
|
||||
override func cancelTracking(with event: UIEvent?) {
|
||||
super.cancelTracking(with: event)
|
||||
|
||||
self.endRecording(false)
|
||||
self.dismissRecordingOverlay()
|
||||
}
|
||||
|
||||
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
if super.continueTracking(touch, with: event) {
|
||||
if let startTouchLocation = self.startTouchLocation {
|
||||
let horiontalOffset = startTouchLocation.x - touch.location(in: self).x
|
||||
let controlsOffset = max(0.0, horiontalOffset - offsetThreshold)
|
||||
if !controlsOffset.isEqual(to: self.controlsOffset) {
|
||||
self.recordingOverlay?.dismissFactor = 1.0 - controlsOffset / dismissOffsetThreshold
|
||||
self.controlsOffset = controlsOffset
|
||||
self.offsetRecordingControls()
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissRecordingOverlay() {
|
||||
if let recordingOverlay = self.recordingOverlay {
|
||||
self.recordingOverlay = nil
|
||||
recordingOverlay.dismiss()
|
||||
}
|
||||
}*/
|
||||
|
||||
func micButtonInteractionBegan() {
|
||||
self.modeTimeoutTimer?.invalidate()
|
||||
let modeTimeoutTimer = SwiftSignalKit.Timer(timeout: 0.1, repeat: false, completion: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.modeTimeoutTimer = nil
|
||||
strongSelf.beginRecording()
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
self.modeTimeoutTimer = modeTimeoutTimer
|
||||
modeTimeoutTimer.start()
|
||||
}
|
||||
|
||||
func micButtonInteractionCancelled(_ velocity: CGPoint) {
|
||||
self.modeTimeoutTimer?.invalidate()
|
||||
self.endRecording(false)
|
||||
}
|
||||
|
||||
func micButtonInteractionCompleted(_ velocity: CGPoint) {
|
||||
if let modeTimeoutTimer = self.modeTimeoutTimer {
|
||||
modeTimeoutTimer.invalidate()
|
||||
self.modeTimeoutTimer = nil
|
||||
self.switchMode()
|
||||
}
|
||||
self.endRecording(true)
|
||||
}
|
||||
|
||||
func micButtonInteractionUpdate(_ offset: CGPoint) {
|
||||
self.controlsOffset = offset.x
|
||||
self.offsetRecordingControls()
|
||||
}
|
||||
|
||||
func micButtonInteractionLocked() {
|
||||
self.updateLocked(true)
|
||||
}
|
||||
|
||||
func micButtonInteractionRequestedLockedAction() {
|
||||
}
|
||||
|
||||
func micButtonInteractionStopped() {
|
||||
self.stopRecording()
|
||||
}
|
||||
|
||||
func micButtonShouldLock() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func micButtonPresenter() -> TGModernConversationInputMicButtonPresentation! {
|
||||
return ChatTextInputMediaRecordingButtonPresenter(account: self.account!, presentController: self.presentController)
|
||||
}
|
||||
|
||||
private var previousSize = CGSize()
|
||||
func layoutItems() {
|
||||
let size = self.bounds.size
|
||||
if size != self.previousSize {
|
||||
self.previousSize = size
|
||||
let iconSize = self.innerIconView.bounds.size
|
||||
self.innerIconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,33 +53,83 @@ enum ChatTextInputAccessoryItem: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatTextInputPanelAudioRecordingState: Equatable {
|
||||
let recorder: ManagedAudioRecorder
|
||||
enum ChatVideoRecordingStatus: Equatable {
|
||||
case recording(InstantVideoControllerRecordingStatus)
|
||||
case editing
|
||||
|
||||
init(recorder: ManagedAudioRecorder) {
|
||||
self.recorder = recorder
|
||||
static func ==(lhs: ChatVideoRecordingStatus, rhs: ChatVideoRecordingStatus) -> Bool {
|
||||
switch lhs {
|
||||
case let .recording(lhsStatus):
|
||||
if case let .recording(rhsStatus) = rhs, lhsStatus === rhsStatus {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .editing:
|
||||
if case .editing = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatTextInputPanelMediaRecordingState: Equatable {
|
||||
case audio(recorder: ManagedAudioRecorder, isLocked: Bool)
|
||||
case video(status: ChatVideoRecordingStatus, isLocked: Bool)
|
||||
|
||||
var isLocked: Bool {
|
||||
switch self {
|
||||
case let .audio(_, isLocked):
|
||||
return isLocked
|
||||
case let .video(_, isLocked):
|
||||
return isLocked
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatTextInputPanelAudioRecordingState, rhs: ChatTextInputPanelAudioRecordingState) -> Bool {
|
||||
return lhs.recorder === rhs.recorder
|
||||
func withLocked(_ isLocked: Bool) -> ChatTextInputPanelMediaRecordingState {
|
||||
switch self {
|
||||
case let .audio(recorder, _):
|
||||
return .audio(recorder: recorder, isLocked: isLocked)
|
||||
case let .video(status, _):
|
||||
return .video(status: status, isLocked: isLocked)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatTextInputPanelMediaRecordingState, rhs: ChatTextInputPanelMediaRecordingState) -> Bool {
|
||||
switch lhs {
|
||||
case let .audio(lhsRecorder, lhsIsLocked):
|
||||
if case let .audio(rhsRecorder, rhsIsLocked) = rhs, lhsRecorder === rhsRecorder, lhsIsLocked == rhsIsLocked {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .video(status, isLocked):
|
||||
if case .video(status, isLocked) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatTextInputPanelState: Equatable {
|
||||
let accessoryItems: [ChatTextInputAccessoryItem]
|
||||
let contextPlaceholder: NSAttributedString?
|
||||
let audioRecordingState: ChatTextInputPanelAudioRecordingState?
|
||||
let mediaRecordingState: ChatTextInputPanelMediaRecordingState?
|
||||
|
||||
init(accessoryItems: [ChatTextInputAccessoryItem], contextPlaceholder: NSAttributedString?, audioRecordingState: ChatTextInputPanelAudioRecordingState?) {
|
||||
init(accessoryItems: [ChatTextInputAccessoryItem], contextPlaceholder: NSAttributedString?, mediaRecordingState: ChatTextInputPanelMediaRecordingState?) {
|
||||
self.accessoryItems = accessoryItems
|
||||
self.contextPlaceholder = contextPlaceholder
|
||||
self.audioRecordingState = audioRecordingState
|
||||
self.mediaRecordingState = mediaRecordingState
|
||||
}
|
||||
|
||||
init() {
|
||||
self.accessoryItems = []
|
||||
self.contextPlaceholder = nil
|
||||
self.audioRecordingState = nil
|
||||
self.mediaRecordingState = nil
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatTextInputPanelState, rhs: ChatTextInputPanelState) -> Bool {
|
||||
@@ -91,14 +141,14 @@ struct ChatTextInputPanelState: Equatable {
|
||||
} else if (lhs.contextPlaceholder != nil) != (rhs.contextPlaceholder != nil) {
|
||||
return false
|
||||
}
|
||||
if lhs.audioRecordingState != rhs.audioRecordingState {
|
||||
if lhs.mediaRecordingState != rhs.mediaRecordingState {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func withUpdatedAudioRecordingState(_ audioRecordingState: ChatTextInputPanelAudioRecordingState?) -> ChatTextInputPanelState {
|
||||
return ChatTextInputPanelState(accessoryItems: self.accessoryItems, contextPlaceholder: self.contextPlaceholder, audioRecordingState: audioRecordingState)
|
||||
func withUpdatedMediaRecordingState(_ mediaRecordingState: ChatTextInputPanelMediaRecordingState?) -> ChatTextInputPanelState {
|
||||
return ChatTextInputPanelState(accessoryItems: self.accessoryItems, contextPlaceholder: self.contextPlaceholder, mediaRecordingState: mediaRecordingState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +221,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
var textInputNode: ASEditableTextNode?
|
||||
|
||||
let textInputBackgroundView: UIImageView
|
||||
let micButton: ChatTextInputAudioRecordingButton
|
||||
let micButton: ChatTextInputMediaRecordingButton
|
||||
let sendButton: HighlightableButton
|
||||
let attachmentButton: HighlightableButton
|
||||
let searchLayoutClearButton: HighlightableButton
|
||||
@@ -265,7 +315,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let accessoryButtonSpacing: CGFloat = 0.0
|
||||
let accessoryButtonInset: CGFloat = 4.0 + UIScreenPixel
|
||||
|
||||
override init() {
|
||||
init(theme: PresentationTheme, presentController: @escaping (ViewController) -> Void) {
|
||||
self.textInputBackgroundView = UIImageView()
|
||||
self.textPlaceholderNode = TextNode()
|
||||
self.textPlaceholderNode.isLayerBacked = true
|
||||
@@ -273,7 +323,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
self.searchLayoutClearButton = HighlightableButton()
|
||||
self.searchLayoutProgressView = UIImageView(image: searchLayoutProgressImage)
|
||||
self.searchLayoutProgressView.isHidden = true
|
||||
self.micButton = ChatTextInputAudioRecordingButton()
|
||||
self.micButton = ChatTextInputMediaRecordingButton(theme: theme, presentController: presentController)
|
||||
self.sendButton = HighlightableButton()
|
||||
|
||||
super.init()
|
||||
@@ -282,13 +332,20 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
self.view.addSubview(self.attachmentButton)
|
||||
|
||||
self.micButton.beginRecording = { [weak self] in
|
||||
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
||||
interfaceInteraction.beginAudioRecording()
|
||||
if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let interfaceInteraction = strongSelf.interfaceInteraction {
|
||||
let isVideo: Bool
|
||||
switch presentationInterfaceState.interfaceState.mediaRecordingMode {
|
||||
case .audio:
|
||||
isVideo = false
|
||||
case .video:
|
||||
isVideo = true
|
||||
}
|
||||
interfaceInteraction.beginMediaRecording(isVideo)
|
||||
}
|
||||
}
|
||||
self.micButton.endRecording = { [weak self] sendAudio in
|
||||
self.micButton.endRecording = { [weak self] sendMedia in
|
||||
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
||||
interfaceInteraction.finishAudioRecording(sendAudio)
|
||||
interfaceInteraction.finishMediaRecording(sendMedia)
|
||||
}
|
||||
}
|
||||
self.micButton.offsetRecordingControls = { [weak self] in
|
||||
@@ -296,6 +353,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let _ = strongSelf.updateLayout(width: strongSelf.bounds.size.width, transition: .immediate, interfaceState: presentationInterfaceState)
|
||||
}
|
||||
}
|
||||
self.micButton.stopRecording = { [weak self] in
|
||||
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
||||
interfaceInteraction.stopMediaRecording()
|
||||
}
|
||||
}
|
||||
self.micButton.updateLocked = { [weak self] _ in
|
||||
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
||||
interfaceInteraction.lockMediaRecording()
|
||||
}
|
||||
}
|
||||
self.micButton.switchMode = { [weak self] in
|
||||
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
||||
interfaceInteraction.switchMediaRecordingMode()
|
||||
}
|
||||
}
|
||||
self.view.addSubview(self.micButton)
|
||||
|
||||
self.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), for: .touchUpInside)
|
||||
@@ -522,18 +594,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: width)
|
||||
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight)
|
||||
|
||||
self.micButton.updateMode(mode: interfaceState.interfaceState.mediaRecordingMode, animated: transition.isAnimated)
|
||||
|
||||
var hideMicButton = false
|
||||
var audioRecordingItemsVerticalOffset: CGFloat = 0.0
|
||||
if let audioRecordingState = interfaceState.inputTextPanelState.audioRecordingState {
|
||||
self.micButton.audioRecorder = audioRecordingState.recorder
|
||||
let audioRecordingInfoContainerNode: ASDisplayNode
|
||||
if let currentAudioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode {
|
||||
audioRecordingInfoContainerNode = currentAudioRecordingInfoContainerNode
|
||||
} else {
|
||||
audioRecordingInfoContainerNode = ASDisplayNode()
|
||||
self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode
|
||||
self.insertSubnode(audioRecordingInfoContainerNode, at: 0)
|
||||
}
|
||||
|
||||
if let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState {
|
||||
audioRecordingItemsVerticalOffset = panelHeight * 2.0
|
||||
transition.updateAlpha(layer: self.textInputBackgroundView.layer, alpha: 0.0)
|
||||
if let textInputNode = self.textInputNode {
|
||||
@@ -543,81 +608,107 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
transition.updateAlpha(layer: button.layer, alpha: 0.0)
|
||||
}
|
||||
|
||||
var animateCancelSlideIn = false
|
||||
let audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator
|
||||
if let currentAudioRecordingCancelIndicator = self.audioRecordingCancelIndicator {
|
||||
audioRecordingCancelIndicator = currentAudioRecordingCancelIndicator
|
||||
} else {
|
||||
animateCancelSlideIn = transition.isAnimated
|
||||
|
||||
audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings)
|
||||
self.audioRecordingCancelIndicator = audioRecordingCancelIndicator
|
||||
self.insertSubnode(audioRecordingCancelIndicator, at: 0)
|
||||
}
|
||||
|
||||
audioRecordingCancelIndicator.frame = CGRect(origin: CGPoint(x: floor((width - audioRecordingCancelIndicator.bounds.size.width) / 2.0) - self.micButton.controlsOffset, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)), size: audioRecordingCancelIndicator.bounds.size)
|
||||
|
||||
if animateCancelSlideIn {
|
||||
let position = audioRecordingCancelIndicator.layer.position
|
||||
audioRecordingCancelIndicator.layer.animatePosition(from: CGPoint(x: width + audioRecordingCancelIndicator.bounds.size.width, y: position.y), to: position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
var animateTimeSlideIn = false
|
||||
let audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode
|
||||
if let currentAudioRecordingTimeNode = self.audioRecordingTimeNode {
|
||||
audioRecordingTimeNode = currentAudioRecordingTimeNode
|
||||
} else {
|
||||
audioRecordingTimeNode = ChatTextInputAudioRecordingTimeNode(theme: interfaceState.theme)
|
||||
self.audioRecordingTimeNode = audioRecordingTimeNode
|
||||
audioRecordingInfoContainerNode.addSubnode(audioRecordingTimeNode)
|
||||
|
||||
if transition.isAnimated {
|
||||
animateTimeSlideIn = true
|
||||
}
|
||||
}
|
||||
|
||||
let audioRecordingTimeSize = audioRecordingTimeNode.measure(CGSize(width: 200.0, height: 100.0))
|
||||
|
||||
audioRecordingInfoContainerNode.frame = CGRect(origin: CGPoint(x: min(0.0, audioRecordingCancelIndicator.frame.minX - audioRecordingTimeSize.width - 8.0 - 28.0), y: 0.0), size: CGSize(width: width, height: panelHeight))
|
||||
|
||||
audioRecordingTimeNode.frame = CGRect(origin: CGPoint(x: 28.0, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0)), size: audioRecordingTimeSize)
|
||||
if animateTimeSlideIn {
|
||||
let position = audioRecordingTimeNode.layer.position
|
||||
audioRecordingTimeNode.layer.animatePosition(from: CGPoint(x: position.x - 28.0 - audioRecordingTimeSize.width, y: position.y), to: position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
audioRecordingTimeNode.audioRecorder = audioRecordingState.recorder
|
||||
|
||||
var animateDotSlideIn = false
|
||||
let audioRecordingDotNode: ASImageNode
|
||||
if let currentAudioRecordingDotNode = self.audioRecordingDotNode {
|
||||
audioRecordingDotNode = currentAudioRecordingDotNode
|
||||
} else {
|
||||
animateDotSlideIn = transition.isAnimated
|
||||
|
||||
audioRecordingDotNode = ASImageNode()
|
||||
audioRecordingDotNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingDotImage(interfaceState.theme)
|
||||
self.audioRecordingDotNode = audioRecordingDotNode
|
||||
audioRecordingInfoContainerNode.addSubnode(audioRecordingDotNode)
|
||||
}
|
||||
audioRecordingDotNode.frame = CGRect(origin: CGPoint(x: audioRecordingTimeNode.frame.minX - 17.0, y: panelHeight - minimalHeight + floor((minimalHeight - 9.0) / 2.0)), size: CGSize(width: 9.0, height: 9.0))
|
||||
if animateDotSlideIn {
|
||||
let position = audioRecordingDotNode.layer.position
|
||||
audioRecordingDotNode.layer.animatePosition(from: CGPoint(x: position.x - 9.0 - 51.0, y: position.y), to: position, duration: 0.7, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak audioRecordingDotNode] finished in
|
||||
if finished {
|
||||
let animation = CAKeyframeAnimation(keyPath: "opacity")
|
||||
animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber]
|
||||
animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber]
|
||||
animation.duration = 0.5
|
||||
animation.autoreverses = true
|
||||
animation.repeatCount = Float.infinity
|
||||
|
||||
audioRecordingDotNode?.layer.add(animation, forKey: "recording")
|
||||
switch mediaRecordingState {
|
||||
case let .audio(recorder, isLocked):
|
||||
self.micButton.audioRecorder = recorder
|
||||
let audioRecordingInfoContainerNode: ASDisplayNode
|
||||
if let currentAudioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode {
|
||||
audioRecordingInfoContainerNode = currentAudioRecordingInfoContainerNode
|
||||
} else {
|
||||
audioRecordingInfoContainerNode = ASDisplayNode()
|
||||
self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode
|
||||
self.insertSubnode(audioRecordingInfoContainerNode, at: 0)
|
||||
}
|
||||
|
||||
var animateCancelSlideIn = false
|
||||
let audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator
|
||||
if let currentAudioRecordingCancelIndicator = self.audioRecordingCancelIndicator {
|
||||
audioRecordingCancelIndicator = currentAudioRecordingCancelIndicator
|
||||
} else {
|
||||
animateCancelSlideIn = transition.isAnimated
|
||||
|
||||
audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings, cancel: { [weak self] in
|
||||
self?.interfaceInteraction?.finishMediaRecording(false)
|
||||
})
|
||||
self.audioRecordingCancelIndicator = audioRecordingCancelIndicator
|
||||
self.insertSubnode(audioRecordingCancelIndicator, at: 0)
|
||||
}
|
||||
|
||||
audioRecordingCancelIndicator.frame = CGRect(origin: CGPoint(x: floor((width - audioRecordingCancelIndicator.bounds.size.width) / 2.0) - self.micButton.controlsOffset, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)), size: audioRecordingCancelIndicator.bounds.size)
|
||||
|
||||
if animateCancelSlideIn {
|
||||
let position = audioRecordingCancelIndicator.layer.position
|
||||
audioRecordingCancelIndicator.layer.animatePosition(from: CGPoint(x: width + audioRecordingCancelIndicator.bounds.size.width, y: position.y), to: position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
audioRecordingCancelIndicator.updateIsDisplayingCancel(isLocked, animated: !animateCancelSlideIn)
|
||||
|
||||
var animateTimeSlideIn = false
|
||||
let audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode
|
||||
if let currentAudioRecordingTimeNode = self.audioRecordingTimeNode {
|
||||
audioRecordingTimeNode = currentAudioRecordingTimeNode
|
||||
} else {
|
||||
audioRecordingTimeNode = ChatTextInputAudioRecordingTimeNode(theme: interfaceState.theme)
|
||||
self.audioRecordingTimeNode = audioRecordingTimeNode
|
||||
audioRecordingInfoContainerNode.addSubnode(audioRecordingTimeNode)
|
||||
|
||||
if transition.isAnimated {
|
||||
animateTimeSlideIn = true
|
||||
}
|
||||
}
|
||||
|
||||
let audioRecordingTimeSize = audioRecordingTimeNode.measure(CGSize(width: 200.0, height: 100.0))
|
||||
|
||||
audioRecordingInfoContainerNode.frame = CGRect(origin: CGPoint(x: min(0.0, audioRecordingCancelIndicator.frame.minX - audioRecordingTimeSize.width - 8.0 - 28.0), y: 0.0), size: CGSize(width: width, height: panelHeight))
|
||||
|
||||
audioRecordingTimeNode.frame = CGRect(origin: CGPoint(x: 28.0, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0)), size: audioRecordingTimeSize)
|
||||
if animateTimeSlideIn {
|
||||
let position = audioRecordingTimeNode.layer.position
|
||||
audioRecordingTimeNode.layer.animatePosition(from: CGPoint(x: position.x - 28.0 - audioRecordingTimeSize.width, y: position.y), to: position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
audioRecordingTimeNode.audioRecorder = recorder
|
||||
|
||||
var animateDotSlideIn = false
|
||||
let audioRecordingDotNode: ASImageNode
|
||||
if let currentAudioRecordingDotNode = self.audioRecordingDotNode {
|
||||
audioRecordingDotNode = currentAudioRecordingDotNode
|
||||
} else {
|
||||
animateDotSlideIn = transition.isAnimated
|
||||
|
||||
audioRecordingDotNode = ASImageNode()
|
||||
audioRecordingDotNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingDotImage(interfaceState.theme)
|
||||
self.audioRecordingDotNode = audioRecordingDotNode
|
||||
audioRecordingInfoContainerNode.addSubnode(audioRecordingDotNode)
|
||||
}
|
||||
audioRecordingDotNode.frame = CGRect(origin: CGPoint(x: audioRecordingTimeNode.frame.minX - 17.0, y: panelHeight - minimalHeight + floor((minimalHeight - 9.0) / 2.0)), size: CGSize(width: 9.0, height: 9.0))
|
||||
if animateDotSlideIn {
|
||||
let position = audioRecordingDotNode.layer.position
|
||||
audioRecordingDotNode.layer.animatePosition(from: CGPoint(x: position.x - 9.0 - 51.0, y: position.y), to: position, duration: 0.7, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak audioRecordingDotNode] finished in
|
||||
if finished {
|
||||
let animation = CAKeyframeAnimation(keyPath: "opacity")
|
||||
animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.0 as NSNumber]
|
||||
animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber]
|
||||
animation.duration = 0.5
|
||||
animation.autoreverses = true
|
||||
animation.repeatCount = Float.infinity
|
||||
|
||||
audioRecordingDotNode?.layer.add(animation, forKey: "recording")
|
||||
}
|
||||
})
|
||||
}
|
||||
case let .video(status, _):
|
||||
switch status {
|
||||
case let .recording(recordingStatus):
|
||||
self.micButton.videoRecordingStatus = recordingStatus
|
||||
case .editing:
|
||||
self.micButton.videoRecordingStatus = nil
|
||||
hideMicButton = true
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
self.micButton.audioRecorder = nil
|
||||
self.micButton.videoRecordingStatus = nil
|
||||
transition.updateAlpha(layer: self.textInputBackgroundView.layer, alpha: 1.0)
|
||||
if let textInputNode = self.textInputNode {
|
||||
transition.updateAlpha(node: textInputNode, alpha: 1.0)
|
||||
@@ -662,6 +753,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
composeButtonsOffset = 44.0
|
||||
textInputBackgroundWidthOffset = 36.0
|
||||
}
|
||||
|
||||
self.micButton.layoutItems()
|
||||
transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(x: width - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight)))
|
||||
transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(x: width - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight)))
|
||||
|
||||
@@ -731,6 +824,24 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
|
||||
hideMicButton = true
|
||||
}
|
||||
|
||||
if self.extendedSearchLayout {
|
||||
hideMicButton = true
|
||||
}
|
||||
|
||||
if hideMicButton {
|
||||
if !self.micButton.alpha.isZero {
|
||||
transition.updateAlpha(layer: self.micButton.layer, alpha: 0.0)
|
||||
}
|
||||
} else {
|
||||
if self.micButton.alpha.isZero {
|
||||
transition.updateAlpha(layer: self.micButton.layer, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
|
||||
@@ -744,12 +855,25 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
|
||||
private func updateTextNodeText(animated: Bool) {
|
||||
var hasText = false
|
||||
var hideMicButton = false
|
||||
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
|
||||
hasText = true
|
||||
hideMicButton = true
|
||||
}
|
||||
self.textPlaceholderNode.isHidden = hasText
|
||||
|
||||
if let presentationInterfaceState = self.presentationInterfaceState {
|
||||
if let mediaRecordingState = presentationInterfaceState.inputTextPanelState.mediaRecordingState {
|
||||
if case .video(.editing, false) = mediaRecordingState {
|
||||
hideMicButton = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var animateWithBounce = false
|
||||
if self.extendedSearchLayout {
|
||||
hideMicButton = true
|
||||
|
||||
if !self.sendButton.alpha.isZero {
|
||||
self.sendButton.alpha = 0.0
|
||||
if animated {
|
||||
@@ -757,13 +881,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
self.sendButton.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2)
|
||||
}
|
||||
}
|
||||
if !self.micButton.alpha.isZero {
|
||||
self.micButton.alpha = 0.0
|
||||
if animated {
|
||||
self.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
self.micButton.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2)
|
||||
}
|
||||
}
|
||||
if self.searchLayoutClearButton.alpha.isZero {
|
||||
self.searchLayoutClearButton.alpha = 1.0
|
||||
if animated {
|
||||
@@ -772,7 +889,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var animateWithBounce = true
|
||||
animateWithBounce = true
|
||||
if !self.searchLayoutClearButton.alpha.isZero {
|
||||
animateWithBounce = false
|
||||
self.searchLayoutClearButton.alpha = 0.0
|
||||
@@ -783,6 +900,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
|
||||
if hasText || self.keepSendButtonEnabled {
|
||||
hideMicButton = true
|
||||
if self.sendButton.alpha.isZero {
|
||||
self.sendButton.alpha = 1.0
|
||||
if animated {
|
||||
@@ -794,24 +912,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
if !self.micButton.alpha.isZero {
|
||||
self.micButton.alpha = 0.0
|
||||
if animated {
|
||||
self.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.micButton.alpha.isZero {
|
||||
self.micButton.alpha = 1.0
|
||||
if animated {
|
||||
self.micButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
if animateWithBounce {
|
||||
self.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
|
||||
} else {
|
||||
self.micButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !self.sendButton.alpha.isZero {
|
||||
self.sendButton.alpha = 0.0
|
||||
if animated {
|
||||
@@ -821,6 +922,27 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if hideMicButton {
|
||||
if !self.micButton.alpha.isZero {
|
||||
self.micButton.alpha = 0.0
|
||||
if animated {
|
||||
self.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.micButton.alpha.isZero {
|
||||
self.micButton.alpha = 1.0
|
||||
if animated {
|
||||
self.micButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
if animateWithBounce {
|
||||
self.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
|
||||
} else {
|
||||
self.micButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: self.bounds.size.width)
|
||||
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight)
|
||||
if !self.bounds.size.height.isEqual(to: panelHeight) {
|
||||
@@ -916,4 +1038,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let audioRecordingCancelIndicator = self.audioRecordingCancelIndicator {
|
||||
if let result = audioRecordingCancelIndicator.hitTest(point.offsetBy(dx: -audioRecordingCancelIndicator.frame.minX, dy: -audioRecordingCancelIndicator.frame.minY), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,6 +632,6 @@ final class ContactListNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ private let rootNavigationBar = PresentationThemeRootNavigationBar(
|
||||
controlColor: UIColor(rgb: 0x5e5e5e),
|
||||
accentTextColor: accentColor,
|
||||
backgroundColor: UIColor(rgb: 0x121212),
|
||||
separatorColor: UIColor(rgb: 0x1a1a1a)
|
||||
separatorColor: UIColor(rgb: 0x1a1a1a),
|
||||
badgeBackgroundColor: UIColor(rgb: 0xff3600),
|
||||
badgeTextColor: .white
|
||||
)
|
||||
|
||||
private let activeNavigationSearchBar = PresentationThemeActiveNavigationSearchBar(
|
||||
|
||||
@@ -15,7 +15,8 @@ private let rootTabBar = PresentationThemeRootTabBar(
|
||||
textColor: UIColor(rgb: 0x929292),
|
||||
selectedTextColor: accentColor,
|
||||
badgeBackgroundColor: UIColor(rgb: 0xff3b30),
|
||||
badgeTextColor: .white)
|
||||
badgeTextColor: .white
|
||||
)
|
||||
|
||||
private let rootNavigationBar = PresentationThemeRootNavigationBar(
|
||||
buttonColor: accentColor,
|
||||
@@ -24,7 +25,9 @@ private let rootNavigationBar = PresentationThemeRootNavigationBar(
|
||||
controlColor: UIColor(rgb: 0x7e8791),
|
||||
accentTextColor: accentColor,
|
||||
backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0),
|
||||
separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0)
|
||||
separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0),
|
||||
badgeBackgroundColor: UIColor(rgb: 0xff3b30),
|
||||
badgeTextColor: .white
|
||||
)
|
||||
|
||||
private let activeNavigationSearchBar = PresentationThemeActiveNavigationSearchBar(
|
||||
|
||||
@@ -134,20 +134,16 @@ private func fetchCachedVideoFirstFrameRepresentation(account: Account, resource
|
||||
if resourceData.complete {
|
||||
let tempFilePath = NSTemporaryDirectory() + "\(arc4random()).mov"
|
||||
|
||||
_ = try? FileManager.default.removeItem(atPath: tempFilePath)
|
||||
_ = try? FileManager.default.linkItem(atPath: resourceData.path, toPath: tempFilePath)
|
||||
|
||||
var fullSizeImage: CGImage?
|
||||
|
||||
let asset = AVAsset(url: URL(fileURLWithPath: tempFilePath))
|
||||
let imageGenerator = AVAssetImageGenerator(asset: asset)
|
||||
imageGenerator.maximumSize = CGSize(width: 800.0, height: 800.0)
|
||||
imageGenerator.appliesPreferredTrackTransform = true
|
||||
if let image = try? imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) {
|
||||
fullSizeImage = image
|
||||
}
|
||||
|
||||
if let fullSizeImage = fullSizeImage {
|
||||
do {
|
||||
let _ = try? FileManager.default.removeItem(atPath: tempFilePath)
|
||||
try FileManager.default.linkItem(atPath: resourceData.path, toPath: tempFilePath)
|
||||
|
||||
let asset = AVAsset(url: URL(fileURLWithPath: tempFilePath))
|
||||
let imageGenerator = AVAssetImageGenerator(asset: asset)
|
||||
imageGenerator.maximumSize = CGSize(width: 800.0, height: 800.0)
|
||||
imageGenerator.appliesPreferredTrackTransform = true
|
||||
let fullSizeImage = try imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil)
|
||||
|
||||
var randomId: Int64 = 0
|
||||
arc4random_buf(&randomId, 8)
|
||||
let path = NSTemporaryDirectory() + "\(randomId)"
|
||||
@@ -170,6 +166,8 @@ private func fetchCachedVideoFirstFrameRepresentation(account: Account, resource
|
||||
|
||||
subscriber.putNext(CachedMediaResourceRepresentationResult(temporaryPath: path))
|
||||
subscriber.putCompletion()
|
||||
} catch (let e) {
|
||||
print("\(e)")
|
||||
}
|
||||
}
|
||||
return EmptyDisposable
|
||||
|
||||
@@ -35,6 +35,11 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.backgroundColor = UIColor.black
|
||||
self.scrollView = UIScrollView()
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
self.pager = GalleryPagerNode(pageGap: pageGap)
|
||||
self.footerNode = GalleryFooterNode(controllerInteraction: controllerInteraction)
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
init(pageGap: CGFloat) {
|
||||
self.pageGap = pageGap
|
||||
self.scrollView = UIScrollView()
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
@@ -145,7 +148,7 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
private func updateItemNodes() {
|
||||
if self.items.isEmpty {
|
||||
if self.items.isEmpty || self.containerLayout == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ public struct GeneratedMediaStoreSettings: PreferencesEntry, Equatable {
|
||||
self.storeEditedPhotos = storeEditedPhotos
|
||||
}
|
||||
|
||||
public init(decoder: Decoder) {
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.storeEditedPhotos = decoder.decodeInt32ForKey("eph", orElse: 0) != 0
|
||||
}
|
||||
|
||||
public func encode(_ encoder: Encoder) {
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.storeEditedPhotos ? 1 : 0, forKey: "eph")
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@ struct InAppNotificationSettings: PreferencesEntry, Equatable {
|
||||
self.displayPreviews = displayPreviews
|
||||
}
|
||||
|
||||
init(decoder: Decoder) {
|
||||
init(decoder: PostboxDecoder) {
|
||||
self.playSounds = decoder.decodeInt32ForKey("s", orElse: 0) != 0
|
||||
self.vibrate = decoder.decodeInt32ForKey("v", orElse: 0) != 0
|
||||
self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0
|
||||
}
|
||||
|
||||
func encode(_ encoder: Encoder) {
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.playSounds ? 1 : 0, forKey: "s")
|
||||
encoder.encodeInt32(self.vibrate ? 1 : 0, forKey: "v")
|
||||
encoder.encodeInt32(self.displayPreviews ? 1 : 0, forKey: "p")
|
||||
|
||||
211
TelegramUI/InstantImageGalleryItem.swift
Normal file
@@ -0,0 +1,211 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
class InstantImageGalleryItem: GalleryItem {
|
||||
let account: Account
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let image: TelegramMediaImage
|
||||
let caption: String
|
||||
let location: InstantPageGalleryEntryLocation
|
||||
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, image: TelegramMediaImage, caption: String, location: InstantPageGalleryEntryLocation) {
|
||||
self.account = account
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.image = image
|
||||
self.caption = caption
|
||||
self.location = location
|
||||
}
|
||||
|
||||
func node() -> GalleryItemNode {
|
||||
let node = InstantImageGalleryItemNode(account: self.account, theme: self.theme, strings: self.strings)
|
||||
|
||||
node.setImage(image: self.image)
|
||||
|
||||
node._title.set(.single("\(self.location.position + 1) of \(self.location.totalCount)"))
|
||||
|
||||
node.setCaption(self.caption)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func updateNode(node: GalleryItemNode) {
|
||||
if let node = node as? InstantImageGalleryItemNode {
|
||||
node._title.set(.single("\(self.location.position + 1) of \(self.location.totalCount)"))
|
||||
|
||||
node.setCaption(self.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
private let account: Account
|
||||
|
||||
private let imageNode: TransformImageNode
|
||||
fileprivate let _ready = Promise<Void>()
|
||||
fileprivate let _title = Promise<String>()
|
||||
private let footerContentNode: InstantPageGalleryFooterContentNode
|
||||
|
||||
private var accountAndMedia: (Account, Media)?
|
||||
|
||||
private var fetchDisposable = MetaDisposable()
|
||||
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.account = account
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
self.footerContentNode = InstantPageGalleryFooterContentNode(account: account, theme: theme, strings: strings)
|
||||
|
||||
super.init()
|
||||
|
||||
self.imageNode.imageUpdated = { [weak self] in
|
||||
self?._ready.set(.single(Void()))
|
||||
}
|
||||
|
||||
self.imageNode.view.contentMode = .scaleAspectFill
|
||||
self.imageNode.clipsToBounds = true
|
||||
}
|
||||
|
||||
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 setCaption(_ caption: String) {
|
||||
self.footerContentNode.setCaption(caption)
|
||||
}
|
||||
|
||||
fileprivate func setImage(image: TelegramMediaImage) {
|
||||
if self.accountAndMedia == nil || !self.accountAndMedia!.1.isEqual(image) {
|
||||
if let largestSize = largestRepresentationForPhoto(image) {
|
||||
let displaySize = largestSize.dimensions.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor
|
||||
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(largestSize.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).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: false), 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 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.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(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start())
|
||||
} else {
|
||||
self.fetchDisposable.set(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func title() -> Signal<String, NoError> {
|
||||
return self._title.get()
|
||||
}
|
||||
|
||||
override func footerContent() -> Signal<GalleryFooterContentNode?, NoError> {
|
||||
return .single(self.footerContentNode)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
final class InstantPageAnchorItem: InstantPageItem {
|
||||
let hasLinks: Bool = false
|
||||
let wantsNode: Bool = false
|
||||
let medias: [InstantPageMedia] = []
|
||||
|
||||
@@ -21,7 +21,7 @@ final class InstantPageAnchorItem: InstantPageItem {
|
||||
func drawInTile(context: CGContext) {
|
||||
}
|
||||
|
||||
func node(account: Account) -> InstantPageNode? {
|
||||
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ final class InstantPageAnchorItem: InstantPageItem {
|
||||
return false
|
||||
}
|
||||
|
||||
func linkSelectionViews() -> [InstantPageLinkSelectionView] {
|
||||
func linkSelectionRects(at point: CGPoint) -> [CGRect] {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
55
TelegramUI/InstantPageAudioItem.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
final class InstantPageAudioItem: InstantPageItem {
|
||||
var frame: CGRect
|
||||
let wantsNode: Bool = true
|
||||
let medias: [InstantPageMedia]
|
||||
|
||||
let media: InstantPageMedia
|
||||
let webpage: TelegramMediaWebpage
|
||||
|
||||
init(frame: CGRect, media: InstantPageMedia, webpage: TelegramMediaWebpage) {
|
||||
self.frame = frame
|
||||
self.media = media
|
||||
self.webpage = webpage
|
||||
self.medias = [media]
|
||||
}
|
||||
|
||||
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? {
|
||||
return InstantPageAudioNode(account: account, strings: strings, theme: theme, webpage: self.webpage, media: self.media, openMedia: openMedia)
|
||||
}
|
||||
|
||||
func matchesAnchor(_ anchor: String) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func matchesNode(_ node: InstantPageNode) -> Bool {
|
||||
if let node = node as? InstantPageAudioNode {
|
||||
return self.media == node.media
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func distanceThresholdGroup() -> Int? {
|
||||
return 4
|
||||
}
|
||||
|
||||
func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat {
|
||||
if count > 3 {
|
||||
return 1000.0
|
||||
} else {
|
||||
return CGFloat.greatestFiniteMagnitude
|
||||
}
|
||||
}
|
||||
|
||||
func linkSelectionRects(at point: CGPoint) -> [CGRect] {
|
||||
return []
|
||||
}
|
||||
|
||||
func drawInTile(context: CGContext) {
|
||||
}
|
||||
}
|
||||
|
||||
264
TelegramUI/InstantPageAudioNode.swift
Normal file
@@ -0,0 +1,264 @@
|
||||
import Foundation
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
private func generatePlayButton(color: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 48.0, height: 48.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(color.cgColor)
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setLineWidth(1.65)
|
||||
let _ = try? drawSvgPath(context, path: "M24,0.825 C11.2008009,0.825 0.825,11.2008009 0.825,24 C0.825,36.7991991 11.2008009,47.175 24,47.175 C36.7991991,47.175 47.175,36.7991991 47.175,24 C47.175,11.2008009 36.7991991,0.825 24,0.825 S ")
|
||||
let _ = try? drawSvgPath(context, path: "M19,16.8681954 L19,32.1318046 L19,32.1318046 C19,32.6785665 19.4432381,33.1218046 19.99,33.1218046 C20.1882157,33.1218046 20.3818677,33.0623041 20.5458864,32.9510057 L31.7927564,25.319201 L31.7927564,25.319201 C32.2451886,25.0121934 32.3630786,24.3965458 32.056071,23.9441136 C31.9857457,23.8404762 31.8963938,23.7511243 31.7927564,23.680799 L20.5458864,16.0489943 L20.5458864,16.0489943 C20.0934542,15.7419868 19.4778066,15.8598767 19.170799,16.312309 C19.0595006,16.4763277 19,16.6699796 19,16.8681954 Z ")
|
||||
})
|
||||
}
|
||||
|
||||
private func generatePauseButton(color: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 48.0, height: 48.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(color.cgColor)
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setLineWidth(1.65)
|
||||
|
||||
let _ = try? drawSvgPath(context, path: "M24,0.825 C11.2008009,0.825 0.825,11.2008009 0.825,24 C0.825,36.7991991 11.2008009,47.175 24,47.175 C36.7991991,47.175 47.175,36.7991991 47.175,24 C47.175,11.2008009 36.7991991,0.825 24,0.825 S ")
|
||||
let _ = try? drawSvgPath(context, path: "M17,16 L21,16 C21.5567619,16 22,16.4521029 22,17 L22,32 C22,32.5478971 21.5567619,33 21,33 L17,33 C16.4432381,33 16,32.5478971 16,32 L16,17 C16,16.4521029 16.4432381,16 17,16 Z ")
|
||||
let _ = try? drawSvgPath(context, path: "M26.99,16 L31.01,16 C31.5567619,16 32,16.4432381 32,16.99 L32,32.01 C32,32.5567619 31.5567619,33 31.01,33 L26.99,33 C26.4432381,33 26,32.5567619 26,32.01 L26,16.99 C26,16.4432381 26.4432381,16 26.99,16 Z ")
|
||||
})
|
||||
}
|
||||
|
||||
private func titleString(media: InstantPageMedia, theme: InstantPageTheme) -> NSAttributedString {
|
||||
let string = NSMutableAttributedString()
|
||||
if let file = media.media as? TelegramMediaFile {
|
||||
loop: for attribute in file.attributes {
|
||||
if case let .Audio(isVoice, _, title, performer, _) = attribute, !isVoice {
|
||||
let titleText: String = title ?? "Unknown Track"
|
||||
let subtitleText: String = performer ?? "Unknown Artist"
|
||||
|
||||
let titleString = NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: theme.textCategories.paragraph.color)
|
||||
let subtitleString = NSAttributedString(string: " — \(subtitleText)", font: Font.regular(17.0), textColor: theme.textCategories.paragraph.color)
|
||||
|
||||
string.append(titleString)
|
||||
string.append(subtitleString)
|
||||
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
final class InstantPageAudioNode: ASDisplayNode, InstantPageNode {
|
||||
private let account: Account
|
||||
let media: InstantPageMedia
|
||||
private let openMedia: (InstantPageMedia) -> Void
|
||||
private var strings: PresentationStrings
|
||||
private var theme: InstantPageTheme
|
||||
|
||||
private var playImage: UIImage
|
||||
private var pauseImage: UIImage
|
||||
|
||||
private let buttonNode: HighlightableButtonNode
|
||||
private let statusNode: RadialStatusNode
|
||||
private let titleNode: ASTextNode
|
||||
private let scrubbingNode: MediaPlayerScrubbingNode
|
||||
private var playbackStatusDisposable: Disposable?
|
||||
private var playerStatusDisposable: Disposable?
|
||||
|
||||
private var isPlaying: Bool = false
|
||||
private var playlistStateAndStatus: AudioPlaylistStateAndStatus?
|
||||
|
||||
init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, webpage: TelegramMediaWebpage, media: InstantPageMedia, openMedia: @escaping (InstantPageMedia) -> Void) {
|
||||
self.account = account
|
||||
self.strings = strings
|
||||
self.theme = theme
|
||||
self.media = media
|
||||
self.openMedia = openMedia
|
||||
|
||||
self.playImage = generatePlayButton(color: theme.textCategories.paragraph.color)!
|
||||
self.pauseImage = generatePauseButton(color: theme.textCategories.paragraph.color)!
|
||||
|
||||
self.buttonNode = HighlightableButtonNode()
|
||||
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
|
||||
var backgroundAlpha: CGFloat = 0.1
|
||||
var brightness: CGFloat = 0.0
|
||||
theme.textCategories.paragraph.color.getHue(nil, saturation: nil, brightness: &brightness, alpha: nil)
|
||||
if brightness > 0.5 {
|
||||
backgroundAlpha = 0.4
|
||||
}
|
||||
self.scrubbingNode = MediaPlayerScrubbingNode(lineHeight: 3.0, lineCap: .round, scrubberHandle: true, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color)
|
||||
|
||||
super.init()
|
||||
|
||||
self.titleNode.attributedText = titleString(media: media, theme: theme)
|
||||
|
||||
self.addSubnode(self.statusNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.scrubbingNode)
|
||||
|
||||
self.statusNode.transitionToState(RadialStatusNodeState.customIcon(self.playImage), animated: false, completion: {})
|
||||
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.statusNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.statusNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.statusNode.alpha = 1.0
|
||||
strongSelf.statusNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.scrubbingNode.seek = { [weak self] timestamp in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.playlistStateAndStatus {
|
||||
strongSelf.account.telegramApplicationContext.mediaManager.playlistPlayerControl(AudioPlaylistControl.playback(.seek(timestamp)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let applicationContext = account.applicationContext as? TelegramApplicationContext, let (playlistId, itemId) = instantPageAudioPlaylistAndItemIds(webpage: webpage, media: self.media) {
|
||||
let playbackStatus: Signal<MediaPlayerPlaybackStatus?, NoError> = applicationContext.mediaManager.filteredPlaylistPlayerStateAndStatus(playlistId: playlistId, itemId: itemId)
|
||||
|> mapToSignal { status -> Signal<MediaPlayerPlaybackStatus?, NoError> in
|
||||
if let status = status, let playbackStatus = status.status {
|
||||
return playbackStatus
|
||||
|> map { playbackStatus -> MediaPlayerPlaybackStatus? in
|
||||
return playbackStatus.status
|
||||
}
|
||||
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
||||
return lhs == rhs
|
||||
})
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
self.playbackStatusDisposable = (playbackStatus |> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let strongSelf = self {
|
||||
var isPlaying = false
|
||||
if let status = status {
|
||||
switch status {
|
||||
case .paused:
|
||||
break
|
||||
case let .buffering(whilePlaying):
|
||||
isPlaying = whilePlaying
|
||||
case .playing:
|
||||
isPlaying = true
|
||||
}
|
||||
}
|
||||
if strongSelf.isPlaying != isPlaying {
|
||||
strongSelf.isPlaying = isPlaying
|
||||
if isPlaying {
|
||||
strongSelf.statusNode.transitionToState(RadialStatusNodeState.customIcon(strongSelf.pauseImage), animated: false, completion: {})
|
||||
} else {
|
||||
strongSelf.statusNode.transitionToState(RadialStatusNodeState.customIcon(strongSelf.playImage), animated: false, completion: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
self.playerStatusDisposable = (applicationContext.mediaManager.playlistPlayerStateAndStatus
|
||||
|> deliverOnMainQueue).start(next: { [weak self] playlistStateAndStatus in
|
||||
if let strongSelf = self {
|
||||
var filteredValue: AudioPlaylistStateAndStatus?
|
||||
if let playlistStateAndStatus = playlistStateAndStatus {
|
||||
if playlistStateAndStatus.state.playlistId.isEqual(to: playlistId) {
|
||||
if let item = playlistStateAndStatus.state.item {
|
||||
if item.id.isEqual(to: itemId) {
|
||||
filteredValue = playlistStateAndStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if strongSelf.playlistStateAndStatus != filteredValue {
|
||||
strongSelf.playlistStateAndStatus = filteredValue
|
||||
strongSelf.scrubbingNode.status = filteredValue?.status
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.playbackStatusDisposable?.dispose()
|
||||
self.playerStatusDisposable?.dispose()
|
||||
}
|
||||
|
||||
func update(strings: PresentationStrings, theme: InstantPageTheme) {
|
||||
if self.strings !== strings || self.theme !== theme {
|
||||
let themeUpdated = self.theme !== theme
|
||||
self.strings = strings
|
||||
self.theme = theme
|
||||
|
||||
if themeUpdated {
|
||||
self.playImage = generatePlayButton(color: theme.textCategories.paragraph.color)!
|
||||
self.pauseImage = generatePauseButton(color: theme.textCategories.paragraph.color)!
|
||||
|
||||
self.titleNode.attributedText = titleString(media: self.media, theme: theme)
|
||||
|
||||
var backgroundAlpha: CGFloat = 0.1
|
||||
var brightness: CGFloat = 0.0
|
||||
theme.textCategories.paragraph.color.getHue(nil, saturation: nil, brightness: &brightness, alpha: nil)
|
||||
if brightness > 0.5 {
|
||||
backgroundAlpha = 0.4
|
||||
}
|
||||
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func transitionNode(media: InstantPageMedia) -> ASDisplayNode? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateHiddenMedia(media: InstantPageMedia?) {
|
||||
}
|
||||
|
||||
func updateIsVisible(_ isVisible: Bool) {
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
if let _ = self.playlistStateAndStatus {
|
||||
if self.isPlaying { self.account.telegramApplicationContext.mediaManager.playlistPlayerControl(AudioPlaylistControl.playback(.pause))
|
||||
} else {
|
||||
self.account.telegramApplicationContext.mediaManager.playlistPlayerControl(AudioPlaylistControl.playback(.play))
|
||||
}
|
||||
} else {
|
||||
self.openMedia(self.media)
|
||||
}
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 17.0, bottom: 18.0, right: 17.0)
|
||||
let leftInset: CGFloat = 46.0 + 10.0
|
||||
let rightInset: CGFloat = 0.0
|
||||
|
||||
let maxTitleWidth = max(1.0, size.width - insets.left - leftInset - rightInset - insets.right)
|
||||
let titleSize = self.titleNode.measure(CGSize(width: maxTitleWidth, height: size.height))
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: insets.left + leftInset, y: 2.0), size: titleSize)
|
||||
|
||||
self.buttonNode.frame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: CGSize(width: 48.0, height: 48.0))
|
||||
self.statusNode.frame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: CGSize(width: 48.0, height: 48.0))
|
||||
|
||||
var topOffset: CGFloat = 0.0
|
||||
if self.titleNode.attributedText == nil || self.titleNode.attributedText!.length == 0 {
|
||||
topOffset = -10.0
|
||||
}
|
||||
|
||||
let leftScrubberInset: CGFloat = insets.left + 46.0 + 10.0
|
||||
let rightScrubberInset: CGFloat = insets.right
|
||||
self.scrubbingNode.frame = CGRect(origin: CGPoint(x: leftScrubberInset, y: 26.0 + topOffset), size: CGSize(width: size.width - leftScrubberInset - rightScrubberInset, height: 15.0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import Foundation
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
|
||||
final class InstantPageController: ViewController {
|
||||
private let account: Account
|
||||
private let webPage: TelegramMediaWebpage
|
||||
private var webPage: TelegramMediaWebpage
|
||||
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
override var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
var controllerNode: InstantPageControllerNode {
|
||||
return self.displayNode as! InstantPageControllerNode
|
||||
}
|
||||
|
||||
private var webpageDisposable: Disposable?
|
||||
|
||||
private var settings: InstantPagePresentationSettings?
|
||||
private var settingsDisposable: Disposable?
|
||||
|
||||
init(account: Account, webPage: TelegramMediaWebpage) {
|
||||
self.account = account
|
||||
self.presentationData = (account.telegramApplicationContext.currentPresentationData.with { $0 })
|
||||
@@ -21,14 +33,52 @@ final class InstantPageController: ViewController {
|
||||
super.init(navigationBarTheme: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
|
||||
self.webpageDisposable = (actualizedWebpage(postbox: self.account.postbox, network: self.account.network, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
strongSelf.webPage = result
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.controllerNode.updateWebPage(result)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.settingsDisposable = (self.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.instantPagePresentationSettings]) |> deliverOnMainQueue).start(next: { [weak self] view in
|
||||
if let strongSelf = self {
|
||||
let settings: InstantPagePresentationSettings
|
||||
if let current = view.values[ApplicationSpecificPreferencesKeys.instantPagePresentationSettings] as? InstantPagePresentationSettings {
|
||||
settings = current
|
||||
} else {
|
||||
settings = InstantPagePresentationSettings.defaultSettings
|
||||
}
|
||||
strongSelf.settings = settings
|
||||
if strongSelf.isNodeLoaded {
|
||||
strongSelf.controllerNode.update(settings: settings, strings: strongSelf.presentationData.strings)
|
||||
}
|
||||
strongSelf._ready.set(.single(true))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.webpageDisposable?.dispose()
|
||||
self.settingsDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = InstantPageControllerNode(account: self.account, strings: self.presentationData.strings, statusBar: self.statusBar)
|
||||
self.displayNode = InstantPageControllerNode(account: self.account, settings: self.settings, strings: self.presentationData.strings, statusBar: self.statusBar, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root), with: a)
|
||||
}, openPeer: { [weak self] peerId in
|
||||
if let strongSelf = self {
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId))
|
||||
}
|
||||
}, navigateBack: { [weak self] in
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
})
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
|
||||
@@ -1,37 +1,55 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SafariServices
|
||||
|
||||
final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private let account: Account
|
||||
private var settings: InstantPagePresentationSettings?
|
||||
private var strings: PresentationStrings
|
||||
private var theme: InstantPageTheme?
|
||||
private let present: (ViewController, Any?) -> Void
|
||||
private let openPeer: (PeerId) -> Void
|
||||
|
||||
private var webPage: TelegramMediaWebpage?
|
||||
|
||||
|
||||
private var containerLayout: ContainerViewLayout?
|
||||
private let statusBar: StatusBar
|
||||
private let navigationBar: InstantPageNavigationBar
|
||||
private let scrollNode: ASScrollNode
|
||||
private let scrollNodeHeader: ASDisplayNode
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
private var textSelectionNode: LinkHighlightingNode?
|
||||
private var settingsNode: InstantPageSettingsNode?
|
||||
private var settingsDimNode: ASDisplayNode?
|
||||
|
||||
var currentLayout: InstantPageLayout?
|
||||
var currentLayoutTiles: [InstantPageTile] = []
|
||||
var currentLayoutItemsWithViews: [InstantPageItem] = []
|
||||
var currentLayoutItemsWithLinks: [InstantPageItem] = []
|
||||
var distanceThresholdGroupCount: [Int: Int] = [:]
|
||||
|
||||
var visibleTiles: [Int: InstantPageTileNode] = [:]
|
||||
var visibleItemsWithViews: [Int: InstantPageNode] = [:]
|
||||
var visibleLinkSelectionViews: [Int: InstantPageLinkSelectionView] = [:]
|
||||
|
||||
var previousContentOffset: CGPoint?
|
||||
var isDeceleratingBecauseOfDragging = false
|
||||
|
||||
init(account: Account, strings: PresentationStrings, statusBar: StatusBar) {
|
||||
private let hiddenMediaDisposable = MetaDisposable()
|
||||
private let resolveUrlDisposable = MetaDisposable()
|
||||
|
||||
init(account: Account, settings: InstantPagePresentationSettings?, strings: PresentationStrings, statusBar: StatusBar, present: @escaping (ViewController, Any?) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) {
|
||||
self.account = account
|
||||
self.strings = strings
|
||||
self.settings = settings
|
||||
self.theme = settings.flatMap(instantPageThemeForSettings)
|
||||
|
||||
self.statusBar = statusBar
|
||||
self.present = present
|
||||
self.openPeer = openPeer
|
||||
|
||||
self.navigationBar = InstantPageNavigationBar(strings: strings)
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNodeHeader = ASDisplayNode()
|
||||
@@ -43,11 +61,129 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
return UITracingLayerView()
|
||||
})
|
||||
|
||||
self.backgroundColor = .white
|
||||
if let theme = self.theme {
|
||||
self.backgroundColor = theme.pageBackgroundColor
|
||||
}
|
||||
self.addSubnode(self.scrollNode)
|
||||
self.scrollNode.addSubnode(self.scrollNodeHeader)
|
||||
self.addSubnode(self.navigationBar)
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.delegate = self
|
||||
|
||||
self.navigationBar.back = navigateBack
|
||||
self.navigationBar.share = { [weak self] in
|
||||
if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content {
|
||||
var shareImpl: (([PeerId]) -> Void)?
|
||||
let shareController = ShareController(account: account, shareAction: { peerIds in
|
||||
shareImpl?(peerIds)
|
||||
}, defaultAction: nil)
|
||||
strongSelf.present(shareController, nil)
|
||||
shareImpl = { [weak shareController] peerIds in
|
||||
shareController?.dismiss()
|
||||
|
||||
for peerId in peerIds {
|
||||
let _ = enqueueMessages(account: account, peerId: peerId, messages: [.message(text: content.url, attributes: [], media: nil, replyToMessageId: nil)]).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.navigationBar.settings = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.presentSettings()
|
||||
}
|
||||
}
|
||||
self.navigationBar.scrollToTop = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: -strongSelf.scrollNode.view.contentInset.top), animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.hiddenMediaDisposable.dispose()
|
||||
self.resolveUrlDisposable.dispose()
|
||||
}
|
||||
|
||||
func update(settings: InstantPagePresentationSettings, strings: PresentationStrings) {
|
||||
if self.settings != settings || self.strings !== strings {
|
||||
let previousSettings = self.settings
|
||||
var updateLayout = previousSettings == nil
|
||||
|
||||
self.settings = settings
|
||||
let theme = instantPageThemeForSettings(settings)
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
var animated = false
|
||||
if let previousSettings = previousSettings {
|
||||
if previousSettings.themeType != settings.themeType {
|
||||
updateLayout = true
|
||||
animated = true
|
||||
}
|
||||
if previousSettings.fontSize != settings.fontSize || previousSettings.forceSerif != settings.forceSerif {
|
||||
animated = false
|
||||
updateLayout = true
|
||||
}
|
||||
}
|
||||
|
||||
self.backgroundColor = theme.pageBackgroundColor
|
||||
|
||||
if updateLayout {
|
||||
if animated {
|
||||
if let snapshotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
self.view.insertSubview(snapshotView, aboveSubview: self.scrollNode.view)
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.updateLayout()
|
||||
|
||||
for (_, itemNode) in self.visibleItemsWithViews {
|
||||
itemNode.update(strings: strings, theme: theme)
|
||||
}
|
||||
|
||||
self.updateVisibleItems()
|
||||
self.updateNavigationBar()
|
||||
|
||||
self.recursivelyEnsureDisplaySynchronously(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
recognizer.delaysTouchesBegan = false
|
||||
recognizer.tapActionAtPoint = { [weak self] point in
|
||||
if let strongSelf = self {
|
||||
if let currentLayout = strongSelf.currentLayout {
|
||||
for item in currentLayout.items {
|
||||
if item.frame.contains(point) {
|
||||
if item is InstantPagePeerReferenceItem {
|
||||
return .fail
|
||||
} else if item is InstantPageAudioItem {
|
||||
return .fail
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return .waitForSingleTap
|
||||
}
|
||||
recognizer.highlight = { [weak self] point in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateTouchesAtPoint(point)
|
||||
}
|
||||
}
|
||||
self.scrollNode.view.addGestureRecognizer(recognizer)
|
||||
}
|
||||
|
||||
func updateWebPage(_ webPage: TelegramMediaWebpage?) {
|
||||
@@ -67,18 +203,30 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.containerLayout = layout
|
||||
|
||||
if let settingsDimNode = self.settingsDimNode {
|
||||
transition.updateFrame(node: settingsDimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
}
|
||||
|
||||
if let settingsNode = self.settingsNode {
|
||||
settingsNode.updateLayout(layout: layout, transition: transition)
|
||||
transition.updateFrame(node: settingsNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
}
|
||||
|
||||
let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0
|
||||
let scrollInsetTop = 44.0 + statusBarHeight
|
||||
|
||||
let resetOffset = self.scrollNode.bounds.size.width.isZero
|
||||
let widthUpdated = !self.scrollNode.bounds.size.width.isEqual(to: layout.size.width)
|
||||
|
||||
if self.scrollNode.bounds.size != layout.size || !self.scrollNode.view.contentInset.top.isEqual(to: scrollInsetTop) {
|
||||
if !self.scrollNode.bounds.size.width.isEqual(to: layout.size.width) {
|
||||
self.updateLayout()
|
||||
}
|
||||
self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
||||
self.scrollNodeHeader.frame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0), size: CGSize(width: layout.size.width, height: 2000.0))
|
||||
self.scrollNode.view.contentInset = UIEdgeInsetsMake(scrollInsetTop, 0.0, 0.0, 0.0)
|
||||
if self.visibleItemsWithViews.isEmpty && self.visibleTiles.isEmpty {
|
||||
self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: 0.0)
|
||||
if resetOffset {
|
||||
self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top)
|
||||
}
|
||||
if widthUpdated {
|
||||
self.updateLayout()
|
||||
}
|
||||
self.updateVisibleItems()
|
||||
self.updateNavigationBar()
|
||||
@@ -86,26 +234,20 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
private func updateLayout() {
|
||||
guard let containerLayout = self.containerLayout, let webPage = self.webPage else {
|
||||
guard let containerLayout = self.containerLayout, let webPage = self.webPage, let theme = self.theme else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentLayout = instantPageLayoutForWebPage(webPage, boundingWidth: containerLayout.size.width)
|
||||
let currentLayout = instantPageLayoutForWebPage(webPage, boundingWidth: containerLayout.size.width, strings: self.strings, theme: theme)
|
||||
|
||||
for (_, tileNode) in self.visibleTiles {
|
||||
tileNode.removeFromSupernode()
|
||||
}
|
||||
self.visibleTiles.removeAll()
|
||||
|
||||
for (_, linkView) in self.visibleLinkSelectionViews {
|
||||
linkView.removeFromSuperview()
|
||||
}
|
||||
self.visibleLinkSelectionViews.removeAll()
|
||||
|
||||
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: containerLayout.size.width)
|
||||
|
||||
var currentLayoutItemsWithViews: [InstantPageItem] = []
|
||||
var currentLayoutItemsWithLinks: [InstantPageItem] = []
|
||||
var distanceThresholdGroupCount: [Int: Int] = [:]
|
||||
|
||||
for item in currentLayout.items {
|
||||
@@ -121,26 +263,25 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
distanceThresholdGroupCount[Int(group)] = count + 1
|
||||
}
|
||||
}
|
||||
if item.hasLinks {
|
||||
currentLayoutItemsWithLinks.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
self.currentLayout = currentLayout
|
||||
self.currentLayoutTiles = currentLayoutTiles
|
||||
self.currentLayoutItemsWithViews = currentLayoutItemsWithViews
|
||||
self.currentLayoutItemsWithLinks = currentLayoutItemsWithLinks
|
||||
self.distanceThresholdGroupCount = distanceThresholdGroupCount
|
||||
|
||||
self.scrollNode.view.contentSize = currentLayout.contentSize
|
||||
}
|
||||
|
||||
func updateVisibleItems() {
|
||||
guard let theme = self.theme else {
|
||||
return
|
||||
}
|
||||
|
||||
var visibleTileIndices = Set<Int>()
|
||||
var visibleItemIndices = Set<Int>()
|
||||
var visibleItemLinkIndices = Set<Int>()
|
||||
|
||||
var visibleBounds = self.scrollNode.view.bounds
|
||||
let visibleBounds = self.scrollNode.view.bounds
|
||||
|
||||
var topNode: ASDisplayNode?
|
||||
for node in self.scrollNode.subnodes.reversed() {
|
||||
@@ -160,7 +301,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
visibleTileIndices.insert(tileIndex)
|
||||
|
||||
if visibleTiles[tileIndex] == nil {
|
||||
let tileNode = InstantPageTileNode(tile: tile)
|
||||
let tileNode = InstantPageTileNode(tile: tile, backgroundColor: theme.pageBackgroundColor)
|
||||
tileNode.frame = tile.frame
|
||||
if let topNode = topNode {
|
||||
self.scrollNode.insertSubnode(tileNode, aboveSubnode: topNode)
|
||||
@@ -200,7 +341,11 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
if itemNode == nil {
|
||||
if let itemNode = item.node(account: self.account) {
|
||||
if let itemNode = item.node(account: self.account, strings: self.strings, theme: theme, openMedia: { [weak self] media in
|
||||
self?.openMedia(media)
|
||||
}, openPeer: { [weak self] peerId in
|
||||
self?.openPeer(peerId)
|
||||
}) {
|
||||
(itemNode as! ASDisplayNode).frame = item.frame
|
||||
if let topNode = topNode {
|
||||
self.scrollNode.insertSubnode(itemNode as! ASDisplayNode, aboveSubnode: topNode)
|
||||
@@ -245,37 +390,6 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
for index in removeItemIndices {
|
||||
self.visibleItemsWithViews.removeValue(forKey: index)
|
||||
}
|
||||
|
||||
/*
|
||||
itemIndex = -1;
|
||||
for (id<TGInstantPageLayoutItem> item in _currentLayoutItemsWithLinks) {
|
||||
itemIndex++;
|
||||
CGRect itemFrame = item.frame;
|
||||
if (CGRectIntersectsRect(itemFrame, visibleBounds)) {
|
||||
[visibleItemLinkIndices addObject:@(itemIndex)];
|
||||
|
||||
if (_visibleLinkSelectionViews[@(itemIndex)] == nil) {
|
||||
NSArray<TGInstantPageLinkSelectionView *> *linkViews = [item linkSelectionViews];
|
||||
for (TGInstantPageLinkSelectionView *linkView in linkViews) {
|
||||
linkView.itemTapped = _urlItemTapped;
|
||||
|
||||
[_scrollView addSubview:linkView];
|
||||
}
|
||||
_visibleLinkSelectionViews[@(itemIndex)] = linkViews;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSMutableArray *removeItemLinkIndices = [[NSMutableArray alloc] init];
|
||||
[_visibleLinkSelectionViews enumerateKeysAndObjectsUsingBlock:^(NSNumber *nIndex, NSArray<TGInstantPageLinkSelectionView *> *linkViews, __unused BOOL *stop) {
|
||||
if (![visibleItemLinkIndices containsObject:nIndex]) {
|
||||
for (UIView *linkView in linkViews) {
|
||||
[linkView removeFromSuperview];
|
||||
}
|
||||
[removeItemLinkIndices addObject:nIndex];
|
||||
}
|
||||
}];
|
||||
[_visibleLinkSelectionViews removeObjectsForKeys:removeItemLinkIndices];*/
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
@@ -300,6 +414,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
let bounds = self.scrollNode.view.bounds
|
||||
let contentOffset = self.scrollNode.view.contentOffset
|
||||
|
||||
var pageProgress: CGFloat = 0.0
|
||||
if !self.scrollNode.view.contentSize.height.isZero {
|
||||
let value = (contentOffset.y + self.scrollNode.view.contentInset.top) / (self.scrollNode.view.contentSize.height - bounds.size.height + self.scrollNode.view.contentInset.top)
|
||||
pageProgress = max(0.0, min(1.0, value))
|
||||
}
|
||||
|
||||
let delta: CGFloat
|
||||
if let previousContentOffset = self.previousContentOffset {
|
||||
delta = contentOffset.y - previousContentOffset.y
|
||||
@@ -308,23 +428,6 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
self.previousContentOffset = contentOffset
|
||||
|
||||
/*void (^block)(CGRect) = ^(CGRect navigationBarFrame) {
|
||||
_navigationBar.frame = navigationBarFrame;
|
||||
CGFloat navigationBarHeight = _navigationBar.bounds.size.height;
|
||||
if (navigationBarHeight < FLT_EPSILON)
|
||||
navigationBarHeight = 64.0f;
|
||||
|
||||
CGFloat statusBarOffset = -MAX(0.0f, MIN(_statusBarHeight, _statusBarHeight + 44.0f - navigationBarHeight));
|
||||
if (ABS(_statusBarOffset - statusBarOffset) > FLT_EPSILON) {
|
||||
_statusBarOffset = statusBarOffset;
|
||||
if (_statusBarOffsetUpdated) {
|
||||
_statusBarOffsetUpdated(statusBarOffset);
|
||||
}
|
||||
|
||||
_scrollView.scrollIndicatorInsets = UIEdgeInsetsMake(_navigationBar.bounds.size.height, 0.0f, 0.0f, 0.0f);
|
||||
};
|
||||
};*/
|
||||
|
||||
var transition: ContainedViewLayoutTransition = .immediate
|
||||
var navigationBarFrame = self.navigationBar.frame
|
||||
navigationBarFrame.size.width = bounds.size.width
|
||||
@@ -352,21 +455,330 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
assert(true)
|
||||
}
|
||||
|
||||
let statusBarAlpha = min(1.0, max(0.0, (navigationBarFrame.size.height - 20.0) / 44.0))
|
||||
var statusBarAlpha = min(1.0, max(0.0, (navigationBarFrame.size.height - 20.0) / 44.0))
|
||||
transition.updateAlpha(node: self.statusBar, alpha: statusBarAlpha * statusBarAlpha)
|
||||
self.statusBar.verticalOffset = navigationBarFrame.size.height - 64.0
|
||||
|
||||
transition.updateFrame(node: self.navigationBar, frame: navigationBarFrame)
|
||||
self.navigationBar.updateLayout(size: navigationBarFrame.size, transition: transition)
|
||||
self.navigationBar.updateLayout(size: navigationBarFrame.size, pageProgress: pageProgress, transition: transition)
|
||||
|
||||
transition.animateView {
|
||||
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: navigationBarFrame.size.height, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
}
|
||||
|
||||
/*CGFloat progress = 0.0f;
|
||||
if (_scrollView.contentSize.height > FLT_EPSILON) {
|
||||
progress = MAX(0.0f, MIN(1.0f, (_scrollView.contentOffset.y + _scrollView.contentInset.top) / (_scrollView.contentSize.height - _scrollView.frame.size.height + _scrollView.contentInset.top)));
|
||||
}
|
||||
|
||||
private func updateTouchesAtPoint(_ location: CGPoint?) {
|
||||
var rects: [CGRect]?
|
||||
if let location = location, let currentLayout = self.currentLayout {
|
||||
for item in currentLayout.items {
|
||||
if item.frame.contains(location) {
|
||||
let textNodeFrame = item.frame
|
||||
var itemRects = item.linkSelectionRects(at: location.offsetBy(dx: -item.frame.minX, dy: -item.frame.minY))
|
||||
for i in 0 ..< itemRects.count {
|
||||
itemRects[i] = itemRects[i].offsetBy(dx: textNodeFrame.minX, dy: textNodeFrame.minY).insetBy(dx: -2.0, dy: -2.0)
|
||||
}
|
||||
if !itemRects.isEmpty {
|
||||
rects = itemRects
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let rects = rects {
|
||||
let linkHighlightingNode: LinkHighlightingNode
|
||||
if let current = self.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
} else {
|
||||
linkHighlightingNode = LinkHighlightingNode(color: UIColor(rgb: 0x007BE8).withAlphaComponent(0.4))
|
||||
linkHighlightingNode.isUserInteractionEnabled = false
|
||||
self.linkHighlightingNode = linkHighlightingNode
|
||||
self.scrollNode.addSubnode(linkHighlightingNode)
|
||||
}
|
||||
linkHighlightingNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)
|
||||
linkHighlightingNode.updateRects(rects)
|
||||
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
||||
self.linkHighlightingNode = nil
|
||||
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
||||
linkHighlightingNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func textItemAtLocation(_ location: CGPoint) -> InstantPageTextItem? {
|
||||
if let currentLayout = self.currentLayout {
|
||||
for item in currentLayout.items {
|
||||
if let item = item as? InstantPageTextItem, item.frame.contains(location) {
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func urlForTapLocation(_ location: CGPoint) -> InstantPageUrlItem? {
|
||||
if let item = self.textItemAtLocation(location) {
|
||||
return item.urlAttribute(at: location.offsetBy(dx: -item.frame.minX, dy: -item.frame.minY))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
if let url = self.urlForTapLocation(location) {
|
||||
self.openUrl(url)
|
||||
}
|
||||
case .longTap:
|
||||
if let url = self.urlForTapLocation(location) {
|
||||
let actionSheet = ActionSheetController()
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: url.url),
|
||||
ActionSheetButtonItem(title: self.self.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.openUrl(url)
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: self.strings.Web_CopyLink, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = url.url
|
||||
}),
|
||||
ActionSheetButtonItem(title: self.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let link = URL(string: url.url) {
|
||||
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
|
||||
}
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
self.present(actionSheet, nil)
|
||||
} else if let item = self.textItemAtLocation(location) {
|
||||
let textNodeFrame = item.frame
|
||||
var itemRects = item.lineRects()
|
||||
for i in 0 ..< itemRects.count {
|
||||
itemRects[i] = itemRects[i].offsetBy(dx: textNodeFrame.minX, dy: textNodeFrame.minY).insetBy(dx: -2.0, dy: -2.0)
|
||||
}
|
||||
self.updateTextSelectionRects(itemRects, text: item.plainText())
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTextSelectionRects(_ rects: [CGRect], text: String?) {
|
||||
if let text = text, !rects.isEmpty {
|
||||
let textSelectionNode: LinkHighlightingNode
|
||||
if let current = self.textSelectionNode {
|
||||
textSelectionNode = current
|
||||
} else {
|
||||
textSelectionNode = LinkHighlightingNode(color: UIColor.lightGray.withAlphaComponent(0.4))
|
||||
textSelectionNode.isUserInteractionEnabled = false
|
||||
self.textSelectionNode = textSelectionNode
|
||||
self.scrollNode.addSubnode(textSelectionNode)
|
||||
}
|
||||
textSelectionNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)
|
||||
textSelectionNode.updateRects(rects)
|
||||
|
||||
var coveringRect = rects[0]
|
||||
for i in 1 ..< rects.count {
|
||||
coveringRect = coveringRect.union(rects[i])
|
||||
}
|
||||
|
||||
let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(self.strings.Conversation_ContextMenuCopy), action: {
|
||||
UIPasteboard.general.string = text
|
||||
})])
|
||||
controller.dismissed = { [weak self] in
|
||||
self?.updateTextSelectionRects([], text: nil)
|
||||
}
|
||||
self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
return (strongSelf.scrollNode, coveringRect.insetBy(dx: -3.0, dy: -3.0))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}))
|
||||
textSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
||||
} else if let textSelectionNode = self.textSelectionNode {
|
||||
self.textSelectionNode = nil
|
||||
textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in
|
||||
textSelectionNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func openUrl(_ url: InstantPageUrlItem) {
|
||||
guard let items = self.currentLayout?.items else {
|
||||
return
|
||||
}
|
||||
|
||||
if let webPage = self.webPage, url.webpageId == webPage.id, let anchorRange = url.url.range(of: "#") {
|
||||
let anchor = url.url.substring(from: anchorRange.upperBound)
|
||||
if !anchor.isEmpty {
|
||||
for item in items {
|
||||
if let item = item as? InstantPageAnchorItem, item.anchor == anchor {
|
||||
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: item.frame.origin.y - self.scrollNode.view.contentInset.top), animated: true)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.resolveUrlDisposable.set((resolveUrl(account: self.account, url: url.url) |> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
switch result {
|
||||
case let .externalUrl(url):
|
||||
if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext {
|
||||
applicationContext.applicationBindings.openUrl(url)
|
||||
}
|
||||
default:
|
||||
break
|
||||
/*case let .peer(peerId):
|
||||
strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil), fromMessageId: nil)
|
||||
case let .botStart(peerId, payload):
|
||||
strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive)), fromMessageId: nil)
|
||||
case let .groupBotStart(peerId, payload):
|
||||
break
|
||||
case let .channelMessage(peerId, messageId):
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: messageId))*/
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
private func openMedia(_ media: InstantPageMedia) {
|
||||
guard let items = self.currentLayout?.items, let webPage = self.webPage else {
|
||||
return
|
||||
}
|
||||
|
||||
if let file = media.media as? TelegramMediaFile, (file.isVoice || file.isMusic) {
|
||||
var medias: [InstantPageMedia] = []
|
||||
for item in items {
|
||||
for itemMedia in item.medias {
|
||||
if let itemFile = itemMedia.media as? TelegramMediaFile, (itemFile.isVoice || itemFile.isMusic) {
|
||||
medias.append(itemMedia)
|
||||
}
|
||||
}
|
||||
}
|
||||
let player = ManagedAudioPlaylistPlayer(audioSessionManager: self.account.telegramApplicationContext.mediaManager.audioSession, overlayMediaManager: self.account.telegramApplicationContext.mediaManager.overlayMediaManager, mediaManager: self.account.telegramApplicationContext.mediaManager, account: self.account, postbox: self.account.postbox, playlist: instantPageAudioPlaylist(account: self.account, webpage: webPage, medias: medias, at: media))
|
||||
self.account.telegramApplicationContext.mediaManager.setPlaylistPlayer(player)
|
||||
player.control(.navigation(.next))
|
||||
return
|
||||
}
|
||||
|
||||
var medias: [InstantPageMedia] = []
|
||||
for item in items {
|
||||
medias.append(contentsOf: item.medias)
|
||||
}
|
||||
|
||||
medias = medias.filter {
|
||||
$0.media is TelegramMediaImage
|
||||
}
|
||||
|
||||
var entries: [InstantPageGalleryEntry] = []
|
||||
for media in medias {
|
||||
entries.append(InstantPageGalleryEntry(index: Int32(media.index), media: media, caption: media.caption ?? "", location: InstantPageGalleryEntryLocation(position: Int32(entries.count), totalCount: Int32(medias.count))))
|
||||
}
|
||||
|
||||
var centralIndex: Int?
|
||||
for i in 0 ..< entries.count {
|
||||
if entries[i].media == media {
|
||||
centralIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let centralIndex = centralIndex {
|
||||
let controller = InstantPageGalleryController(account: self.account, entries: entries, centralIndex: centralIndex, replaceRootController: { _, _ in
|
||||
})
|
||||
self.hiddenMediaDisposable.set((controller.hiddenMedia |> deliverOnMainQueue).start(next: { [weak self] entry in
|
||||
if let strongSelf = self {
|
||||
for (_, itemNode) in strongSelf.visibleItemsWithViews {
|
||||
if let itemNode = itemNode as? InstantPageNode {
|
||||
itemNode.updateHiddenMedia(media: entry?.media)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
self.present(controller, InstantPageGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in
|
||||
if let strongSelf = self {
|
||||
for (_, itemNode) in strongSelf.visibleItemsWithViews {
|
||||
if let itemNode = itemNode as? InstantPageNode {
|
||||
if let transitionNode = itemNode.transitionNode(media: entry.media) {
|
||||
return GalleryTransitionArguments(transitionNode: transitionNode, transitionContainerNode: strongSelf, transitionBackgroundNode: strongSelf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
private func presentSettings() {
|
||||
guard let settings = self.settings, let containerLayout = self.containerLayout else {
|
||||
return
|
||||
}
|
||||
if self.settingsNode == nil {
|
||||
let settingsNode = InstantPageSettingsNode(strings: self.strings, settings: settings, applySettings: { [weak self] settings in
|
||||
if let strongSelf = self {
|
||||
strongSelf.update(settings: settings, strings: strongSelf.strings)
|
||||
let _ = updateInstantPagePresentationSettingsInteractively(postbox: strongSelf.account.postbox, { _ in
|
||||
return settings
|
||||
}).start()
|
||||
}
|
||||
})
|
||||
self.addSubnode(settingsNode)
|
||||
self.settingsNode = settingsNode
|
||||
|
||||
let settingsDimNode = ASDisplayNode()
|
||||
settingsDimNode.backgroundColor = UIColor(rgb: 0, alpha: 0.1)
|
||||
settingsDimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(settingsDimTapped(_:))))
|
||||
self.insertSubnode(settingsDimNode, belowSubnode: self.navigationBar)
|
||||
self.settingsDimNode = settingsDimNode
|
||||
|
||||
settingsDimNode.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
||||
|
||||
settingsNode.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
||||
settingsNode.updateLayout(layout: containerLayout, transition: .immediate)
|
||||
settingsNode.animateIn()
|
||||
settingsDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
|
||||
self.navigationBar.updateDimmed(true, transition: transition)
|
||||
transition.updateAlpha(node: self.statusBar, alpha: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func settingsDimTapped(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
if let settingsNode = self.settingsNode {
|
||||
self.settingsNode = nil
|
||||
settingsNode.animateOut(completion: { [weak settingsNode] in
|
||||
settingsNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
if let settingsDimNode = self.settingsDimNode {
|
||||
self.settingsDimNode = nil
|
||||
settingsDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak settingsDimNode] _ in
|
||||
settingsDimNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
|
||||
self.navigationBar.updateDimmed(false, transition: transition)
|
||||
transition.updateAlpha(node: self.statusBar, alpha: 1.0)
|
||||
}
|
||||
[_navigationBar setProgress:progress];*/
|
||||
}
|
||||
}
|
||||
|
||||
255
TelegramUI/InstantPageGalleryController.swift
Normal file
@@ -0,0 +1,255 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import QuickLook
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
|
||||
struct InstantPageGalleryEntryLocation: Equatable {
|
||||
let position: Int32
|
||||
let totalCount: Int32
|
||||
|
||||
static func ==(lhs: InstantPageGalleryEntryLocation, rhs: InstantPageGalleryEntryLocation) -> Bool {
|
||||
return lhs.position == rhs.position && lhs.totalCount == rhs.totalCount
|
||||
}
|
||||
}
|
||||
|
||||
struct InstantPageGalleryEntry: Equatable {
|
||||
let index: Int32
|
||||
let media: InstantPageMedia
|
||||
let caption: String
|
||||
let location: InstantPageGalleryEntryLocation
|
||||
|
||||
static func ==(lhs: InstantPageGalleryEntry, rhs: InstantPageGalleryEntry) -> Bool {
|
||||
return lhs.index == rhs.index && lhs.media == rhs.media && lhs.caption == rhs.caption && lhs.location == rhs.location
|
||||
}
|
||||
|
||||
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings) -> GalleryItem {
|
||||
if let image = self.media.media as? TelegramMediaImage {
|
||||
return InstantImageGalleryItem(account: account, theme: theme, strings: strings, image: image, caption: self.caption, location: self.location)
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantPageGalleryControllerPresentationArguments {
|
||||
let transitionArguments: (InstantPageGalleryEntry) -> GalleryTransitionArguments?
|
||||
|
||||
init(transitionArguments: @escaping (InstantPageGalleryEntry) -> GalleryTransitionArguments?) {
|
||||
self.transitionArguments = transitionArguments
|
||||
}
|
||||
}
|
||||
|
||||
class InstantPageGalleryController: ViewController {
|
||||
private var galleryNode: GalleryControllerNode {
|
||||
return self.displayNode as! GalleryControllerNode
|
||||
}
|
||||
|
||||
private let account: Account
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
override var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
private var didSetReady = false
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var entries: [InstantPageGalleryEntry] = []
|
||||
private var centralEntryIndex: Int?
|
||||
|
||||
private let centralItemTitle = Promise<String>()
|
||||
private let centralItemTitleView = Promise<UIView?>()
|
||||
private let centralItemNavigationStyle = Promise<GalleryItemNodeNavigationStyle>()
|
||||
private let centralItemFooterContentNode = Promise<GalleryFooterContentNode?>()
|
||||
private let centralItemAttributesDisposable = DisposableSet();
|
||||
|
||||
private let _hiddenMedia = Promise<InstantPageGalleryEntry?>(nil)
|
||||
var hiddenMedia: Signal<InstantPageGalleryEntry?, NoError> {
|
||||
return self._hiddenMedia.get()
|
||||
}
|
||||
|
||||
private let replaceRootController: (ViewController, ValuePromise<Bool>?) -> Void
|
||||
|
||||
init(account: Account, entries: [InstantPageGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, ValuePromise<Bool>?) -> Void) {
|
||||
self.account = account
|
||||
self.replaceRootController = replaceRootController
|
||||
|
||||
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
|
||||
super.init(navigationBarTheme: GalleryController.darkNavigationTheme)
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
|
||||
let entriesSignal: Signal<[InstantPageGalleryEntry], NoError> = .single(entries)
|
||||
|
||||
self.disposable.set((entriesSignal |> deliverOnMainQueue).start(next: { [weak self] entries in
|
||||
if let strongSelf = self {
|
||||
strongSelf.entries = entries
|
||||
strongSelf.centralEntryIndex = centralIndex
|
||||
if strongSelf.isViewLoaded {
|
||||
strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({
|
||||
$0.item(account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings)
|
||||
}), centralItemIndex: centralIndex, keepFirst: false)
|
||||
|
||||
let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in
|
||||
strongSelf?.didSetReady = true
|
||||
}
|
||||
strongSelf._ready.set(ready |> map { true })
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in
|
||||
self?.navigationItem.title = title
|
||||
}))
|
||||
|
||||
self.centralItemAttributesDisposable.add(self.centralItemTitleView.get().start(next: { [weak self] titleView in
|
||||
self?.navigationItem.titleView = titleView
|
||||
}))
|
||||
|
||||
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in
|
||||
self?.galleryNode.updatePresentationState({
|
||||
$0.withUpdatedFooterContentNode(footerContentNode)
|
||||
}, transition: .immediate)
|
||||
}))
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
self.centralItemAttributesDisposable.dispose()
|
||||
}
|
||||
|
||||
@objc func donePressed() {
|
||||
self.dismiss(forceAway: false)
|
||||
}
|
||||
|
||||
private func dismiss(forceAway: Bool) {
|
||||
var animatedOutNode = true
|
||||
var animatedOutInterface = false
|
||||
|
||||
let completion = { [weak self] in
|
||||
if animatedOutNode && animatedOutInterface {
|
||||
self?._hiddenMedia.set(.single(nil))
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? InstantPageGalleryControllerPresentationArguments {
|
||||
if !self.entries.isEmpty {
|
||||
if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway {
|
||||
animatedOutNode = false
|
||||
centralItemNode.animateOut(to: transitionArguments.transitionNode, completion: {
|
||||
animatedOutNode = true
|
||||
completion()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.galleryNode.animateOut(animateContent: animatedOutNode, completion: {
|
||||
animatedOutInterface = true
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(controller, in: .window(.root), with: arguments)
|
||||
}
|
||||
}, dismissController: { [weak self] in
|
||||
self?.dismiss(forceAway: true)
|
||||
}, replaceRootController: { [weak self] controller, ready in
|
||||
if let strongSelf = self {
|
||||
strongSelf.replaceRootController(controller, ready)
|
||||
}
|
||||
})
|
||||
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction)
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.galleryNode.statusBar = self.statusBar
|
||||
self.galleryNode.navigationBar = self.navigationBar
|
||||
|
||||
self.galleryNode.transitionNodeForCentralItem = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? InstantPageGalleryControllerPresentationArguments {
|
||||
if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) {
|
||||
return transitionArguments.transitionNode
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
self.galleryNode.dismiss = { [weak self] in
|
||||
self?._hiddenMedia.set(.single(nil))
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
|
||||
self.galleryNode.pager.replaceItems(self.entries.map({
|
||||
$0.item(account: account, theme: self.presentationData.theme, strings: self.presentationData.strings)
|
||||
}), centralItemIndex: self.centralEntryIndex)
|
||||
|
||||
self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in
|
||||
if let strongSelf = self {
|
||||
var hiddenItem: InstantPageGalleryEntry?
|
||||
if let index = index {
|
||||
hiddenItem = strongSelf.entries[index]
|
||||
|
||||
if let node = strongSelf.galleryNode.pager.centralItemNode() {
|
||||
strongSelf.centralItemTitle.set(node.title())
|
||||
strongSelf.centralItemTitleView.set(node.titleView())
|
||||
strongSelf.centralItemNavigationStyle.set(node.navigationStyle())
|
||||
strongSelf.centralItemFooterContentNode.set(node.footerContent())
|
||||
}
|
||||
}
|
||||
if strongSelf.didSetReady {
|
||||
strongSelf._hiddenMedia.set(.single(hiddenItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in
|
||||
self?.didSetReady = true
|
||||
}
|
||||
self._ready.set(ready |> map { true })
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
var nodeAnimatesItself = false
|
||||
|
||||
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? InstantPageGalleryControllerPresentationArguments {
|
||||
self.centralItemTitle.set(centralItemNode.title())
|
||||
self.centralItemTitleView.set(centralItemNode.titleView())
|
||||
self.centralItemNavigationStyle.set(centralItemNode.navigationStyle())
|
||||
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
|
||||
|
||||
if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]) {
|
||||
nodeAnimatesItself = true
|
||||
centralItemNode.animateIn(from: transitionArguments.transitionNode)
|
||||
|
||||
self._hiddenMedia.set(.single(self.entries[centralItemNode.index]))
|
||||
}
|
||||
}
|
||||
|
||||
self.galleryNode.animateIn(animateContent: !nodeAnimatesItself)
|
||||
}
|
||||
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
|
||||
}
|
||||
}
|
||||
76
TelegramUI/InstantPageGalleryFooterContentNode.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Photos
|
||||
|
||||
private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionAction"), color: .white)
|
||||
|
||||
private let textFont = Font.regular(16.0)
|
||||
|
||||
final class InstantPageGalleryFooterContentNode: GalleryFooterContentNode {
|
||||
private let account: Account
|
||||
private var theme: PresentationTheme
|
||||
private var strings: PresentationStrings
|
||||
|
||||
private let actionButton: UIButton
|
||||
private let textNode: ASTextNode
|
||||
|
||||
private var currentMessageText: String?
|
||||
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.account = account
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
self.actionButton = UIButton()
|
||||
|
||||
self.actionButton.setImage(actionImage, for: [.normal])
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.view.addSubview(self.actionButton)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), for: [.touchUpInside])
|
||||
}
|
||||
|
||||
func setCaption(_ caption: String) {
|
||||
if self.currentMessageText != caption {
|
||||
self.currentMessageText = caption
|
||||
|
||||
if caption.isEmpty {
|
||||
self.textNode.isHidden = true
|
||||
self.textNode.attributedText = nil
|
||||
} else {
|
||||
self.textNode.isHidden = false
|
||||
self.textNode.attributedText = NSAttributedString(string: caption, font: textFont, textColor: .white)
|
||||
}
|
||||
|
||||
self.requestLayout?(.immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
var panelHeight: CGFloat = 44.0
|
||||
if !self.textNode.isHidden {
|
||||
let sideInset: CGFloat = 8.0
|
||||
let topInset: CGFloat = 8.0
|
||||
let bottomInset: CGFloat = 8.0
|
||||
let textSize = self.textNode.measure(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
panelHeight += textSize.height + topInset + bottomInset
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: sideInset, y: topInset), size: textSize))
|
||||
}
|
||||
|
||||
self.actionButton.frame = CGRect(origin: CGPoint(x: 0.0, y: panelHeight - 44.0), size: CGSize(width: 44.0, height: 44.0))
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
|
||||
@objc func actionButtonPressed() {
|
||||
}
|
||||
}
|
||||
61
TelegramUI/InstantPageImageItem.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
final class InstantPageImageItem: InstantPageItem {
|
||||
var frame: CGRect
|
||||
|
||||
let media: InstantPageMedia
|
||||
var medias: [InstantPageMedia] {
|
||||
return [self.media]
|
||||
}
|
||||
|
||||
let interactive: Bool
|
||||
let roundCorners: Bool
|
||||
let fit: Bool
|
||||
|
||||
let wantsNode: Bool = true
|
||||
|
||||
init(frame: CGRect, media: InstantPageMedia, interactive: Bool, roundCorners: Bool, fit: Bool) {
|
||||
self.frame = frame
|
||||
self.media = media
|
||||
self.interactive = interactive
|
||||
self.roundCorners = roundCorners
|
||||
self.fit = fit
|
||||
}
|
||||
|
||||
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? {
|
||||
return InstantPageImageNode(account: account, media: self.media, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia)
|
||||
}
|
||||
|
||||
func matchesAnchor(_ anchor: String) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func matchesNode(_ node: InstantPageNode) -> Bool {
|
||||
if let node = node as? InstantPageImageNode {
|
||||
return node.media == self.media
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func distanceThresholdGroup() -> Int? {
|
||||
return 1
|
||||
}
|
||||
|
||||
func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat {
|
||||
if count > 3 {
|
||||
return 400.0
|
||||
} else {
|
||||
return CGFloat.greatestFiniteMagnitude
|
||||
}
|
||||
}
|
||||
|
||||
func drawInTile(context: CGContext) {
|
||||
}
|
||||
|
||||
func linkSelectionRects(at point: CGPoint) -> [CGRect] {
|
||||
return []
|
||||
}
|
||||
}
|
||||
102
TelegramUI/InstantPageImageNode.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
private let account: Account
|
||||
let media: InstantPageMedia
|
||||
private let interactive: Bool
|
||||
private let roundCorners: Bool
|
||||
private let fit: Bool
|
||||
private let openMedia: (InstantPageMedia) -> Void
|
||||
|
||||
private let imageNode: TransformImageNode
|
||||
|
||||
private var currentSize: CGSize?
|
||||
|
||||
private var fetchedDisposable = MetaDisposable()
|
||||
|
||||
init(account: Account, media: InstantPageMedia, interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void) {
|
||||
self.account = account
|
||||
self.media = media
|
||||
self.interactive = interactive
|
||||
self.roundCorners = roundCorners
|
||||
self.fit = fit
|
||||
self.openMedia = openMedia
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.imageNode.alphaTransitionOnFirstUpdate = true
|
||||
self.addSubnode(self.imageNode)
|
||||
|
||||
if let image = media.media as? TelegramMediaImage {
|
||||
self.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image))
|
||||
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start())
|
||||
} else if let file = media.media as? TelegramMediaFile {
|
||||
self.imageNode.setSignal(account: account, signal: chatMessageVideo(account: account, video: file))
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
if self.interactive {
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
}
|
||||
|
||||
func updateIsVisible(_ isVisible: Bool) {
|
||||
}
|
||||
|
||||
func update(strings: PresentationStrings, theme: InstantPageTheme) {
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
if self.currentSize != size {
|
||||
self.currentSize = size
|
||||
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
if let image = self.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
||||
let imageSize = largest.dimensions.aspectFilled(size)
|
||||
let boundingSize = size
|
||||
var radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
|
||||
|
||||
let makeLayout = self.imageNode.asyncLayout()
|
||||
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))
|
||||
apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func transitionNode(media: InstantPageMedia) -> ASDisplayNode? {
|
||||
if media == self.media {
|
||||
return self.imageNode
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateHiddenMedia(media: InstantPageMedia?) {
|
||||
self.imageNode.isHidden = self.media == media
|
||||
}
|
||||
|
||||
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.openMedia(self.media)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
protocol InstantPageItem {
|
||||
var frame: CGRect { get set }
|
||||
var hasLinks: Bool { get }
|
||||
var wantsNode: Bool { get }
|
||||
var medias: [InstantPageMedia] { get }
|
||||
|
||||
func matchesAnchor(_ anchor: String) -> Bool
|
||||
func drawInTile(context: CGContext)
|
||||
func node(account: Account) -> InstantPageNode?
|
||||
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode?
|
||||
func matchesNode(_ node: InstantPageNode) -> Bool
|
||||
func linkSelectionViews() -> [InstantPageLinkSelectionView]
|
||||
func linkSelectionRects(at point: CGPoint) -> [CGRect]
|
||||
|
||||
func distanceThresholdGroup() -> Int?
|
||||
func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat
|
||||
|
||||
@@ -23,28 +23,41 @@ final class InstantPageLayout {
|
||||
}
|
||||
}
|
||||
|
||||
func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToWidthAndHeight: Bool, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, theme: InstantPageTheme) -> InstantPageLayout {
|
||||
private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, category: InstantPageTextCategoryType, link: Bool) {
|
||||
let attributes = theme.textCategories.attributes(type: category, link: link)
|
||||
stack.push(.textColor(attributes.color))
|
||||
switch attributes.font.style {
|
||||
case .sans:
|
||||
stack.push(.fontSerif(false))
|
||||
case .serif:
|
||||
stack.push(.fontSerif(true))
|
||||
}
|
||||
stack.push(.fontSize(attributes.font.size))
|
||||
stack.push(.lineSpacingFactor(attributes.font.lineSpacingFactor))
|
||||
if attributes.underline {
|
||||
stack.push(.underline)
|
||||
}
|
||||
}
|
||||
|
||||
func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToWidthAndHeight: Bool, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, theme: InstantPageTheme) -> InstantPageLayout {
|
||||
switch block {
|
||||
case let .cover(block):
|
||||
return layoutInstantPageBlock(block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, isCover: true, previousItems:previousItems, fillToWidthAndHeight: fillToWidthAndHeight, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme)
|
||||
return layoutInstantPageBlock(webpage: webpage, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, isCover: true, previousItems:previousItems, fillToWidthAndHeight: fillToWidthAndHeight, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme)
|
||||
case let .title(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(28.0))
|
||||
styleStack.push(.fontSerif(true))
|
||||
styleStack.push(.lineSpacingFactor(0.685))
|
||||
setupStyleStack(styleStack, theme: theme, category: .header, link: false)
|
||||
let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item])
|
||||
case let .subtitle(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(17.0))
|
||||
setupStyleStack(styleStack, theme: theme, category: .subheader, link: false)
|
||||
let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item])
|
||||
case let .authorDate(author: author, date: date):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(15.0))
|
||||
styleStack.push(.textColor(UIColor(rgb: 0x79828b)))
|
||||
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
|
||||
var text: RichText?
|
||||
if case .empty = author {
|
||||
if date != 0 {
|
||||
@@ -91,31 +104,25 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h
|
||||
}
|
||||
case let .header(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(24.0))
|
||||
styleStack.push(.fontSerif(true))
|
||||
styleStack.push(.lineSpacingFactor(0.685))
|
||||
setupStyleStack(styleStack, theme: theme, category: .header, link: false)
|
||||
let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item])
|
||||
case let .subheader(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(19.0))
|
||||
styleStack.push(.fontSerif(true))
|
||||
styleStack.push(.lineSpacingFactor(0.685))
|
||||
setupStyleStack(styleStack, theme: theme, category: .subheader, link: false)
|
||||
let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item])
|
||||
case let .paragraph(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(17.0))
|
||||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item])
|
||||
case let .preformatted(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(16.0))
|
||||
styleStack.push(.fontFixed(true))
|
||||
styleStack.push(.lineSpacingFactor(0.685))
|
||||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
let backgroundInset: CGFloat = 14.0
|
||||
let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0)
|
||||
item.frame = item.frame.offsetBy(dx: horizontalInset, dy: backgroundInset)
|
||||
@@ -129,7 +136,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item])
|
||||
case .divider:
|
||||
let lineWidth = floor(boundingWidth / 2.0)
|
||||
let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - lineWidth) / 2.0), y: 0.0), size: CGSize(width: lineWidth, height: 1.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: 1.0)), shape: .rect, color: UIColor(rgb: 0x79828b))
|
||||
let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - lineWidth) / 2.0), y: 0.0), size: CGSize(width: lineWidth, height: 1.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: 1.0)), shape: .rect, color: theme.textCategories.caption.color)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: shapeItem.frame.size, items: [shapeItem])
|
||||
case let .list(contentItems, ordered):
|
||||
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
||||
@@ -139,14 +146,14 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h
|
||||
for i in 0 ..< contentItems.count {
|
||||
if ordered {
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(17.0))
|
||||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
let textItem = layoutTextItemWithString(attributedStringForRichText(.plain("\(i + 1)."), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
if let line = textItem.lines.first {
|
||||
maxIndexWidth = max(maxIndexWidth, line.frame.size.width)
|
||||
}
|
||||
indexItems.append(textItem)
|
||||
} else {
|
||||
let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 6.0, height: 12.0)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: 6.0, height: 6.0)), shape: .ellipse, color: UIColor.black)
|
||||
let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 6.0, height: 12.0)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: 6.0, height: 6.0)), shape: .ellipse, color: theme.textCategories.paragraph.color)
|
||||
indexItems.append(shapeItem)
|
||||
}
|
||||
}
|
||||
@@ -156,7 +163,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h
|
||||
contentSize.height += 20.0
|
||||
}
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(17.0))
|
||||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
|
||||
let textItem = layoutTextItemWithString(attributedStringForRichText(contentItems[i], styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth)
|
||||
textItem.frame = textItem.frame.offsetBy(dx: horizontalInset + indexSpacing + maxIndexWidth, dy: contentSize.height)
|
||||
@@ -175,8 +182,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(17.0))
|
||||
styleStack.push(.fontSerif(true))
|
||||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
styleStack.push(.italic)
|
||||
|
||||
let textItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset)
|
||||
@@ -190,7 +196,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h
|
||||
contentSize.height += 14.0
|
||||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(15.0))
|
||||
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
|
||||
|
||||
let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset)
|
||||
captionItem.frame = captionItem.frame.offsetBy(dx: horizontalInset + lineInset, dy: contentSize.height)
|
||||
@@ -212,8 +218,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(17.0))
|
||||
styleStack.push(.fontSerif(true))
|
||||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
styleStack.push(.italic)
|
||||
|
||||
let textItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
@@ -228,7 +233,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h
|
||||
contentSize.height += 14.0
|
||||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(15.0))
|
||||
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
|
||||
|
||||
let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height)
|
||||
@@ -259,7 +264,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h
|
||||
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
let mediaItem = InstantPageMediaItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: image, caption: caption.plainText), arguments: InstantPageMediaArguments.image(interactive: true, roundCorners: false, fit: false))
|
||||
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: image, caption: caption.plainText), interactive: true, roundCorners: false, fit: false)
|
||||
|
||||
items.append(mediaItem)
|
||||
contentSize.height += filledSize.height
|
||||
@@ -269,7 +274,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h
|
||||
contentSize.height += 10.0
|
||||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
styleStack.push(.fontSize(15.0))
|
||||
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
|
||||
|
||||
let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height)
|
||||
@@ -283,6 +288,206 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h
|
||||
} else {
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: [])
|
||||
}
|
||||
case let .video(id, caption, autoplay, loop):
|
||||
if let file = media[id] as? TelegramMediaFile, let dimensions = file.dimensions {
|
||||
let imageSize = dimensions
|
||||
var filledSize = imageSize.aspectFitted(CGSize(width: boundingWidth, height: 1200.0))
|
||||
|
||||
if fillToWidthAndHeight {
|
||||
filledSize = CGSize(width: boundingWidth, height: boundingWidth)
|
||||
} else if isCover {
|
||||
filledSize = imageSize.aspectFilled(CGSize(width: boundingWidth, height: 1.0))
|
||||
if !filledSize.height.isZero {
|
||||
filledSize = filledSize.cropped(CGSize(width: boundingWidth, height: floor(boundingWidth * 3.0 / 5.0)))
|
||||
}
|
||||
}
|
||||
|
||||
let mediaIndex = mediaIndexCounter
|
||||
mediaIndexCounter += 1
|
||||
|
||||
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
if autoplay {
|
||||
let mediaItem = InstantPagePlayableVideoItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: file, caption: caption.plainText), interactive: true)
|
||||
|
||||
items.append(mediaItem)
|
||||
} else {
|
||||
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: file, caption: caption.plainText), interactive: true, roundCorners: false, fit: false)
|
||||
|
||||
items.append(mediaItem)
|
||||
}
|
||||
contentSize.height += filledSize.height
|
||||
|
||||
if case .empty = caption {
|
||||
} else {
|
||||
contentSize.height += 10.0
|
||||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
|
||||
|
||||
let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height)
|
||||
captionItem.alignment = .center
|
||||
|
||||
contentSize.height += captionItem.frame.size.height
|
||||
items.append(captionItem)
|
||||
}
|
||||
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
} else {
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: [])
|
||||
}
|
||||
case let .collage(items: innerItems, caption: caption):
|
||||
let spacing: CGFloat = 2.0
|
||||
let itemsPerRow = 3
|
||||
let itemSize = floor((boundingWidth - spacing * max(0.0, CGFloat(itemsPerRow - 1))) / CGFloat(itemsPerRow))
|
||||
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
var nextItemOrigin = CGPoint(x: 0.0, y: 0.0)
|
||||
for subItem in innerItems {
|
||||
if nextItemOrigin.x + itemSize > boundingWidth {
|
||||
nextItemOrigin.x = 0.0
|
||||
nextItemOrigin.y += itemSize + spacing
|
||||
}
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, block: subItem, boundingWidth: itemSize, horizontalInset: 0.0, isCover: false, previousItems: items, fillToWidthAndHeight: true, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme)
|
||||
items.append(contentsOf: subLayout.flattenedItemsWithOrigin(nextItemOrigin))
|
||||
nextItemOrigin.x += itemSize + spacing
|
||||
}
|
||||
|
||||
var contentSize = CGSize(width: boundingWidth, height: nextItemOrigin.y + itemSize)
|
||||
|
||||
if case .empty = caption {
|
||||
} else {
|
||||
contentSize.height += 10.0
|
||||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
|
||||
|
||||
let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height)
|
||||
captionItem.alignment = .center
|
||||
|
||||
contentSize.height += captionItem.frame.size.height
|
||||
items.append(captionItem)
|
||||
}
|
||||
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .postEmbed(url, webpageId, avatarId, author, date, blocks, caption):
|
||||
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
||||
let lineInset: CGFloat = 20.0
|
||||
let verticalInset: CGFloat = 4.0
|
||||
let itemSpacing: CGFloat = 10.0
|
||||
var avatarInset: CGFloat = 0.0
|
||||
var avatarVerticalInset: CGFloat = 0.0
|
||||
|
||||
contentSize.height += verticalInset
|
||||
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
if !author.isEmpty {
|
||||
let avatar: TelegramMediaImage? = avatarId.flatMap { media[$0] as? TelegramMediaImage }
|
||||
if let avatar = avatar {
|
||||
let avatarItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: horizontalInset + lineInset + 1.0, y: contentSize.height - 2.0), size: CGSize(width: 50.0, height: 50.0)), media: InstantPageMedia(index: -1, media: avatar, caption: ""), interactive: false, roundCorners: true, fit: false)
|
||||
items.append(avatarItem)
|
||||
|
||||
avatarInset += 62.0
|
||||
avatarVerticalInset += 6.0
|
||||
if date == 0 {
|
||||
avatarVerticalInset += 11.0
|
||||
}
|
||||
}
|
||||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
styleStack.push(.bold)
|
||||
|
||||
let textItem = layoutTextItemWithString(attributedStringForRichText(.plain(author), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset - avatarInset)
|
||||
textItem.frame = textItem.frame.offsetBy(dx: horizontalInset + lineInset + avatarInset, dy: contentSize.height + avatarVerticalInset)
|
||||
items.append(textItem)
|
||||
|
||||
contentSize.height += textItem.frame.size.height + avatarVerticalInset
|
||||
}
|
||||
if date != 0 {
|
||||
if items.count != 0 {
|
||||
contentSize.height += itemSpacing
|
||||
}
|
||||
|
||||
let dateString = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(date)), dateStyle: .long, timeStyle: .none)
|
||||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
|
||||
|
||||
let textItem = layoutTextItemWithString(attributedStringForRichText(.plain(dateString), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset - avatarInset)
|
||||
textItem.frame = textItem.frame.offsetBy(dx: horizontalInset + lineInset + avatarInset, dy: contentSize.height)
|
||||
items.append(textItem)
|
||||
|
||||
contentSize.height += textItem.frame.size.height
|
||||
}
|
||||
|
||||
if items.count != 0 {
|
||||
contentSize.height += itemSpacing
|
||||
}
|
||||
|
||||
var previousBlock: InstantPageBlock?
|
||||
for subBlock in blocks {
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme)
|
||||
|
||||
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock)
|
||||
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + lineInset, y: contentSize.height + spacing))
|
||||
items.append(contentsOf: blockItems)
|
||||
contentSize.height += subLayout.contentSize.height + spacing
|
||||
previousBlock = subBlock
|
||||
}
|
||||
|
||||
contentSize.height += verticalInset
|
||||
|
||||
items.append(InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: horizontalInset, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shape: .roundLine, color: .black))
|
||||
|
||||
if case .empty = caption {
|
||||
} else {
|
||||
contentSize.height += 14.0
|
||||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
|
||||
|
||||
let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height)
|
||||
captionItem.alignment = .center
|
||||
|
||||
contentSize.height += captionItem.frame.size.height
|
||||
items.append(captionItem)
|
||||
}
|
||||
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .slideshow(items: subItems, caption: caption):
|
||||
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
var itemMedias: [InstantPageMedia] = []
|
||||
|
||||
for subBlock in subItems {
|
||||
switch subBlock {
|
||||
case let .image(id, caption):
|
||||
if let image = media[id] as? TelegramMediaImage, let imageSize = largestImageRepresentation(image.representations)?.dimensions {
|
||||
let mediaIndex = mediaIndexCounter
|
||||
mediaIndexCounter += 1
|
||||
|
||||
let filledSize = imageSize.fitted(CGSize(width: boundingWidth, height: 1200.0))
|
||||
contentSize.height = max(contentSize.height, filledSize.height)
|
||||
|
||||
itemMedias.append(InstantPageMedia(index: mediaIndex, media: image, caption: ""))
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
items.append(InstantPageSlideshowItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height)), medias: itemMedias))
|
||||
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .webEmbed(url, html, dimensions, caption, stretchToWidth, allowScrolling, coverId):
|
||||
var embedBoundingWidth = boundingWidth - horizontalInset * 2.0
|
||||
if stretchToWidth {
|
||||
@@ -294,14 +499,83 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h
|
||||
} else {
|
||||
size = dimensions.aspectFitted(CGSize(width: embedBoundingWidth, height: embedBoundingWidth))
|
||||
}
|
||||
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
let item = InstantPageWebEmbedItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size), url: url, html: html, enableScrolling: allowScrolling)
|
||||
items.append(item)
|
||||
|
||||
var contentSize = item.frame.size
|
||||
|
||||
if case .empty = caption {
|
||||
} else {
|
||||
contentSize.height += 10.0
|
||||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
|
||||
|
||||
let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height)
|
||||
captionItem.alignment = .center
|
||||
|
||||
contentSize.height += captionItem.frame.size.height
|
||||
items.append(captionItem)
|
||||
}
|
||||
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .channelBanner(peer):
|
||||
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
var rtl = false
|
||||
if let previousItem = previousItems.last as? InstantPageTextItem, previousItem.containsRTL {
|
||||
rtl = true
|
||||
}
|
||||
|
||||
if let peer = peer {
|
||||
let item = InstantPagePeerReferenceItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: 40.0)), initialPeer: peer, rtl: rtl)
|
||||
items.append(item)
|
||||
contentSize.height += 40.0
|
||||
}
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .anchor(name):
|
||||
let item = InstantPageAnchorItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: 0.0)), anchor: name)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item])
|
||||
case let .audio(id: audioId, caption: caption):
|
||||
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
if let file = media[audioId] as? TelegramMediaFile {
|
||||
let mediaIndex = mediaIndexCounter
|
||||
mediaIndexCounter += 1
|
||||
let item = InstantPageAudioItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: 48.0)), media: InstantPageMedia(index: mediaIndex, media: file, caption: ""), webpage: webpage)
|
||||
|
||||
contentSize.height += item.frame.size.height
|
||||
items.append(item)
|
||||
|
||||
if case .empty = caption {
|
||||
} else {
|
||||
contentSize.height += 10.0
|
||||
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
|
||||
|
||||
let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0)
|
||||
captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height)
|
||||
captionItem.alignment = .center
|
||||
|
||||
contentSize.height += captionItem.frame.size.height
|
||||
items.append(captionItem)
|
||||
}
|
||||
}
|
||||
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
default:
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: [])
|
||||
}
|
||||
}
|
||||
|
||||
func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: CGFloat) -> InstantPageLayout {
|
||||
func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: CGFloat, strings: PresentationStrings, theme: InstantPageTheme) -> InstantPageLayout {
|
||||
var maybeLoadedContent: TelegramMediaWebpageLoadedContent?
|
||||
if case let .Loaded(content) = webPage.content {
|
||||
maybeLoadedContent = content
|
||||
@@ -322,11 +596,10 @@ func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth:
|
||||
|
||||
var mediaIndexCounter: Int = 0
|
||||
var embedIndexCounter: Int = 0
|
||||
let theme = InstantPageTheme()
|
||||
|
||||
var previousBlock: InstantPageBlock?
|
||||
for block in pageBlocks {
|
||||
let blockLayout = layoutInstantPageBlock(block, boundingWidth: boundingWidth, horizontalInset: 17.0, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme)
|
||||
let blockLayout = layoutInstantPageBlock(webpage: webPage, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme)
|
||||
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block)
|
||||
let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing))
|
||||
items.append(contentsOf: blockItems)
|
||||
|
||||
@@ -4,7 +4,7 @@ import TelegramCore
|
||||
func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> CGFloat {
|
||||
if let upper = upper, let lower = lower {
|
||||
switch (upper, lower) {
|
||||
case (_, .cover):
|
||||
case (_, .cover), (_, .channelBanner):
|
||||
return 0.0
|
||||
case (.divider, _), (_, .divider):
|
||||
return 25.0
|
||||
|
||||
27
TelegramUI/InstantPageManagedMediaId.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
|
||||
struct InstantPageManagedMediaId: ManagedMediaId {
|
||||
let media: InstantPageMedia
|
||||
|
||||
init(media: InstantPageMedia) {
|
||||
self.media = media
|
||||
}
|
||||
|
||||
var hashValue: Int {
|
||||
if let id = self.media.media.id {
|
||||
return id.hashValue
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func isEqual(to: ManagedMediaId) -> Bool {
|
||||
if let to = to as? InstantPageManagedMediaId {
|
||||
return self.media == to.media
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
133
TelegramUI/InstantPageMediaAudioPlaylist.swift
Normal file
@@ -0,0 +1,133 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
struct InstantPageAudioPlaylistItemId: AudioPlaylistItemId {
|
||||
let index: Int
|
||||
let id: MediaId
|
||||
|
||||
var hashValue: Int {
|
||||
return self.id.hashValue &+ self.index.hashValue
|
||||
}
|
||||
|
||||
func isEqual(to: AudioPlaylistItemId) -> Bool {
|
||||
if let other = to as? InstantPageAudioPlaylistItemId {
|
||||
return self.index == other.index && self.id == other.id
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantPageAudioPlaylistItem: AudioPlaylistItem {
|
||||
let media: InstantPageMedia
|
||||
|
||||
var id: AudioPlaylistItemId {
|
||||
return InstantPageAudioPlaylistItemId(index: self.media.index, id: self.media.media.id!)
|
||||
}
|
||||
|
||||
var resource: MediaResource? {
|
||||
if let file = self.media.media as? TelegramMediaFile {
|
||||
return file.resource
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var streamable: Bool {
|
||||
if let file = self.media.media as? TelegramMediaFile {
|
||||
if file.isMusic {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var info: AudioPlaylistItemInfo? {
|
||||
if let file = self.media.media as? TelegramMediaFile {
|
||||
for attribute in file.attributes {
|
||||
switch attribute {
|
||||
case let .Audio(isVoice, duration, title, performer, _):
|
||||
if isVoice {
|
||||
return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .voice)
|
||||
} else {
|
||||
return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .music(title: title, performer: performer))
|
||||
}
|
||||
case let .Video(duration, _, flags):
|
||||
if flags.contains(.instantRoundVideo) {
|
||||
return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .video)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
init(media: InstantPageMedia) {
|
||||
self.media = media
|
||||
}
|
||||
|
||||
func isEqual(to: AudioPlaylistItem) -> Bool {
|
||||
if let other = to as? InstantPageAudioPlaylistItem {
|
||||
return self.media == other.media
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InstantPageAudioPlaylistId: AudioPlaylistId {
|
||||
let webpageId: MediaId
|
||||
|
||||
func isEqual(to: AudioPlaylistId) -> Bool {
|
||||
if let other = to as? InstantPageAudioPlaylistId {
|
||||
if self.webpageId != other.webpageId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func instantPageAudioPlaylistAndItemIds(webpage: TelegramMediaWebpage, media: InstantPageMedia) -> (AudioPlaylistId, AudioPlaylistItemId)? {
|
||||
return (InstantPageAudioPlaylistId(webpageId: webpage.webpageId), InstantPageAudioPlaylistItemId(index: media.index, id: media.media.id!))
|
||||
}
|
||||
|
||||
func instantPageAudioPlaylist(account: Account, webpage: TelegramMediaWebpage, medias: [InstantPageMedia], at centralMedia: InstantPageMedia) -> AudioPlaylist {
|
||||
return AudioPlaylist(id: InstantPageAudioPlaylistId(webpageId: webpage.webpageId), navigate: { item, navigation in
|
||||
if let item = item as? InstantPageAudioPlaylistItem {
|
||||
if let index = medias.index(of: item.media) {
|
||||
switch navigation {
|
||||
case .previous:
|
||||
if index == 0 {
|
||||
return .single(item)
|
||||
} else {
|
||||
return .single(InstantPageAudioPlaylistItem(media: medias[index - 1]))
|
||||
}
|
||||
case .next:
|
||||
if index == medias.count - 1 {
|
||||
return .single(nil)
|
||||
} else {
|
||||
return .single(InstantPageAudioPlaylistItem(media: medias[index + 1]))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
} else {
|
||||
if let index = medias.index(of: centralMedia) {
|
||||
return .single(InstantPageAudioPlaylistItem(media: medias[index]))
|
||||
} else if let media = medias.first {
|
||||
return .single(InstantPageAudioPlaylistItem(media: media))
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
final class InstantPageMediaNode: ASDisplayNode, InstantPageNode {
|
||||
private let account: Account
|
||||
let media: InstantPageMedia
|
||||
private let arguments: InstantPageMediaArguments
|
||||
|
||||
private let imageNode: TransformImageNode
|
||||
|
||||
private var currentSize: CGSize?
|
||||
|
||||
private var fetchedDisposable = MetaDisposable()
|
||||
|
||||
init(account: Account, media: InstantPageMedia, arguments: InstantPageMediaArguments) {
|
||||
self.account = account
|
||||
self.media = media
|
||||
self.arguments = arguments
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.imageNode.alphaTransitionOnFirstUpdate = true
|
||||
self.addSubnode(self.imageNode)
|
||||
|
||||
if let image = media.media as? TelegramMediaImage {
|
||||
self.imageNode.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image))
|
||||
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start())
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
func updateIsVisible(_ isVisible: Bool) {
|
||||
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
if self.currentSize != size {
|
||||
self.currentSize = size
|
||||
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
if let image = self.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
||||
let imageSize = largest.dimensions.aspectFilled(size)
|
||||
let boundingSize = size
|
||||
var radius: CGFloat = 0.0
|
||||
|
||||
switch arguments {
|
||||
case let .image(_, roundCorners, fit):
|
||||
radius = roundCorners ? floor(min(size.width, size.height) / 2.0) : 0.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
let makeLayout = self.imageNode.asyncLayout()
|
||||
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))
|
||||
apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGSize size = self.bounds.size;
|
||||
_button.frame = self.bounds;
|
||||
CGSize overlaySize = _overlayView.bounds.size;
|
||||
_overlayView.frame = CGRectMake(CGFloor((size.width - overlaySize.width) / 2.0f), CGFloor((size.height - overlaySize.height) / 2.0f), overlaySize.width, overlaySize.height);
|
||||
_imageView.frame = self.bounds;
|
||||
|
||||
_videoView.frame = self.bounds;
|
||||
|
||||
if (!CGSizeEqualToSize(_currentSize, size)) {
|
||||
_currentSize = size;
|
||||
|
||||
if ([_media.media isKindOfClass:[TGImageMediaAttachment class]]) {
|
||||
TGImageMediaAttachment *image = _media.media;
|
||||
CGSize imageSize = TGFillSize([image dimensions], size);
|
||||
CGSize boundingSize = size;
|
||||
|
||||
CGFloat radius = 0.0f;
|
||||
if ([_arguments isKindOfClass:[TGInstantPageImageMediaArguments class]]) {
|
||||
TGInstantPageImageMediaArguments *imageArguments = (TGInstantPageImageMediaArguments *)_arguments;
|
||||
if (imageArguments.fit) {
|
||||
_imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
imageSize = TGFitSize([image dimensions], size);
|
||||
boundingSize = imageSize;
|
||||
}
|
||||
radius = imageArguments.roundCorners ? CGFloor(MIN(size.width, size.height) / 2.0f) : 0.0f;
|
||||
}
|
||||
[_imageView setArguments:[[TransformImageArguments alloc] initWithImageSize:imageSize boundingSize:boundingSize cornerRadius:radius]];
|
||||
} else if ([_media.media isKindOfClass:[TGVideoMediaAttachment class]]) {
|
||||
TGVideoMediaAttachment *video = _media.media;
|
||||
CGSize imageSize = TGFillSize([video dimensions], size);
|
||||
[_imageView setArguments:[[TransformImageArguments alloc] initWithImageSize:imageSize boundingSize:size cornerRadius:0.0f]];
|
||||
}
|
||||
}
|
||||
}*/
|
||||
@@ -2,41 +2,54 @@ import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
|
||||
private let backArrowImage = UIImage(bundleImageName: "Instant View/BackArrow")?.precomposed()
|
||||
private let settingsImage = UIImage(bundleImageName: "Instant View/SettingsIcon")?.precomposed()
|
||||
private let backArrowImage = NavigationBarTheme.generateBackArrowImage(color: .white)
|
||||
private let moreImage = generateTintedImage(image: UIImage(bundleImageName: "Instant View/MoreIcon"), color: .white)
|
||||
private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Instant View/ActionIcon"), color: .white)
|
||||
|
||||
final class InstantPageNavigationBar: ASDisplayNode {
|
||||
private var strings: PresentationStrings
|
||||
|
||||
private var pageProgress: CGFloat = 0.0
|
||||
private let pageProgressNode: ASDisplayNode
|
||||
private let backButton: HighlightableButtonNode
|
||||
private let moreButton: HighlightableButtonNode
|
||||
private let actionButton: HighlightableButtonNode
|
||||
private let scrollToTopButton: HighlightableButtonNode
|
||||
private let arrowNode: ASImageNode
|
||||
|
||||
let pageProgressNode: ASDisplayNode
|
||||
let backButton: HighlightableButtonNode
|
||||
let shareButton: HighlightableButtonNode
|
||||
let settingsButton: HighlightableButtonNode
|
||||
let scrollToTopButton: HighlightableButtonNode
|
||||
let arrowNode: ASImageNode
|
||||
let shareLabel: ASTextNode
|
||||
var shareLabelSize: CGSize
|
||||
var shareLabelSmallSize: CGSize
|
||||
private let intrinsicMoreSize: CGSize
|
||||
private let intrinsicSmallMoreSize: CGSize
|
||||
private let intrinsicActionSize: CGSize
|
||||
private let intrinsicSmallActionSize: CGSize
|
||||
|
||||
private var dimmed: Bool = false
|
||||
private var buttonsAlphaFactor: CGFloat = 1.0
|
||||
|
||||
var back: (() -> Void)?
|
||||
var share: (() -> Void)?
|
||||
var settings: (() -> Void)?
|
||||
var scrollToTop: (() -> Void)?
|
||||
|
||||
init(strings: PresentationStrings) {
|
||||
self.strings = strings
|
||||
|
||||
self.pageProgressNode = ASDisplayNode()
|
||||
self.pageProgressNode.isLayerBacked = true
|
||||
self.pageProgressNode.backgroundColor = UIColor(rgb: 0x242425)
|
||||
|
||||
self.backButton = HighlightableButtonNode()
|
||||
self.shareButton = HighlightableButtonNode()
|
||||
self.settingsButton = HighlightableButtonNode()
|
||||
self.moreButton = HighlightableButtonNode()
|
||||
self.actionButton = HighlightableButtonNode()
|
||||
self.scrollToTopButton = HighlightableButtonNode()
|
||||
|
||||
self.settingsButton.setImage(settingsImage, for: [])
|
||||
self.settingsButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: 44.0, height: 44.0))
|
||||
self.actionButton.setImage(actionImage, for: [])
|
||||
self.intrinsicActionSize = CGSize(width: 44.0, height: 44.0)
|
||||
self.intrinsicSmallActionSize = CGSize(width: 20.0, height: 20.0)
|
||||
self.actionButton.frame = CGRect(origin: CGPoint(), size: self.intrinsicActionSize)
|
||||
|
||||
self.moreButton.setImage(moreImage, for: [])
|
||||
self.intrinsicMoreSize = CGSize(width: 44.0, height: 44.0)
|
||||
self.intrinsicSmallMoreSize = CGSize(width: 20.0, height: 20.0)
|
||||
self.moreButton.frame = CGRect(origin: CGPoint(), size: self.intrinsicMoreSize)
|
||||
|
||||
self.arrowNode = ASImageNode()
|
||||
self.arrowNode.image = backArrowImage
|
||||
@@ -44,50 +57,55 @@ final class InstantPageNavigationBar: ASDisplayNode {
|
||||
self.arrowNode.displayWithoutProcessing = true
|
||||
self.arrowNode.displaysAsynchronously = false
|
||||
|
||||
self.shareLabel = ASTextNode()
|
||||
self.shareLabel.attributedText = NSAttributedString(string: strings.Channel_Share, font: Font.regular(17.0), textColor: UIColor(white: 1.0, alpha: 0.7))
|
||||
self.shareLabel.isLayerBacked = true
|
||||
self.shareLabel.displaysAsynchronously = false
|
||||
|
||||
let shareLabelSmall = ASTextNode()
|
||||
shareLabelSmall.attributedText = NSAttributedString(string: strings.Channel_Share, font: Font.regular(12.0), textColor: UIColor(white: 1.0, alpha: 0.7))
|
||||
|
||||
self.shareLabelSize = self.shareLabel.measure(CGSize(width: 200.0, height: 100.0))
|
||||
self.shareLabelSmallSize = shareLabelSmall.measure(CGSize(width: 200.0, height: 100.0))
|
||||
|
||||
self.shareLabel.frame = CGRect(origin: CGPoint(), size: self.shareLabelSize)
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = .black
|
||||
|
||||
self.backButton.addSubnode(self.arrowNode)
|
||||
self.shareButton.addSubnode(self.shareLabel)
|
||||
|
||||
self.addSubnode(self.pageProgressNode)
|
||||
self.addSubnode(self.backButton)
|
||||
self.addSubnode(self.shareButton)
|
||||
self.addSubnode(self.scrollToTopButton)
|
||||
//self.addSubnode(self.settingsButton)
|
||||
self.addSubnode(self.moreButton)
|
||||
self.addSubnode(self.actionButton)
|
||||
|
||||
self.backButton.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside)
|
||||
self.shareButton.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside)
|
||||
self.settingsButton.addTarget(self, action: #selector(self.settingsPressed), forControlEvents: .touchUpInside)
|
||||
self.actionButton.addTarget(self, action: #selector(self.actionPressed), forControlEvents: .touchUpInside)
|
||||
self.moreButton.addTarget(self, action: #selector(self.morePressed), forControlEvents: .touchUpInside)
|
||||
self.scrollToTopButton.addTarget(self, action: #selector(self.scrollToTopPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func backPressed() {
|
||||
self.back?()
|
||||
}
|
||||
|
||||
@objc func sharePressed() {
|
||||
@objc func actionPressed() {
|
||||
self.share?()
|
||||
}
|
||||
|
||||
@objc func settingsPressed() {
|
||||
@objc func morePressed() {
|
||||
self.settings?()
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
@objc func scrollToTopPressed() {
|
||||
self.scrollToTop?()
|
||||
}
|
||||
|
||||
func updateDimmed(_ dimmed: Bool, transition: ContainedViewLayoutTransition) {
|
||||
if dimmed != self.dimmed {
|
||||
self.dimmed = dimmed
|
||||
transition.updateAlpha(node: self.arrowNode, alpha: dimmed ? 0.5 : 1.0)
|
||||
var buttonsAlpha = self.buttonsAlphaFactor
|
||||
if dimmed {
|
||||
buttonsAlpha *= 0.5
|
||||
}
|
||||
transition.updateAlpha(node: self.actionButton, alpha: buttonsAlpha)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, pageProgress: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
transition.updateFrame(node: self.pageProgressNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: floorToScreenPixels(size.width * pageProgress), height: size.height)))
|
||||
|
||||
transition.updateFrame(node: self.backButton, frame: CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: CGSize(width: 100.0, height: size.height)))
|
||||
if let image = arrowNode.image {
|
||||
let arrowImageSize = image.size
|
||||
@@ -102,41 +120,39 @@ final class InstantPageNavigationBar: ASDisplayNode {
|
||||
transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: 8.0, y: max(0.0, size.height - 44.0) + floor((min(size.height, 44.0) - scaledArrowSize.height) / 2.0)), size: scaledArrowSize))
|
||||
}
|
||||
|
||||
transition.updateFrame(node: shareButton, frame: CGRect(origin: CGPoint(x: size.width - 80.0, y: 0.0), size: CGSize(width: 80.0, height: size.height)))
|
||||
|
||||
let shareImageSize = self.shareLabelSize
|
||||
let shareSmallImageSize = self.shareLabelSmallSize
|
||||
let shareHeight: CGFloat
|
||||
let offsetScaleFactor: CGFloat
|
||||
let buttonScaleFactor: CGFloat
|
||||
if size.height.isLess(than: 64.0) {
|
||||
let k = (shareImageSize.height - shareSmallImageSize.height) / 44.0
|
||||
let b = shareSmallImageSize.height - k * 20.0;
|
||||
shareHeight = k * size.height + b
|
||||
offsetScaleFactor = max(size.height - 20.0, 0.0) / 44.0
|
||||
let k = (self.intrinsicMoreSize.height - self.intrinsicSmallMoreSize.height) / 44.0
|
||||
let b = self.intrinsicSmallMoreSize.height - k * 20.0;
|
||||
buttonScaleFactor = (k * size.height + b) / self.intrinsicMoreSize.height
|
||||
} else {
|
||||
shareHeight = shareImageSize.height;
|
||||
offsetScaleFactor = 1.0
|
||||
buttonScaleFactor = 1.0
|
||||
}
|
||||
let shareHeightFactor = shareHeight / shareImageSize.height
|
||||
transition.updateTransformScale(node: self.shareLabel, scale: shareHeightFactor)
|
||||
|
||||
let scaledShareSize = CGSize(width: shareImageSize.width * shareHeightFactor, height: shareImageSize.height * shareHeightFactor)
|
||||
let shareLabelCenter = CGPoint(x: 80.0 - 8.0 - scaledShareSize.width / 2.0, y: max(0.0, size.height - 44.0) + min(size.height, 44.0) / 2.0)
|
||||
transition.updatePosition(node: self.shareLabel, position: shareLabelCenter)
|
||||
var alphaFactor = min(1.0, offsetScaleFactor * offsetScaleFactor)
|
||||
self.buttonsAlphaFactor = alphaFactor
|
||||
if self.dimmed {
|
||||
alphaFactor *= 0.5
|
||||
}
|
||||
|
||||
let alpha = 1.0 - (shareImageSize.height - shareHeight) / (shareImageSize.height - shareSmallImageSize.height)
|
||||
let diffFactor = shareSmallImageSize.height / shareImageSize.height
|
||||
let smallSettingsWidth = 44.0 * diffFactor
|
||||
let offset = smallSettingsWidth / 4.0
|
||||
transition.updateTransformScale(node: self.moreButton, scale: buttonScaleFactor)
|
||||
transition.updatePosition(node: self.moreButton, position: CGPoint(x: size.width - buttonScaleFactor * self.intrinsicMoreSize.width / 2.0, y: offsetScaleFactor * 20.0 + buttonScaleFactor * self.intrinsicMoreSize.height / 2.0))
|
||||
transition.updateAlpha(node: self.moreButton, alpha: alphaFactor)
|
||||
transition.updateTransformScale(node: self.actionButton, scale: buttonScaleFactor)
|
||||
transition.updatePosition(node: self.actionButton, position: CGPoint(x: size.width - buttonScaleFactor * self.intrinsicMoreSize.width - buttonScaleFactor * self.intrinsicActionSize.width / 2.0, y: offsetScaleFactor * 20.0 + buttonScaleFactor * self.intrinsicActionSize.height / 2.0))
|
||||
transition.updateAlpha(node: self.actionButton, alpha: alphaFactor)
|
||||
|
||||
let spacing = max(4.0, (shareLabelCenter.x - scaledShareSize.width / 2.0) * -1.0 + 4.0)
|
||||
|
||||
let xa = shareLabelCenter.x - scaledShareSize.width / 2.0
|
||||
let xb = spacing - (44.0 * shareHeightFactor) / 2.0
|
||||
let ya = max(0.0, size.height - 44.0)
|
||||
let yb = min(size.height, 44.0) / 2.0 - 22.0 - 44.0 / 2.0
|
||||
transition.updatePosition(node: self.settingsButton, position: CGPoint(x: xa - xb, y: ya + yb))
|
||||
transition.updateTransformScale(node: self.settingsButton, scale: shareHeightFactor)
|
||||
|
||||
transition.updateAlpha(node: self.settingsButton, alpha: alpha)
|
||||
|
||||
transition.updateFrame(node: self.scrollToTopButton, frame: CGRect(origin: CGPoint(x: 100.0, y: 0.0), size: CGSize(width: size.width - 100.0 - 80.0 - 44.0, height: size.height)))
|
||||
transition.updateFrame(node: self.scrollToTopButton, frame: CGRect(origin: CGPoint(x: 64.0, y: 0.0), size: CGSize(width: size.width - 64.0, height: size.height)))
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.dimmed {
|
||||
return nil
|
||||
} else {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import AsyncDisplayKit
|
||||
|
||||
protocol InstantPageNode {
|
||||
func updateIsVisible(_ isVisible: Bool)
|
||||
|
||||
func transitionNode(media: InstantPageMedia) -> ASDisplayNode?
|
||||
func updateHiddenMedia(media: InstantPageMedia?)
|
||||
func update(strings: PresentationStrings, theme: InstantPageTheme)
|
||||
}
|
||||
|
||||
/*@class TGInstantPageMedia;
|
||||
|
||||
53
TelegramUI/InstantPagePeerReferenceItem.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
final class InstantPagePeerReferenceItem: InstantPageItem {
|
||||
var frame: CGRect
|
||||
let wantsNode: Bool = true
|
||||
let medias: [InstantPageMedia] = []
|
||||
|
||||
let initialPeer: Peer
|
||||
let rtl: Bool
|
||||
|
||||
init(frame: CGRect, initialPeer: Peer, rtl: Bool) {
|
||||
self.frame = frame
|
||||
self.initialPeer = initialPeer
|
||||
self.rtl = rtl
|
||||
}
|
||||
|
||||
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? {
|
||||
return InstantPagePeerReferenceNode(account: account, strings: strings, theme: theme, initialPeer: self.initialPeer, rtl: self.rtl, openPeer: openPeer)
|
||||
}
|
||||
|
||||
func matchesAnchor(_ anchor: String) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func matchesNode(_ node: InstantPageNode) -> Bool {
|
||||
if let node = node as? InstantPagePeerReferenceNode {
|
||||
return self.initialPeer.id == node.initialPeer.id
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func distanceThresholdGroup() -> Int? {
|
||||
return 4
|
||||
}
|
||||
|
||||
func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat {
|
||||
if count > 3 {
|
||||
return 1000.0
|
||||
} else {
|
||||
return CGFloat.greatestFiniteMagnitude
|
||||
}
|
||||
}
|
||||
|
||||
func linkSelectionRects(at point: CGPoint) -> [CGRect] {
|
||||
return []
|
||||
}
|
||||
|
||||
func drawInTile(context: CGContext) {
|
||||
}
|
||||
}
|
||||
271
TelegramUI/InstantPagePeerReferenceNode.swift
Normal file
@@ -0,0 +1,271 @@
|
||||
import Foundation
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
private enum JoinState: Equatable {
|
||||
case none
|
||||
case notJoined
|
||||
case inProgress
|
||||
case joined(justNow: Bool)
|
||||
|
||||
static func ==(lhs: JoinState, rhs: JoinState) -> Bool {
|
||||
switch lhs {
|
||||
case .none:
|
||||
if case .none = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .notJoined:
|
||||
if case .notJoined = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .inProgress:
|
||||
if case .inProgress = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .joined(justNow):
|
||||
if case .joined(justNow) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
||||
private let account: Account
|
||||
let initialPeer: Peer
|
||||
private let rtl: Bool
|
||||
private var strings: PresentationStrings
|
||||
private var theme: InstantPageTheme
|
||||
private let openPeer: (PeerId) -> Void
|
||||
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let buttonNode: HighlightableButtonNode
|
||||
private let nameNode: ASTextNode
|
||||
private let joinNode: HighlightableButtonNode
|
||||
private let activityIndicator: ActivityIndicator
|
||||
private let checkNode: ASImageNode
|
||||
|
||||
private var peer: Peer?
|
||||
private var peerDisposable: Disposable?
|
||||
|
||||
private let joinDisposable = MetaDisposable()
|
||||
|
||||
private var joinState: JoinState = .none
|
||||
|
||||
init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, initialPeer: Peer, rtl: Bool, openPeer: @escaping (PeerId) -> Void) {
|
||||
self.account = account
|
||||
self.strings = strings
|
||||
self.theme = theme
|
||||
self.initialPeer = initialPeer
|
||||
self.rtl = rtl
|
||||
self.openPeer = openPeer
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
self.highlightedBackgroundNode.backgroundColor = theme.panelHighlightedBackgroundColor
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
|
||||
self.buttonNode = HighlightableButtonNode()
|
||||
|
||||
self.nameNode = ASTextNode()
|
||||
self.nameNode.isLayerBacked = true
|
||||
self.nameNode.maximumNumberOfLines = 1
|
||||
|
||||
self.joinNode = HighlightableButtonNode()
|
||||
self.joinNode.hitTestSlop = UIEdgeInsets(top: -17.0, left: -17.0, bottom: -17.0, right: -17.0)
|
||||
|
||||
self.activityIndicator = ActivityIndicator(type: .custom(theme.panelAccentColor))
|
||||
|
||||
self.checkNode = ASImageNode()
|
||||
self.checkNode.isLayerBacked = true
|
||||
self.checkNode.displayWithoutProcessing = true
|
||||
self.checkNode.displaysAsynchronously = false
|
||||
self.checkNode.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = theme.panelBackgroundColor
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
self.addSubnode(self.joinNode)
|
||||
self.addSubnode(self.checkNode)
|
||||
self.addSubnode(self.nameNode)
|
||||
|
||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.highlightedBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
||||
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.joinNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.joinNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.joinNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.joinNode.alpha = 1.0
|
||||
strongSelf.joinNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
self.joinNode.addTarget(self, action: #selector(self.joinPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.peerDisposable = (actualizedPeer(postbox: self.account.postbox, network: self.account.network, peer: self.initialPeer) |> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
if let strongSelf = self {
|
||||
strongSelf.nameNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: strongSelf.theme.panelPrimaryColor)
|
||||
if let peer = peer as? TelegramChannel {
|
||||
var joinState = strongSelf.joinState
|
||||
if case .member = peer.participationStatus {
|
||||
switch joinState {
|
||||
case .none:
|
||||
joinState = .joined(justNow: false)
|
||||
case .inProgress, .notJoined:
|
||||
joinState = .joined(justNow: true)
|
||||
case .joined:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
joinState = .notJoined
|
||||
}
|
||||
strongSelf.updateJoinState(joinState)
|
||||
}
|
||||
strongSelf.setNeedsLayout()
|
||||
}
|
||||
})
|
||||
|
||||
self.applyThemeAndStrings(themeUpdated: true)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.peerDisposable?.dispose()
|
||||
self.joinDisposable.dispose()
|
||||
}
|
||||
|
||||
func update(strings: PresentationStrings, theme: InstantPageTheme) {
|
||||
if self.strings !== strings || self.theme !== theme {
|
||||
let themeUpdated = self.theme !== theme
|
||||
self.strings = strings
|
||||
self.theme = theme
|
||||
self.applyThemeAndStrings(themeUpdated: themeUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyThemeAndStrings(themeUpdated: Bool) {
|
||||
if let peer = self.peer {
|
||||
self.nameNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: self.theme.panelPrimaryColor)
|
||||
}
|
||||
self.joinNode.setAttributedTitle(NSAttributedString(string: self.strings.Channel_JoinChannel, font: Font.medium(17.0), textColor: self.theme.panelAccentColor), for: [])
|
||||
|
||||
if themeUpdated {
|
||||
self.checkNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/PanelCheck"), color: self.theme.panelSecondaryColor)
|
||||
self.activityIndicator.type = .custom(self.theme.panelAccentColor)
|
||||
}
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
private func updateJoinState(_ joinState: JoinState) {
|
||||
if self.joinState != joinState {
|
||||
self.joinState = joinState
|
||||
|
||||
switch joinState {
|
||||
case .none:
|
||||
self.joinNode.isHidden = true
|
||||
self.checkNode.isHidden = true
|
||||
if self.activityIndicator.supernode != nil {
|
||||
self.activityIndicator.removeFromSupernode()
|
||||
}
|
||||
case .notJoined:
|
||||
self.joinNode.isHidden = false
|
||||
self.checkNode.isHidden = true
|
||||
if self.activityIndicator.supernode != nil {
|
||||
self.activityIndicator.removeFromSupernode()
|
||||
}
|
||||
case .inProgress:
|
||||
self.joinNode.isHidden = true
|
||||
self.checkNode.isHidden = true
|
||||
if self.activityIndicator.supernode == nil {
|
||||
self.addSubnode(self.activityIndicator)
|
||||
}
|
||||
case let .joined(justNow):
|
||||
self.joinNode.isHidden = true
|
||||
self.checkNode.isHidden = !justNow
|
||||
if self.activityIndicator.supernode != nil {
|
||||
self.activityIndicator.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
let inset: CGFloat = 17.0
|
||||
|
||||
self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
let joinSize = self.joinNode.measure(size)
|
||||
let nameSize = self.nameNode.measure(CGSize(width: size.width - inset * 2.0 - joinSize.width, height: size.height))
|
||||
let checkSize = self.checkNode.measure(size)
|
||||
let indicatorSize = self.activityIndicator.measure(size)
|
||||
|
||||
if self.rtl {
|
||||
self.nameNode.frame = CGRect(origin: CGPoint(x: size.width - 17.0 - nameSize.width, y: floor((size.height - nameSize.height) / 2.0)), size: nameSize)
|
||||
self.joinNode.frame = CGRect(origin: CGPoint(x: 17.0, y: floor((size.height - joinSize.height) / 2.0)), size: joinSize)
|
||||
self.checkNode.frame = CGRect(origin: CGPoint(x: 17.0, y: floor((size.height - checkSize.height) / 2.0)), size: checkSize)
|
||||
self.activityIndicator.frame = CGRect(origin: CGPoint(x: 17.0, y: floor((size.height - indicatorSize.height) / 2.0)), size: indicatorSize)
|
||||
} else {
|
||||
self.nameNode.frame = CGRect(origin: CGPoint(x: 17.0, y: floor((size.height - nameSize.height) / 2.0)), size: nameSize)
|
||||
self.joinNode.frame = CGRect(origin: CGPoint(x: size.width - 17.0 - joinSize.width, y: floor((size.height - joinSize.height) / 2.0)), size: joinSize)
|
||||
self.checkNode.frame = CGRect(origin: CGPoint(x: size.width - 17.0 - checkSize.width, y: floor((size.height - checkSize.height) / 2.0)), size: checkSize)
|
||||
self.activityIndicator.frame = CGRect(origin: CGPoint(x: size.width - 17.0 - indicatorSize.width, y: floor((size.height - indicatorSize.height) / 2.0)), size: indicatorSize)
|
||||
}
|
||||
}
|
||||
|
||||
func transitionNode(media: InstantPageMedia) -> ASDisplayNode? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateHiddenMedia(media: InstantPageMedia?) {
|
||||
}
|
||||
|
||||
func updateIsVisible(_ isVisible: Bool) {
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
self.openPeer(self.initialPeer.id)
|
||||
}
|
||||
|
||||
@objc func joinPressed() {
|
||||
if case .notJoined = self.joinState {
|
||||
self.updateJoinState(.inProgress)
|
||||
self.joinDisposable.set((joinChannel(account: self.account, peerId: self.initialPeer.id) |> deliverOnMainQueue).start(error: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if case .inProgress = strongSelf.joinState {
|
||||
strongSelf.updateJoinState(.notJoined)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
enum InstantPageMediaArguments {
|
||||
case image(interactive: Bool, roundCorners: Bool, fit: Bool)
|
||||
case video(interactive: Bool, autoplay: Bool)
|
||||
}
|
||||
|
||||
final class InstantPageMediaItem: InstantPageItem {
|
||||
final class InstantPagePlayableVideoItem: InstantPageItem {
|
||||
var frame: CGRect
|
||||
|
||||
let media: InstantPageMedia
|
||||
@@ -14,19 +10,18 @@ final class InstantPageMediaItem: InstantPageItem {
|
||||
return [self.media]
|
||||
}
|
||||
|
||||
let arguments: InstantPageMediaArguments
|
||||
let interactive: Bool
|
||||
|
||||
let wantsNode: Bool = true
|
||||
let hasLinks: Bool = false
|
||||
|
||||
init(frame: CGRect, media: InstantPageMedia, arguments: InstantPageMediaArguments) {
|
||||
init(frame: CGRect, media: InstantPageMedia, interactive: Bool) {
|
||||
self.frame = frame
|
||||
self.media = media
|
||||
self.arguments = arguments
|
||||
self.interactive = interactive
|
||||
}
|
||||
|
||||
func node(account: Account) -> InstantPageNode? {
|
||||
return InstantPageMediaNode(account: account, media: self.media, arguments: self.arguments)
|
||||
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? {
|
||||
return InstantPagePlayableVideoNode(account: account, media: self.media, interactive: self.interactive, openMedia: openMedia)
|
||||
}
|
||||
|
||||
func matchesAnchor(_ anchor: String) -> Bool {
|
||||
@@ -34,7 +29,7 @@ final class InstantPageMediaItem: InstantPageItem {
|
||||
}
|
||||
|
||||
func matchesNode(_ node: InstantPageNode) -> Bool {
|
||||
if let node = node as? InstantPageMediaNode {
|
||||
if let node = node as? InstantPagePlayableVideoNode {
|
||||
return node.media == self.media
|
||||
} else {
|
||||
return false
|
||||
@@ -42,12 +37,12 @@ final class InstantPageMediaItem: InstantPageItem {
|
||||
}
|
||||
|
||||
func distanceThresholdGroup() -> Int? {
|
||||
return 1
|
||||
return 2
|
||||
}
|
||||
|
||||
func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat {
|
||||
if count > 3 {
|
||||
return 400.0
|
||||
return 200.0
|
||||
} else {
|
||||
return CGFloat.greatestFiniteMagnitude
|
||||
}
|
||||
@@ -56,7 +51,8 @@ final class InstantPageMediaItem: InstantPageItem {
|
||||
func drawInTile(context: CGContext) {
|
||||
}
|
||||
|
||||
func linkSelectionViews() -> [InstantPageLinkSelectionView] {
|
||||
func linkSelectionRects(at point: CGPoint) -> [CGRect] {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
116
TelegramUI/InstantPagePlayableVideoNode.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode {
|
||||
private let account: Account
|
||||
let media: InstantPageMedia
|
||||
private let interactive: Bool
|
||||
private let openMedia: (InstantPageMedia) -> Void
|
||||
|
||||
private let imageNode: TransformImageNode
|
||||
private let videoNode: ManagedVideoNode
|
||||
|
||||
private var currentSize: CGSize?
|
||||
|
||||
private var fetchedDisposable = MetaDisposable()
|
||||
|
||||
private var localIsVisible = false
|
||||
|
||||
init(account: Account, media: InstantPageMedia, interactive: Bool, openMedia: @escaping (InstantPageMedia) -> Void) {
|
||||
self.account = account
|
||||
self.media = media
|
||||
self.interactive = interactive
|
||||
self.openMedia = openMedia
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
self.videoNode = ManagedVideoNode(preferSoftwareDecoding: false, backgroundThread: false)
|
||||
|
||||
super.init()
|
||||
|
||||
self.imageNode.alphaTransitionOnFirstUpdate = true
|
||||
self.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.videoNode)
|
||||
|
||||
if let file = media.media as? TelegramMediaFile {
|
||||
self.imageNode.setSignal(account: account, signal: chatMessageVideo(account: account, video: file))
|
||||
self.fetchedDisposable.set(chatMessageFileInteractiveFetched(account: account, file: file).start())
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
if self.interactive {
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
}
|
||||
|
||||
func updateIsVisible(_ isVisible: Bool) {
|
||||
if self.localIsVisible != isVisible {
|
||||
self.localIsVisible = isVisible
|
||||
|
||||
if isVisible {
|
||||
if let file = media.media as? TelegramMediaFile {
|
||||
self.videoNode.acquireContext(account: self.account, mediaManager: account.telegramApplicationContext.mediaManager, id: InstantPageManagedMediaId(media: self.media), resource: file.resource, priority: 0)
|
||||
}
|
||||
} else {
|
||||
self.videoNode.discardContext()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(strings: PresentationStrings, theme: InstantPageTheme) {
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
if self.currentSize != size {
|
||||
self.currentSize = size
|
||||
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.videoNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
if let file = self.media.media as? TelegramMediaFile, let dimensions = file.dimensions {
|
||||
let imageSize = dimensions.aspectFilled(size)
|
||||
let boundingSize = size
|
||||
|
||||
let makeLayout = self.imageNode.asyncLayout()
|
||||
let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())
|
||||
let apply = makeLayout(arguments)
|
||||
apply()
|
||||
|
||||
self.videoNode.transformArguments = arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func transitionNode(media: InstantPageMedia) -> ASDisplayNode? {
|
||||
if media == self.media {
|
||||
return self.videoNode
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateHiddenMedia(media: InstantPageMedia?) {
|
||||
self.imageNode.isHidden = self.media == media
|
||||
self.videoNode.isHidden = self.media == media
|
||||
}
|
||||
|
||||
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.openMedia(self.media)
|
||||
}
|
||||
}
|
||||
}
|
||||
102
TelegramUI/InstantPagePresentationSettings.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
|
||||
enum InstantPageThemeType: Int32 {
|
||||
case light = 0
|
||||
case dark = 1
|
||||
case sepia = 2
|
||||
case gray = 3
|
||||
}
|
||||
|
||||
enum InstantPagePresentationFontSize: Int32 {
|
||||
case small = 0
|
||||
case standard = 1
|
||||
case large = 2
|
||||
case xlarge = 3
|
||||
case xxlarge = 4
|
||||
}
|
||||
|
||||
final class InstantPagePresentationSettings: PreferencesEntry, Equatable {
|
||||
static var defaultSettings = InstantPagePresentationSettings(themeType: .light, fontSize: .standard, forceSerif: false, autoNightMode: true)
|
||||
|
||||
let themeType: InstantPageThemeType
|
||||
let fontSize: InstantPagePresentationFontSize
|
||||
let forceSerif: Bool
|
||||
let autoNightMode: Bool
|
||||
|
||||
init(themeType: InstantPageThemeType, fontSize: InstantPagePresentationFontSize, forceSerif: Bool, autoNightMode: Bool) {
|
||||
self.themeType = themeType
|
||||
self.fontSize = fontSize
|
||||
self.forceSerif = forceSerif
|
||||
self.autoNightMode = autoNightMode
|
||||
}
|
||||
|
||||
init(decoder: PostboxDecoder) {
|
||||
self.themeType = InstantPageThemeType(rawValue: decoder.decodeInt32ForKey("themeType", orElse: 0))!
|
||||
self.fontSize = InstantPagePresentationFontSize(rawValue: decoder.decodeInt32ForKey("fontSize", orElse: 0))!
|
||||
self.forceSerif = decoder.decodeInt32ForKey("forceSerif", orElse: 0) != 0
|
||||
self.autoNightMode = decoder.decodeInt32ForKey("autoNightMode", orElse: 0) != 0
|
||||
}
|
||||
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.themeType.rawValue, forKey: "themeType")
|
||||
encoder.encodeInt32(self.fontSize.rawValue, forKey: "fontSize")
|
||||
encoder.encodeInt32(self.forceSerif ? 1 : 0, forKey: "forceSerif")
|
||||
encoder.encodeInt32(self.autoNightMode ? 1 : 0, forKey: "autoNightMode")
|
||||
}
|
||||
|
||||
func isEqual(to: PreferencesEntry) -> Bool {
|
||||
if let to = to as? InstantPagePresentationSettings {
|
||||
return self == to
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: InstantPagePresentationSettings, rhs: InstantPagePresentationSettings) -> Bool {
|
||||
if lhs.themeType != rhs.themeType {
|
||||
return false
|
||||
}
|
||||
if lhs.fontSize != rhs.fontSize {
|
||||
return false
|
||||
}
|
||||
if lhs.forceSerif != rhs.forceSerif {
|
||||
return false
|
||||
}
|
||||
if lhs.autoNightMode != rhs.autoNightMode {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func withUpdatedThemeType(_ themeType: InstantPageThemeType) -> InstantPagePresentationSettings {
|
||||
return InstantPagePresentationSettings(themeType: themeType, fontSize: self.fontSize, forceSerif: self.forceSerif, autoNightMode: self.autoNightMode)
|
||||
}
|
||||
|
||||
func withUpdatedFontSize(_ fontSize: InstantPagePresentationFontSize) -> InstantPagePresentationSettings {
|
||||
return InstantPagePresentationSettings(themeType: self.themeType, fontSize: fontSize, forceSerif: self.forceSerif, autoNightMode: self.autoNightMode)
|
||||
}
|
||||
|
||||
func withUpdatedForceSerif(_ forceSerif: Bool) -> InstantPagePresentationSettings {
|
||||
return InstantPagePresentationSettings(themeType: self.themeType, fontSize: self.fontSize, forceSerif: forceSerif, autoNightMode: self.autoNightMode)
|
||||
}
|
||||
|
||||
func withUpdatedAutoNightMode(_ autoNightMode: Bool) -> InstantPagePresentationSettings {
|
||||
return InstantPagePresentationSettings(themeType: self.themeType, fontSize: self.fontSize, forceSerif: self.forceSerif, autoNightMode: autoNightMode)
|
||||
}
|
||||
}
|
||||
|
||||
func updateInstantPagePresentationSettingsInteractively(postbox: Postbox, _ f: @escaping (InstantPagePresentationSettings) -> InstantPagePresentationSettings) -> Signal<Void, NoError> {
|
||||
return postbox.modify { modifier -> Void in
|
||||
modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.instantPagePresentationSettings, { entry in
|
||||
let currentSettings: InstantPagePresentationSettings
|
||||
if let entry = entry as? InstantPagePresentationSettings {
|
||||
currentSettings = entry
|
||||
} else {
|
||||
currentSettings = InstantPagePresentationSettings.defaultSettings
|
||||
}
|
||||
return f(currentSettings)
|
||||
})
|
||||
}
|
||||
}
|
||||
76
TelegramUI/InstantPageSettingsBacklightItemNode.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
import LegacyComponents
|
||||
|
||||
private func generateKnobImage() -> UIImage? {
|
||||
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: -1.0), blur: 3.5, color: UIColor(white: 0.0, alpha: 0.25).cgColor)
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0)))
|
||||
})
|
||||
}
|
||||
|
||||
final class InstantPageSettingsBacklightItemNode: InstantPageSettingsItemNode {
|
||||
private let sliderView: TGPhotoEditorSliderView
|
||||
private let leftIconNode: ASImageNode
|
||||
private let rightIconNode: ASImageNode
|
||||
|
||||
init(theme: InstantPageSettingsItemTheme) {
|
||||
self.sliderView = TGPhotoEditorSliderView()
|
||||
self.sliderView.trackCornerRadius = 1.0
|
||||
self.sliderView.lineSize = 2.0
|
||||
self.sliderView.minimumValue = 0.0
|
||||
self.sliderView.startValue = 0.0
|
||||
self.sliderView.maximumValue = 100.0
|
||||
self.sliderView.disablesInteractiveTransitionGestureRecognizer = true
|
||||
|
||||
self.leftIconNode = ASImageNode()
|
||||
self.leftIconNode.displaysAsynchronously = false
|
||||
self.leftIconNode.displayWithoutProcessing = true
|
||||
|
||||
self.rightIconNode = ASImageNode()
|
||||
self.rightIconNode.displaysAsynchronously = false
|
||||
self.rightIconNode.displayWithoutProcessing = true
|
||||
|
||||
super.init(theme: theme, selectable: false)
|
||||
|
||||
self.updateTheme(theme)
|
||||
|
||||
self.sliderView.value = UIScreen.main.brightness * 100.0
|
||||
self.sliderView.addTarget(self, action: #selector(self.sliderChanged), for: .valueChanged)
|
||||
self.view.addSubview(self.sliderView)
|
||||
|
||||
self.addSubnode(self.leftIconNode)
|
||||
self.addSubnode(self.rightIconNode)
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: InstantPageSettingsItemTheme) {
|
||||
super.updateTheme(theme)
|
||||
|
||||
self.sliderView.backgroundColor = theme.itemBackgroundColor
|
||||
self.sliderView.backColor = theme.secondaryColor
|
||||
self.sliderView.trackColor = theme.accentColor
|
||||
self.sliderView.knobImage = generateKnobImage()
|
||||
|
||||
self.leftIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsBrightnessMinIcon"), color: theme.primaryColor)
|
||||
self.rightIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsBrightnessMaxIcon"), color: theme.primaryColor)
|
||||
}
|
||||
|
||||
override func updateInternalLayout(width: CGFloat, insets: UIEdgeInsets, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> (height: CGFloat, separatorInset: CGFloat?) {
|
||||
self.sliderView.frame = CGRect(origin: CGPoint(x: 38.0, y: 8.0), size: CGSize(width: width - 38.0 * 2.0, height: 44.0))
|
||||
if let image = self.leftIconNode.image {
|
||||
self.leftIconNode.frame = CGRect(origin: CGPoint(x: 16.0, y: 24.0), size: CGSize(width: image.size.width, height: image.size.height))
|
||||
}
|
||||
if let image = self.rightIconNode.image {
|
||||
self.rightIconNode.frame = CGRect(origin: CGPoint(x: width - 13.0 - image.size.width, y: 21.0), size: CGSize(width: image.size.width, height: image.size.height))
|
||||
}
|
||||
return (62.0 + insets.top + insets.bottom, nil)
|
||||
}
|
||||
|
||||
@objc func sliderChanged() {
|
||||
UIScreen.main.brightness = self.sliderView.value / 100.0
|
||||
}
|
||||
}
|
||||