no message

This commit is contained in:
Peter
2017-09-05 21:27:04 +03:00
parent cbbcb12b3b
commit baf29247eb
142 changed files with 6533 additions and 998 deletions

View File

@@ -11,6 +11,7 @@
},
{
"idiom" : "universal",
"filename" : "ModernConversationAudioSlideToCancel@3x.png",
"scale" : "3x"
}
],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 B

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 B

View File

@@ -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"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 B

View File

@@ -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"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

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

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