diff --git a/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/Contents.json index 0932520418..e885e800ab 100644 --- a/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/Contents.json @@ -11,6 +11,7 @@ }, { "idiom" : "universal", + "filename" : "ModernConversationAudioSlideToCancel@3x.png", "scale" : "3x" } ], diff --git a/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@2x.png b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@2x.png index 890ef7345a..4eaca6a590 100644 Binary files a/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@2x.png and b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@2x.png differ diff --git a/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@3x.png b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@3x.png new file mode 100644 index 0000000000..5799af0f18 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/AudioRecordingCancelArrow.imageset/ModernConversationAudioSlideToCancel@3x.png differ diff --git a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json index a4e9ce4cfd..6a61e6831b 100644 --- a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/Contents.json @@ -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" : { diff --git a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ModernConversationAttach@2x.png b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ModernConversationAttach@2x.png new file mode 100644 index 0000000000..60e641e6d5 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ModernConversationAttach@2x.png differ diff --git a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ModernConversationAttach@3x.png b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ModernConversationAttach@3x.png new file mode 100644 index 0000000000..4c6d4f6809 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ModernConversationAttach@3x.png differ diff --git a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ic_attach.pdf b/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ic_attach.pdf deleted file mode 100644 index 3a75f7a4f7..0000000000 Binary files a/Images.xcassets/Chat/Input/Text/IconAttachment.imageset/ic_attach.pdf and /dev/null differ diff --git a/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json index 72555bf44c..1bc8a07ac1 100644 --- a/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/Contents.json @@ -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" : { diff --git a/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@2x.png b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@2x.png new file mode 100644 index 0000000000..b407292b29 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@2x.png differ diff --git a/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@3x.png b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@3x.png new file mode 100644 index 0000000000..6e0c6307ad Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ModernConversationMicButton@3x.png differ diff --git a/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ic_voice.pdf b/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ic_voice.pdf deleted file mode 100644 index b5eb5080f5..0000000000 Binary files a/Images.xcassets/Chat/Input/Text/IconMicrophone.imageset/ic_voice.pdf and /dev/null differ diff --git a/Images.xcassets/Chat/Input/Text/IconSend.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/IconSend.imageset/Contents.json index ecfd165d98..64d86a74a3 100644 --- a/Images.xcassets/Chat/Input/Text/IconSend.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Text/IconSend.imageset/Contents.json @@ -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" : { diff --git a/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@2x.png b/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@2x.png new file mode 100644 index 0000000000..f31a1118cd Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@2x.png differ diff --git a/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@3x.png b/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@3x.png new file mode 100644 index 0000000000..f74122a57e Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/IconSend.imageset/ModernConversationSend@3x.png differ diff --git a/Images.xcassets/Chat/Input/Text/IconSend.imageset/Send.pdf b/Images.xcassets/Chat/Input/Text/IconSend.imageset/Send.pdf deleted file mode 100644 index 5ba0bfdaf1..0000000000 Binary files a/Images.xcassets/Chat/Input/Text/IconSend.imageset/Send.pdf and /dev/null differ diff --git a/Images.xcassets/Instant View/SettingsIcon.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/IconVideo.imageset/Contents.json similarity index 71% rename from Images.xcassets/Instant View/SettingsIcon.imageset/Contents.json rename to Images.xcassets/Chat/Input/Text/IconVideo.imageset/Contents.json index a738861b67..0ef8e70c7f 100644 --- a/Images.xcassets/Instant View/SettingsIcon.imageset/Contents.json +++ b/Images.xcassets/Chat/Input/Text/IconVideo.imageset/Contents.json @@ -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" } ], diff --git a/Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@2x.png b/Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@2x.png new file mode 100644 index 0000000000..c13b7b5134 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@2x.png differ diff --git a/Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@3x.png b/Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@3x.png new file mode 100644 index 0000000000..7e019781f7 Binary files /dev/null and b/Images.xcassets/Chat/Input/Text/IconVideo.imageset/RecordVideoIcon@3x.png differ diff --git a/Images.xcassets/Instant View/ActionIcon.imageset/Contents.json b/Images.xcassets/Instant View/ActionIcon.imageset/Contents.json new file mode 100644 index 0000000000..22c1331282 --- /dev/null +++ b/Images.xcassets/Instant View/ActionIcon.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Instant View/ActionIcon.imageset/ic_share@2x.png b/Images.xcassets/Instant View/ActionIcon.imageset/ic_share@2x.png new file mode 100644 index 0000000000..67586711db Binary files /dev/null and b/Images.xcassets/Instant View/ActionIcon.imageset/ic_share@2x.png differ diff --git a/Images.xcassets/Instant View/ActionIcon.imageset/ic_share@3x.png b/Images.xcassets/Instant View/ActionIcon.imageset/ic_share@3x.png new file mode 100644 index 0000000000..ec02872bc2 Binary files /dev/null and b/Images.xcassets/Instant View/ActionIcon.imageset/ic_share@3x.png differ diff --git a/Images.xcassets/Instant View/BackArrow.imageset/InstantPageBackArrow@2x.png b/Images.xcassets/Instant View/BackArrow.imageset/InstantPageBackArrow@2x.png deleted file mode 100644 index a5090c6c8d..0000000000 Binary files a/Images.xcassets/Instant View/BackArrow.imageset/InstantPageBackArrow@2x.png and /dev/null differ diff --git a/Images.xcassets/Instant View/BackArrow.imageset/Contents.json b/Images.xcassets/Instant View/MoreIcon.imageset/Contents.json similarity index 78% rename from Images.xcassets/Instant View/BackArrow.imageset/Contents.json rename to Images.xcassets/Instant View/MoreIcon.imageset/Contents.json index 2b39b50424..6b7a481581 100644 --- a/Images.xcassets/Instant View/BackArrow.imageset/Contents.json +++ b/Images.xcassets/Instant View/MoreIcon.imageset/Contents.json @@ -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" } ], diff --git a/Images.xcassets/Instant View/MoreIcon.imageset/ic_more@2x.png b/Images.xcassets/Instant View/MoreIcon.imageset/ic_more@2x.png new file mode 100644 index 0000000000..b4a1c31720 Binary files /dev/null and b/Images.xcassets/Instant View/MoreIcon.imageset/ic_more@2x.png differ diff --git a/Images.xcassets/Instant View/MoreIcon.imageset/ic_more@3x.png b/Images.xcassets/Instant View/MoreIcon.imageset/ic_more@3x.png new file mode 100644 index 0000000000..5dcf7d8ea4 Binary files /dev/null and b/Images.xcassets/Instant View/MoreIcon.imageset/ic_more@3x.png differ diff --git a/Images.xcassets/Instant View/PanelCheck.imageset/Contents.json b/Images.xcassets/Instant View/PanelCheck.imageset/Contents.json new file mode 100644 index 0000000000..d4e3338f6e --- /dev/null +++ b/Images.xcassets/Instant View/PanelCheck.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@2x.png b/Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@2x.png new file mode 100644 index 0000000000..72914fbdf0 Binary files /dev/null and b/Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@2x.png differ diff --git a/Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@3x.png b/Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@3x.png new file mode 100644 index 0000000000..e9224bfcb0 Binary files /dev/null and b/Images.xcassets/Instant View/PanelCheck.imageset/InstantViewCheck@3x.png differ diff --git a/Images.xcassets/Instant View/SettingsArrow.imageset/Contents.json b/Images.xcassets/Instant View/SettingsArrow.imageset/Contents.json new file mode 100644 index 0000000000..7698ab82a1 --- /dev/null +++ b/Images.xcassets/Instant View/SettingsArrow.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Instant View/SettingsArrow.imageset/InstantViewRightCorner@2x.png b/Images.xcassets/Instant View/SettingsArrow.imageset/InstantViewRightCorner@2x.png new file mode 100644 index 0000000000..50abf7379f Binary files /dev/null and b/Images.xcassets/Instant View/SettingsArrow.imageset/InstantViewRightCorner@2x.png differ diff --git a/Images.xcassets/Instant View/SettingsArrow.imageset/InstantViewRightCorner@3x.png b/Images.xcassets/Instant View/SettingsArrow.imageset/InstantViewRightCorner@3x.png new file mode 100644 index 0000000000..25deb49345 Binary files /dev/null and b/Images.xcassets/Instant View/SettingsArrow.imageset/InstantViewRightCorner@3x.png differ diff --git a/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/Contents.json b/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/Contents.json new file mode 100644 index 0000000000..0931c327d2 --- /dev/null +++ b/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@2x.png b/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@2x.png new file mode 100644 index 0000000000..da102fa6e4 Binary files /dev/null and b/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@2x.png differ diff --git a/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@3x.png b/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@3x.png new file mode 100644 index 0000000000..ac0611dd1c Binary files /dev/null and b/Images.xcassets/Instant View/SettingsBrightnessMaxIcon.imageset/InstantViewBrightnessMaxIcon@3x.png differ diff --git a/Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/Contents.json b/Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/Contents.json new file mode 100644 index 0000000000..acee728d72 --- /dev/null +++ b/Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/InstantViewBrightnessMinIcon@2x.png b/Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/InstantViewBrightnessMinIcon@2x.png new file mode 100644 index 0000000000..4556a3e034 Binary files /dev/null and b/Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/InstantViewBrightnessMinIcon@2x.png differ diff --git a/Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/InstantViewBrightnessMinIcon@3x.png b/Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/InstantViewBrightnessMinIcon@3x.png new file mode 100644 index 0000000000..0f04c3e727 Binary files /dev/null and b/Images.xcassets/Instant View/SettingsBrightnessMinIcon.imageset/InstantViewBrightnessMinIcon@3x.png differ diff --git a/Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/Contents.json b/Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/Contents.json new file mode 100644 index 0000000000..01287f725c --- /dev/null +++ b/Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/InstantViewFontMaxIcon@2x.png b/Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/InstantViewFontMaxIcon@2x.png new file mode 100644 index 0000000000..919c7ecac9 Binary files /dev/null and b/Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/InstantViewFontMaxIcon@2x.png differ diff --git a/Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/InstantViewFontMaxIcon@3x.png b/Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/InstantViewFontMaxIcon@3x.png new file mode 100644 index 0000000000..b9116ff11d Binary files /dev/null and b/Images.xcassets/Instant View/SettingsFontMaxIcon.imageset/InstantViewFontMaxIcon@3x.png differ diff --git a/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/Contents.json b/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/Contents.json new file mode 100644 index 0000000000..49aa90fa7a --- /dev/null +++ b/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/InstantViewFontMinIcon@2x.png b/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/InstantViewFontMinIcon@2x.png new file mode 100644 index 0000000000..eae01e16f5 Binary files /dev/null and b/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/InstantViewFontMinIcon@2x.png differ diff --git a/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/InstantViewFontMinIcon@3x.png b/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/InstantViewFontMinIcon@3x.png new file mode 100644 index 0000000000..d6dbb0fe13 Binary files /dev/null and b/Images.xcassets/Instant View/SettingsFontMinIcon.imageset/InstantViewFontMinIcon@3x.png differ diff --git a/Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@2x.png b/Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@2x.png deleted file mode 100644 index 48c1617c4b..0000000000 Binary files a/Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@2x.png and /dev/null differ diff --git a/Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@3x.png b/Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@3x.png deleted file mode 100644 index 736367a108..0000000000 Binary files a/Images.xcassets/Instant View/SettingsIcon.imageset/InstantViewSettingsIcon@3x.png and /dev/null differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index ec229afe2e..fd700af66c 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -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 = ""; }; D00DE6AC1E8EB2D4003F0D76 /* ShareActionButtonNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareActionButtonNode.swift; sourceTree = ""; }; D00E15251DDBD4E700ACF65C /* LegacyCamera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCamera.swift; sourceTree = ""; }; + D00FF2081F4E2414006FA332 /* InstantPageSettingsNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsNode.swift; sourceTree = ""; }; + D0104F271F47171F004E4881 /* InstantPageGalleryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageGalleryController.swift; sourceTree = ""; }; + D0104F291F471DA6004E4881 /* InstantImageGalleryItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantImageGalleryItem.swift; sourceTree = ""; }; + D0104F2B1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageGalleryFooterContentNode.swift; sourceTree = ""; }; D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatChannelSubscriberInputPanelNode.swift; sourceTree = ""; }; D010C2C91EA7A59F00F41B96 /* PresentationThemeSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationThemeSettings.swift; sourceTree = ""; }; D010C2CB1EA7D74800F41B96 /* DefaultPresentationTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultPresentationTheme.swift; sourceTree = ""; }; @@ -963,8 +990,9 @@ D01C2AAA1E75E010001F6F9A /* TwoStepVerificationUnlockController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationUnlockController.swift; sourceTree = ""; }; D01C2AAC1E768404001F6F9A /* Markdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Markdown.swift; sourceTree = ""; }; D01C7EFF1EF9D45B008305F1 /* DeviceContactsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceContactsManager.swift; sourceTree = ""; }; + D01C99771F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsItemTheme.swift; sourceTree = ""; }; D01D6BFB1E42AB3C006151C6 /* EmojiUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiUtils.swift; sourceTree = ""; }; - D01F66121DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingButton.swift; sourceTree = ""; }; + D01F66121DE8903300345CBE /* ChatTextInputMediaRecordingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputMediaRecordingButton.swift; sourceTree = ""; }; D0215D371E040F53001A0B1E /* InstantPageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageNode.swift; sourceTree = ""; }; D0215D391E041003001A0B1E /* InstantPageLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageLayout.swift; sourceTree = ""; }; D0215D3B1E041014001A0B1E /* InstantPageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageItem.swift; sourceTree = ""; }; @@ -974,8 +1002,8 @@ D0215D431E0413FB001A0B1E /* InstantPageTextStyleStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageTextStyleStack.swift; sourceTree = ""; }; D0215D451E041851001A0B1E /* InstantPageTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageTextItem.swift; sourceTree = ""; }; D0215D471E041B90001A0B1E /* InstantPageAnchorItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageAnchorItem.swift; sourceTree = ""; }; - D0215D491E041CAF001A0B1E /* InstantPageMediaItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageMediaItem.swift; sourceTree = ""; }; - D0215D4B1E041D5E001A0B1E /* InstantPageMediaNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageMediaNode.swift; sourceTree = ""; }; + D0215D491E041CAF001A0B1E /* InstantPageImageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageImageItem.swift; sourceTree = ""; }; + D0215D4B1E041D5E001A0B1E /* InstantPageImageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageImageNode.swift; sourceTree = ""; }; D0215D4D1E042164001A0B1E /* InstantPageWebEmbedNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageWebEmbedNode.swift; sourceTree = ""; }; D0215D4F1E0422C7001A0B1E /* InstantPageWebEmbedItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageWebEmbedItem.swift; sourceTree = ""; }; D0215D511E0423EE001A0B1E /* InstantPageShapeItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageShapeItem.swift; sourceTree = ""; }; @@ -1045,6 +1073,12 @@ D0471B631EFEB5CB0074D609 /* BotPaymentItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotPaymentItemNode.swift; sourceTree = ""; }; D04791661E79A22000F18979 /* ItemListStickerPackItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListStickerPackItem.swift; sourceTree = ""; }; D0486F091E523C8500091F0C /* GroupInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInfoController.swift; sourceTree = ""; }; + D048EA841F4F295300188713 /* InstantPageSettingsBacklightItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsBacklightItemNode.swift; sourceTree = ""; }; + D048EA861F4F296400188713 /* InstantPageSettingsFontSizeItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsFontSizeItemNode.swift; sourceTree = ""; }; + D048EA881F4F297500188713 /* InstantPageSettingsFontFamilyItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsFontFamilyItemNode.swift; sourceTree = ""; }; + D048EA8A1F4F298A00188713 /* InstantPageSettingsThemeItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsThemeItemNode.swift; sourceTree = ""; }; + D048EA8C1F4F299A00188713 /* InstantPageSettingsSwitchItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsSwitchItemNode.swift; sourceTree = ""; }; + D048EA8E1F4F2A9C00188713 /* InstantPageSettingsItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsItemNode.swift; sourceTree = ""; }; D049EAE11E447AD500A2CD3A /* HorizontalStickersChatContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalStickersChatContextPanelNode.swift; sourceTree = ""; }; D049EAE31E44949F00A2CD3A /* HorizontalStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalStickerGridItem.swift; sourceTree = ""; }; D049EAE51E44AD5600A2CD3A /* ChatMediaInputMetaSectionItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputMetaSectionItemNode.swift; sourceTree = ""; }; @@ -1145,6 +1179,8 @@ D0561DE01E57153000E6B9E9 /* ItemListActivityTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListActivityTextItem.swift; sourceTree = ""; }; D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListMultilineTextItem.swift; sourceTree = ""; }; D0561DE71E574C3200E6B9E9 /* ChannelAdminsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelAdminsController.swift; sourceTree = ""; }; + D05677501F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePeerReferenceItem.swift; sourceTree = ""; }; + D05677521F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePeerReferenceNode.swift; sourceTree = ""; }; D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveformNode.swift; sourceTree = ""; }; D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = ""; }; D0575AEA1E9FD579006F2541 /* ChatListTitleLockView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListTitleLockView.swift; sourceTree = ""; }; @@ -1172,6 +1208,7 @@ D0642EFB1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryNavigationButtons.swift; sourceTree = ""; }; D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = ""; }; D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = ""; }; + D06BB8811F58994B0084FC30 /* LegacyInstantVideoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyInstantVideoController.swift; sourceTree = ""; }; D06E4AC31E84806300627D1D /* FetchPhotoLibraryImageResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchPhotoLibraryImageResource.swift; sourceTree = ""; }; D06FFBA71EAFAC4F00CB53D4 /* PresentationThemeEssentialGraphics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationThemeEssentialGraphics.swift; sourceTree = ""; }; D06FFBA91EAFAD2500CB53D4 /* PresentationResourcesChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationResourcesChat.swift; sourceTree = ""; }; @@ -1213,6 +1250,7 @@ D07CFF7C1DCA273400761F81 /* ChatListViewTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListViewTransition.swift; sourceTree = ""; }; D07CFF7E1DCA308500761F81 /* ChatListNodeLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListNodeLocation.swift; sourceTree = ""; }; D07CFF861DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForwardAccessoryPanelNode.swift; sourceTree = ""; }; + D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageManagedMediaId.swift; sourceTree = ""; }; D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListEditableDeleteControlNode.swift; sourceTree = ""; }; D08774F91E3E2A5600A97350 /* ItemListCheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListCheckboxItem.swift; sourceTree = ""; }; D08775081E3E59DE00A97350 /* PeerNotificationSoundStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerNotificationSoundStrings.swift; sourceTree = ""; }; @@ -1224,6 +1262,7 @@ D087751B1E3F542500A97350 /* ContactMultiselectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactMultiselectionControllerNode.swift; sourceTree = ""; }; D087751D1E3F579300A97350 /* CounterContollerTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CounterContollerTitleView.swift; sourceTree = ""; }; D087751F1E3F595000A97350 /* ContactListActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactListActionItem.swift; sourceTree = ""; }; + D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePresentationSettings.swift; sourceTree = ""; }; D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputPanelEntries.swift; sourceTree = ""; }; D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputGridEntries.swift; sourceTree = ""; }; D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputStickerGridItem.swift; sourceTree = ""; }; @@ -1267,6 +1306,8 @@ D0ACCB1B1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageCallBubbleContentNode.swift; sourceTree = ""; }; D0AF7C451ED84BC500CD8E0F /* LanguageSelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageSelectionController.swift; sourceTree = ""; }; D0AF7C491ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageSelectionControllerNode.swift; sourceTree = ""; }; + D0AFCC781F4C8D2C000720C6 /* InstantPageSlideshowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSlideshowItem.swift; sourceTree = ""; }; + D0AFCC7A1F4C8D39000720C6 /* InstantPageSlideshowItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSlideshowItemNode.swift; sourceTree = ""; }; D0B417C21D7DE54E004562A4 /* ChatPresentationInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPresentationInterfaceState.swift; sourceTree = ""; }; 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 = ""; }; @@ -1295,6 +1336,8 @@ D0C0B5B01EE1C421000F4D2C /* ChatDateSelectionSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatDateSelectionSheet.swift; sourceTree = ""; }; D0C0B5B61EE1DEF1000F4D2C /* ThemeGridControllerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeGridControllerItem.swift; sourceTree = ""; }; D0C12A1B1F33964900B3F66D /* ChatWallpaperBuiltin0.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = ChatWallpaperBuiltin0.jpg; path = TelegramUI/Resources/ChatWallpaperBuiltin0.jpg; sourceTree = ""; }; + D0C27B3A1F4B453700A4E170 /* InstantPagePlayableVideoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPagePlayableVideoItem.swift; sourceTree = ""; }; + D0C27B3C1F4B454800A4E170 /* InstantPagePlayableVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPagePlayableVideoNode.swift; sourceTree = ""; }; D0C48F431E81D5110075317D /* ChatEmptyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatEmptyItem.swift; sourceTree = ""; }; D0C50DE81E93A07900F62E39 /* libtgvoip.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = libtgvoip.framework; path = "../libtgvoip/build/Debug-iphoneos/libtgvoip.framework"; sourceTree = ""; }; 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 = ""; }; @@ -1984,6 +2027,9 @@ D0FE4DDF1F0ACA8300E8A0B3 /* InstantVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantVideoNode.swift; sourceTree = ""; }; D0FE4DE31F0AEBB900E8A0B3 /* SharedVideoContextManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedVideoContextManager.swift; sourceTree = ""; }; D0FE4DE51F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayMediaItemNode.swift; sourceTree = ""; }; + D0FFF7F51F55B82500BEBC01 /* InstantPageAudioItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageAudioItem.swift; sourceTree = ""; }; + D0FFF7F71F55B83600BEBC01 /* InstantPageAudioNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageAudioNode.swift; sourceTree = ""; }; + D0FFF7FE1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageMediaAudioPlaylist.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2046,6 +2092,14 @@ name = Share; sourceTree = ""; }; + D0104F261F471702004E4881 /* Instant Page Gallery */ = { + isa = PBXGroup; + children = ( + D0104F271F47171F004E4881 /* InstantPageGalleryController.swift */, + ); + name = "Instant Page Gallery"; + sourceTree = ""; + }; 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 = ""; @@ -2563,6 +2634,7 @@ D0223A911EA5420C00211D94 /* GeneratedMediaStoreSettings.swift */, D0223A931EA5442C00211D94 /* VoiceCallSettings.swift */, D010C2C91EA7A59F00F41B96 /* PresentationThemeSettings.swift */, + D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */, ); name = Settings; sourceTree = ""; @@ -2680,6 +2752,7 @@ D09E637B1F0E7C28003444CD /* SharedMediaPlayer.swift */, D09E637E1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift */, D0736F221DF496D000F2C02A /* PeerMediaAudioPlaylist.swift */, + D0FFF7FE1F55C55800BEBC01 /* InstantPageMediaAudioPlaylist.swift */, ); name = "Shared Media Player"; sourceTree = ""; @@ -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 = ""; @@ -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; diff --git a/TelegramUI/AuthorizationSequenceController.swift b/TelegramUI/AuthorizationSequenceController.swift index 665f2b0cb4..1a90302cea 100644 --- a/TelegramUI/AuthorizationSequenceController.swift +++ b/TelegramUI/AuthorizationSequenceController.swift @@ -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: diff --git a/TelegramUI/AutomaticMediaDownloadSettings.swift b/TelegramUI/AutomaticMediaDownloadSettings.swift index ea4bc63d8e..7417973018 100644 --- a/TelegramUI/AutomaticMediaDownloadSettings.swift +++ b/TelegramUI/AutomaticMediaDownloadSettings.swift @@ -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") } diff --git a/TelegramUI/BotCheckoutInfoControllerNode.swift b/TelegramUI/BotCheckoutInfoControllerNode.swift index 99cf284d76..585d3919cf 100644 --- a/TelegramUI/BotCheckoutInfoControllerNode.swift +++ b/TelegramUI/BotCheckoutInfoControllerNode.swift @@ -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()) }) } } diff --git a/TelegramUI/CallListControllerNode.swift b/TelegramUI/CallListControllerNode.swift index a6e3c6fdeb..e042abc707 100644 --- a/TelegramUI/CallListControllerNode.swift +++ b/TelegramUI/CallListControllerNode.swift @@ -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) } diff --git a/TelegramUI/ChatButtonKeyboardInputNode.swift b/TelegramUI/ChatButtonKeyboardInputNode.swift index 9221e036e7..47e900a15f 100644 --- a/TelegramUI/ChatButtonKeyboardInputNode.swift +++ b/TelegramUI/ChatButtonKeyboardInputNode.swift @@ -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 } diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 7f9fe92f15..7ce8d514bd 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -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() private var audioRecorderDisposable: Disposable? + private var videoRecorderValue: InstantVideoController? + private var tempVideoRecorderValue: InstantVideoController? + private var videoRecorder = Promise() + 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) { diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 1235008417..b466e7552b 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -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 + } } diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index 88e64adddd..a6d12630ab 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -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? { diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index e5859838d7..848c43a201 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -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 + } } diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index 32faf70ac5..9638320208 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -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)) } } } diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index bd20737127..3009d3511e 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -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) } } } diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index eee12a0546..b3eb013e5a 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -2,7 +2,7 @@ import Foundation import Postbox import TelegramCore -struct ChatInterfaceSelectionState: Coding, Equatable { +struct ChatInterfaceSelectionState: PostboxCoding, Equatable { let selectedIds: Set 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 @@ -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) } } diff --git a/TelegramUI/ChatInterfaceStateInputPanels.swift b/TelegramUI/ChatInterfaceStateInputPanels.swift index 627186d809..a9135aa5e3 100644 --- a/TelegramUI/ChatInterfaceStateInputPanels.swift +++ b/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -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 diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 56f845659b..2915324202 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -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 { diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index 1acc03ec5a..a0764a252c 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -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) } diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index 15beb4890f..0ce882870e 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -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) } } } diff --git a/TelegramUI/ChatMessageInstantVideoItemNode.swift b/TelegramUI/ChatMessageInstantVideoItemNode.swift index 31efabb756..36cec2582f 100644 --- a/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -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? 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() + } + }) + } } })) } diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index b31fb7de38..732f281604 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -2,6 +2,7 @@ import Foundation import Postbox import SwiftSignalKit import TelegramCore +import Display final class ChatPanelInterfaceInteractionStatuses { let editingMessage: Signal @@ -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 } } diff --git a/TelegramUI/ChatTextInputAudioRecordingButton.swift b/TelegramUI/ChatTextInputAudioRecordingButton.swift deleted file mode 100644 index c67f4e8205..0000000000 --- a/TelegramUI/ChatTextInputAudioRecordingButton.swift +++ /dev/null @@ -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() - } - } -} diff --git a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift index 1e05311d40..51bcce420c 100644 --- a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift +++ b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift @@ -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() + } } diff --git a/TelegramUI/ChatTextInputMediaRecordingButton.swift b/TelegramUI/ChatTextInputMediaRecordingButton.swift new file mode 100644 index 0000000000..840f5f34fd --- /dev/null +++ b/TelegramUI/ChatTextInputMediaRecordingButton.swift @@ -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) + } + } +} diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 731867f93c..5ba85cf1cb 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -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) + } } diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index aada66e8b0..50b56c4653 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -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 }) } } diff --git a/TelegramUI/DefaultDarkPresentationTheme.swift b/TelegramUI/DefaultDarkPresentationTheme.swift index a32b5b1501..736ac0cc58 100644 --- a/TelegramUI/DefaultDarkPresentationTheme.swift +++ b/TelegramUI/DefaultDarkPresentationTheme.swift @@ -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( diff --git a/TelegramUI/DefaultPresentationTheme.swift b/TelegramUI/DefaultPresentationTheme.swift index 733956a613..68a01a7105 100644 --- a/TelegramUI/DefaultPresentationTheme.swift +++ b/TelegramUI/DefaultPresentationTheme.swift @@ -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( diff --git a/TelegramUI/FetchCachedRepresentations.swift b/TelegramUI/FetchCachedRepresentations.swift index 194d92c41e..1080198af6 100644 --- a/TelegramUI/FetchCachedRepresentations.swift +++ b/TelegramUI/FetchCachedRepresentations.swift @@ -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 diff --git a/TelegramUI/GalleryControllerNode.swift b/TelegramUI/GalleryControllerNode.swift index 2340535ccf..4c66f92d12 100644 --- a/TelegramUI/GalleryControllerNode.swift +++ b/TelegramUI/GalleryControllerNode.swift @@ -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) diff --git a/TelegramUI/GalleryPagerNode.swift b/TelegramUI/GalleryPagerNode.swift index f9795e44de..f42ef22299 100644 --- a/TelegramUI/GalleryPagerNode.swift +++ b/TelegramUI/GalleryPagerNode.swift @@ -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 } diff --git a/TelegramUI/GeneratedMediaStoreSettings.swift b/TelegramUI/GeneratedMediaStoreSettings.swift index 8f3b65b74a..2ca6918bfa 100644 --- a/TelegramUI/GeneratedMediaStoreSettings.swift +++ b/TelegramUI/GeneratedMediaStoreSettings.swift @@ -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") } diff --git a/TelegramUI/InAppNotificationSettings.swift b/TelegramUI/InAppNotificationSettings.swift index 5aa146e72b..623853ff83 100644 --- a/TelegramUI/InAppNotificationSettings.swift +++ b/TelegramUI/InAppNotificationSettings.swift @@ -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") diff --git a/TelegramUI/InstantImageGalleryItem.swift b/TelegramUI/InstantImageGalleryItem.swift new file mode 100644 index 0000000000..23f879516a --- /dev/null +++ b/TelegramUI/InstantImageGalleryItem.swift @@ -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() + fileprivate let _title = Promise() + 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 { + 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 { + return self._title.get() + } + + override func footerContent() -> Signal { + return .single(self.footerContentNode) + } +} diff --git a/TelegramUI/InstantPageAnchorItem.swift b/TelegramUI/InstantPageAnchorItem.swift index 6feba4fce6..25600dc8b4 100644 --- a/TelegramUI/InstantPageAnchorItem.swift +++ b/TelegramUI/InstantPageAnchorItem.swift @@ -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 [] } diff --git a/TelegramUI/InstantPageAudioItem.swift b/TelegramUI/InstantPageAudioItem.swift new file mode 100644 index 0000000000..476a314d31 --- /dev/null +++ b/TelegramUI/InstantPageAudioItem.swift @@ -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) { + } +} + diff --git a/TelegramUI/InstantPageAudioNode.swift b/TelegramUI/InstantPageAudioNode.swift new file mode 100644 index 0000000000..cb86ace557 --- /dev/null +++ b/TelegramUI/InstantPageAudioNode.swift @@ -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 = applicationContext.mediaManager.filteredPlaylistPlayerStateAndStatus(playlistId: playlistId, itemId: itemId) + |> mapToSignal { status -> Signal 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)) + } +} + diff --git a/TelegramUI/InstantPageController.swift b/TelegramUI/InstantPageController.swift index e8eb0f1b33..0c8cba5c4b 100644 --- a/TelegramUI/InstantPageController.swift +++ b/TelegramUI/InstantPageController.swift @@ -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() + override var ready: Promise { + 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() diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index 333eca95d0..77d620a5ad 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -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() var visibleItemIndices = Set() - var visibleItemLinkIndices = Set() - 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 item in _currentLayoutItemsWithLinks) { - itemIndex++; - CGRect itemFrame = item.frame; - if (CGRectIntersectsRect(itemFrame, visibleBounds)) { - [visibleItemLinkIndices addObject:@(itemIndex)]; - - if (_visibleLinkSelectionViews[@(itemIndex)] == nil) { - NSArray *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 *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];*/ } } diff --git a/TelegramUI/InstantPageGalleryController.swift b/TelegramUI/InstantPageGalleryController.swift new file mode 100644 index 0000000000..4bdf71b8b7 --- /dev/null +++ b/TelegramUI/InstantPageGalleryController.swift @@ -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() + override var ready: Promise { + return self._ready + } + private var didSetReady = false + + private let disposable = MetaDisposable() + + private var entries: [InstantPageGalleryEntry] = [] + private var centralEntryIndex: Int? + + private let centralItemTitle = Promise() + private let centralItemTitleView = Promise() + private let centralItemNavigationStyle = Promise() + private let centralItemFooterContentNode = Promise() + private let centralItemAttributesDisposable = DisposableSet(); + + private let _hiddenMedia = Promise(nil) + var hiddenMedia: Signal { + return self._hiddenMedia.get() + } + + private let replaceRootController: (ViewController, ValuePromise?) -> Void + + init(account: Account, entries: [InstantPageGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, ValuePromise?) -> 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) + } +} diff --git a/TelegramUI/InstantPageGalleryFooterContentNode.swift b/TelegramUI/InstantPageGalleryFooterContentNode.swift new file mode 100644 index 0000000000..9a40e4c4cf --- /dev/null +++ b/TelegramUI/InstantPageGalleryFooterContentNode.swift @@ -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() { + } +} diff --git a/TelegramUI/InstantPageImageItem.swift b/TelegramUI/InstantPageImageItem.swift new file mode 100644 index 0000000000..8928bb06e6 --- /dev/null +++ b/TelegramUI/InstantPageImageItem.swift @@ -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 [] + } +} diff --git a/TelegramUI/InstantPageImageNode.swift b/TelegramUI/InstantPageImageNode.swift new file mode 100644 index 0000000000..df128b06cf --- /dev/null +++ b/TelegramUI/InstantPageImageNode.swift @@ -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) + } + } +} diff --git a/TelegramUI/InstantPageItem.swift b/TelegramUI/InstantPageItem.swift index f92e9eb933..3f0a72f44d 100644 --- a/TelegramUI/InstantPageItem.swift +++ b/TelegramUI/InstantPageItem.swift @@ -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 diff --git a/TelegramUI/InstantPageLayout.swift b/TelegramUI/InstantPageLayout.swift index 7fe74fe270..706b9164ca 100644 --- a/TelegramUI/InstantPageLayout.swift +++ b/TelegramUI/InstantPageLayout.swift @@ -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) diff --git a/TelegramUI/InstantPageLayoutSpacings.swift b/TelegramUI/InstantPageLayoutSpacings.swift index 9b72c62046..11140d2193 100644 --- a/TelegramUI/InstantPageLayoutSpacings.swift +++ b/TelegramUI/InstantPageLayoutSpacings.swift @@ -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 diff --git a/TelegramUI/InstantPageManagedMediaId.swift b/TelegramUI/InstantPageManagedMediaId.swift new file mode 100644 index 0000000000..1502af1bbc --- /dev/null +++ b/TelegramUI/InstantPageManagedMediaId.swift @@ -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 + } + } +} + diff --git a/TelegramUI/InstantPageMediaAudioPlaylist.swift b/TelegramUI/InstantPageMediaAudioPlaylist.swift new file mode 100644 index 0000000000..7182df4352 --- /dev/null +++ b/TelegramUI/InstantPageMediaAudioPlaylist.swift @@ -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) + } + } + }) +} + diff --git a/TelegramUI/InstantPageMediaNode.swift b/TelegramUI/InstantPageMediaNode.swift deleted file mode 100644 index bc617dfb4f..0000000000 --- a/TelegramUI/InstantPageMediaNode.swift +++ /dev/null @@ -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]]; - } - } -}*/ diff --git a/TelegramUI/InstantPageNavigationBar.swift b/TelegramUI/InstantPageNavigationBar.swift index a3242f8121..206b47c0e4 100644 --- a/TelegramUI/InstantPageNavigationBar.swift +++ b/TelegramUI/InstantPageNavigationBar.swift @@ -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) + } } } diff --git a/TelegramUI/InstantPageNode.swift b/TelegramUI/InstantPageNode.swift index a765a32968..1b0aeff187 100644 --- a/TelegramUI/InstantPageNode.swift +++ b/TelegramUI/InstantPageNode.swift @@ -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; diff --git a/TelegramUI/InstantPagePeerReferenceItem.swift b/TelegramUI/InstantPagePeerReferenceItem.swift new file mode 100644 index 0000000000..4c70086867 --- /dev/null +++ b/TelegramUI/InstantPagePeerReferenceItem.swift @@ -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) { + } +} diff --git a/TelegramUI/InstantPagePeerReferenceNode.swift b/TelegramUI/InstantPagePeerReferenceNode.swift new file mode 100644 index 0000000000..0361a548ca --- /dev/null +++ b/TelegramUI/InstantPagePeerReferenceNode.swift @@ -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) + } + } + })) + } + } +} diff --git a/TelegramUI/InstantPageMediaItem.swift b/TelegramUI/InstantPagePlayableVideoItem.swift similarity index 54% rename from TelegramUI/InstantPageMediaItem.swift rename to TelegramUI/InstantPagePlayableVideoItem.swift index aa24c324d3..f0acb02b92 100644 --- a/TelegramUI/InstantPageMediaItem.swift +++ b/TelegramUI/InstantPagePlayableVideoItem.swift @@ -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 [] } } + diff --git a/TelegramUI/InstantPagePlayableVideoNode.swift b/TelegramUI/InstantPagePlayableVideoNode.swift new file mode 100644 index 0000000000..fb91356a26 --- /dev/null +++ b/TelegramUI/InstantPagePlayableVideoNode.swift @@ -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) + } + } +} diff --git a/TelegramUI/InstantPagePresentationSettings.swift b/TelegramUI/InstantPagePresentationSettings.swift new file mode 100644 index 0000000000..77a199bf8d --- /dev/null +++ b/TelegramUI/InstantPagePresentationSettings.swift @@ -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 { + 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) + }) + } +} diff --git a/TelegramUI/InstantPageSettingsBacklightItemNode.swift b/TelegramUI/InstantPageSettingsBacklightItemNode.swift new file mode 100644 index 0000000000..0b5955701a --- /dev/null +++ b/TelegramUI/InstantPageSettingsBacklightItemNode.swift @@ -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 + } +} diff --git a/TelegramUI/InstantPageSettingsFontFamilyItemNode.swift b/TelegramUI/InstantPageSettingsFontFamilyItemNode.swift new file mode 100644 index 0000000000..fc3a80d5e4 --- /dev/null +++ b/TelegramUI/InstantPageSettingsFontFamilyItemNode.swift @@ -0,0 +1,89 @@ +import Foundation +import AsyncDisplayKit +import Display + +private func generateCheckIcon(_ color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(color.cgColor) + context.setLineWidth(2.0) + context.move(to: CGPoint(x: 12.0, y: 1.0)) + context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) + context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) + context.strokePath() + }) +} + +final class InstantPageSettingsFontFamilyNode: InstantPageSettingsItemNode { + private let title: String + private let family: String? + private let tapped: () -> Void + + private let labelNode: ASTextNode + private let checkNode: ASImageNode + + var _checked: Bool + var checked: Bool { + get { + return self._checked + } set(value) { + self._checked = value + self.checkNode.isHidden = !value + } + } + + init(theme: InstantPageSettingsItemTheme, title: String, family: String?, checked: Bool, tapped: @escaping () -> Void) { + self.title = title + self.family = family + self._checked = checked + self.tapped = tapped + + self.labelNode = ASTextNode() + + self.checkNode = ASImageNode() + self.checkNode.displayWithoutProcessing = true + self.checkNode.displaysAsynchronously = false + self.checkNode.isHidden = !checked + + super.init(theme: theme, selectable: true) + + self.addSubnode(self.labelNode) + self.addSubnode(self.checkNode) + + self.updateTheme(theme) + } + + override func updateTheme(_ theme: InstantPageSettingsItemTheme) { + super.updateTheme(theme) + + let font: UIFont + if let family = self.family { + if let familyFont = UIFont(name: family, size: 17.0) { + font = familyFont + } else { + font = UIFont.systemFont(ofSize: 17.0) + } + } else { + font = UIFont.systemFont(ofSize: 17.0) + } + self.labelNode.attributedText = NSAttributedString(string: self.title, font: font, textColor: theme.primaryColor) + self.checkNode.image = generateCheckIcon(theme.accentColor) + } + + override func updateInternalLayout(width: CGFloat, insets: UIEdgeInsets, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> (height: CGFloat, separatorInset: CGFloat?) { + var separatorInset: CGFloat? + if case .sameSection = previousItem.0, let previousNode = previousItem.1, previousNode is InstantPageSettingsFontFamilyNode { + separatorInset = 46.0 + } + let labelSize = self.labelNode.measure(CGSize(width: width - 46.0 - 5.0, height: 44.0)) + self.labelNode.frame = CGRect(origin: CGPoint(x: 46.0, y: insets.top + floor((44.0 - labelSize.height) / 2.0)), size: labelSize) + if let image = self.checkNode.image { + self.checkNode.frame = CGRect(origin: CGPoint(x: 16.0, y: insets.top + floor((44.0 - image.size.height) / 2.0)), size: image.size) + } + return (44.0 + insets.top + insets.bottom, separatorInset) + } + + override func pressed() { + self.tapped() + } +} diff --git a/TelegramUI/InstantPageSettingsFontSizeItemNode.swift b/TelegramUI/InstantPageSettingsFontSizeItemNode.swift new file mode 100644 index 0000000000..86307d881f --- /dev/null +++ b/TelegramUI/InstantPageSettingsFontSizeItemNode.swift @@ -0,0 +1,82 @@ +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 InstantPageSettingsFontSizeItemNode: InstantPageSettingsItemNode { + private let updated: (Int) -> Void + + private let sliderView: TGPhotoEditorSliderView + private let leftIconNode: ASImageNode + private let rightIconNode: ASImageNode + + init(theme: InstantPageSettingsItemTheme, fontSizeVariant: Int, updated: @escaping (Int) -> Void) { + self.updated = updated + + self.sliderView = TGPhotoEditorSliderView() + self.sliderView.trackCornerRadius = 1.0 + self.sliderView.lineSize = 2.0 + self.sliderView.dotSize = 5.0 + self.sliderView.minimumValue = 0.0 + self.sliderView.maximumValue = 4.0 + self.sliderView.startValue = 0.0 + self.sliderView.value = CGFloat(fontSizeVariant) + self.sliderView.positionsCount = 5 + 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.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/SettingsFontMinIcon"), color: theme.primaryColor) + self.rightIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMaxIcon"), 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: 18.0, y: 25.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 - 14.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() { + self.updated(max(0, min(4, Int(self.sliderView.value)))) + } +} diff --git a/TelegramUI/InstantPageSettingsItemNode.swift b/TelegramUI/InstantPageSettingsItemNode.swift new file mode 100644 index 0000000000..c191276679 --- /dev/null +++ b/TelegramUI/InstantPageSettingsItemNode.swift @@ -0,0 +1,125 @@ +import Foundation +import AsyncDisplayKit +import Display + +enum InstantPageSettingsItemNodeStatus { + case none + case sameSection + case otherSection +} + +class InstantPageSettingsItemNode: ASDisplayNode { + private let topSeparatorNode: ASDisplayNode + private let bottomSeparatorNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode? + private let highlightButtonNode: HighlightTrackingButtonNode? + + init(theme: InstantPageSettingsItemTheme, selectable: Bool) { + self.topSeparatorNode = ASDisplayNode() + self.topSeparatorNode.isLayerBacked = true + self.topSeparatorNode.isHidden = true + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + self.bottomSeparatorNode.isHidden = true + + if selectable { + let highlightedBackgroundNode = ASDisplayNode() + highlightedBackgroundNode.isLayerBacked = true + highlightedBackgroundNode.alpha = 0.0 + self.highlightedBackgroundNode = highlightedBackgroundNode + self.highlightButtonNode = HighlightTrackingButtonNode() + } else { + self.highlightedBackgroundNode = nil + self.highlightButtonNode = nil + } + + super.init() + + self.backgroundColor = theme.itemBackgroundColor + + self.addSubnode(self.topSeparatorNode) + self.addSubnode(self.bottomSeparatorNode) + if let highlightedBackgroundNode = self.highlightedBackgroundNode { + self.addSubnode(highlightedBackgroundNode) + } + if let highlightButtonNode = self.highlightButtonNode { + self.addSubnode(highlightButtonNode) + highlightButtonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + highlightButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self, let highlightedBackgroundNode = strongSelf.highlightedBackgroundNode { + if highlighted { + strongSelf.supernode?.view.bringSubview(toFront: strongSelf.view) + highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") + highlightedBackgroundNode.alpha = 1.0 + } else { + highlightedBackgroundNode.alpha = 0.0 + highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + } + } + + self.updateTheme(theme) + } + + func updateTheme(_ theme: InstantPageSettingsItemTheme) { + self.backgroundColor = theme.itemBackgroundColor + self.highlightedBackgroundNode?.backgroundColor = theme.itemHighlightedBackgroundColor + self.topSeparatorNode.backgroundColor = theme.separatorColor + self.bottomSeparatorNode.backgroundColor = theme.separatorColor + } + + func updateInternalLayout(width: CGFloat, insets: UIEdgeInsets, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> (height: CGFloat, separatorInset: CGFloat?) { + return (44.0 + insets.top + insets.bottom, nil) + } + + final func updateLayout(width: CGFloat, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> CGFloat { + let separatorHeight = UIScreenPixel + + let separatorInset: CGFloat = 0.0 + var highlightExtension: CGFloat = 0.0 + switch previousItem.0 { + case .none: + self.topSeparatorNode.isHidden = true + case .sameSection: + self.topSeparatorNode.isHidden = false + case .otherSection: + self.topSeparatorNode.isHidden = false + } + + switch nextItem.0 { + case .none: + self.bottomSeparatorNode.isHidden = true + case .sameSection: + self.bottomSeparatorNode.isHidden = true + highlightExtension = separatorHeight + case .otherSection: + self.bottomSeparatorNode.isHidden = false + } + + let (internalHeight, internalSeparatorInset) = self.updateInternalLayout(width: width, insets: UIEdgeInsets(top: self.topSeparatorNode.isHidden ? 0.0 : separatorHeight, left: 0.0, bottom: self.bottomSeparatorNode.isHidden ? 0.0 : separatorHeight, right: 0.0), previousItem: previousItem, nextItem: nextItem) + + let finalSeparatorInset = internalSeparatorInset ?? separatorInset + + self.topSeparatorNode.frame = CGRect(origin: CGPoint(x: finalSeparatorInset, y: 0.0), size: CGSize(width: width - finalSeparatorInset, height: separatorHeight)) + + self.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: internalHeight - separatorHeight), size: CGSize(width: width, height: separatorHeight)) + + if let highlightButtonNode = self.highlightButtonNode { + highlightButtonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -highlightExtension), size: CGSize(width: width, height: internalHeight + highlightExtension)) + } + if let highlightedBackgroundNode = self.highlightedBackgroundNode { + highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: internalHeight + highlightExtension)) + } + + return internalHeight + } + + @objc func buttonPressed() { + self.pressed() + } + + func pressed() { + } +} diff --git a/TelegramUI/InstantPageSettingsItemTheme.swift b/TelegramUI/InstantPageSettingsItemTheme.swift new file mode 100644 index 0000000000..7aec03baf8 --- /dev/null +++ b/TelegramUI/InstantPageSettingsItemTheme.swift @@ -0,0 +1,101 @@ +import Foundation +import UIKit +import Display + +final class InstantPageSettingsItemTheme: Equatable { + let listBackgroundColor: UIColor + let itemBackgroundColor: UIColor + let itemHighlightedBackgroundColor: UIColor + let separatorColor: UIColor + let primaryColor: UIColor + let secondaryColor: UIColor + let accentColor: UIColor + + init(listBackgroundColor: UIColor, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, separatorColor: UIColor, primaryColor: UIColor, secondaryColor: UIColor, accentColor: UIColor) { + self.listBackgroundColor = listBackgroundColor + self.itemBackgroundColor = itemBackgroundColor + self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor + self.separatorColor = separatorColor + self.primaryColor = primaryColor + self.secondaryColor = secondaryColor + self.accentColor = accentColor + } + + static func ==(lhs: InstantPageSettingsItemTheme, rhs: InstantPageSettingsItemTheme) -> Bool { + if !lhs.listBackgroundColor.isEqual(rhs.listBackgroundColor) { + return false + } + if !lhs.itemBackgroundColor.isEqual(rhs.itemBackgroundColor) { + return false + } + if !lhs.itemHighlightedBackgroundColor.isEqual(rhs.itemHighlightedBackgroundColor) { + return false + } + if !lhs.separatorColor.isEqual(rhs.separatorColor) { + return false + } + if !lhs.primaryColor.isEqual(rhs.primaryColor) { + return false + } + if !lhs.secondaryColor.isEqual(rhs.secondaryColor) { + return false + } + if !lhs.accentColor.isEqual(rhs.accentColor) { + return false + } + return true + } + + static func themeFor(_ settings: InstantPagePresentationSettings) -> InstantPageSettingsItemTheme { + switch settings.themeType { + case .light: + return lightTheme + case .sepia: + return sepiaTheme + case .gray: + return grayTheme + case .dark: + return darkTheme + } + } +} + +private let lightTheme = InstantPageSettingsItemTheme( + listBackgroundColor: UIColor(rgb: 0xefeff4), + itemBackgroundColor: .white, + itemHighlightedBackgroundColor: UIColor(rgb: 0xd9d9d9), + separatorColor: UIColor(rgb: 0xc8c7cc), + primaryColor: .black, + secondaryColor: UIColor(rgb: 0xa8a8a8), + accentColor: UIColor(rgb: 0x007ee5) +) + +private let sepiaTheme = InstantPageSettingsItemTheme( + listBackgroundColor: UIColor(rgb: 0xefeff4), + itemBackgroundColor: .white, + itemHighlightedBackgroundColor: UIColor(rgb: 0xd9d9d9), + separatorColor: UIColor(rgb: 0xc8c7cc), + primaryColor: .black, + secondaryColor: UIColor(rgb: 0xb7b7b7), + accentColor: UIColor(rgb: 0xb06900) +) + +private let grayTheme = InstantPageSettingsItemTheme( + listBackgroundColor: UIColor(rgb: 0xefeff4), + itemBackgroundColor: .white, + itemHighlightedBackgroundColor: UIColor(rgb: 0xd9d9d9), + separatorColor: UIColor(rgb: 0xc8c7cc), + primaryColor: .black, + secondaryColor: UIColor(rgb: 0xb6b6b6), + accentColor: UIColor(rgb: 0xc7c7c7) +) + +private let darkTheme = InstantPageSettingsItemTheme( + listBackgroundColor: UIColor(rgb: 0x232323), + itemBackgroundColor: UIColor(rgb: 0x1a1a1a), + itemHighlightedBackgroundColor: UIColor(rgb: 0x4c4c4c), + separatorColor: UIColor(rgb: 0x151515), + primaryColor: UIColor(rgb: 0x878787), + secondaryColor: UIColor(rgb: 0xa6a6a6), + accentColor: UIColor(rgb: 0xbfc0c2) +) diff --git a/TelegramUI/InstantPageSettingsNode.swift b/TelegramUI/InstantPageSettingsNode.swift new file mode 100644 index 0000000000..34427ca7bc --- /dev/null +++ b/TelegramUI/InstantPageSettingsNode.swift @@ -0,0 +1,241 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import SwiftSignalKit + +private func generateArrowImage(color: UIColor) -> UIImage? { + let smallRadius: CGFloat = 5.0 + let largeRadius: CGFloat = 14.0 + return generateImage(CGSize(width: smallRadius + largeRadius, height: smallRadius + largeRadius + 16.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsArrow"), color: color), let cgImage = image.cgImage { + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height - image.size.height - 16.0), size: CGSize(width: size.width, height: 16.0))) + context.draw(cgImage, in: CGRect(origin: CGPoint(x: size.width - image.size.width, y: size.height - image.size.height), size: image.size)) + } + }) +} + +final class InstantPageSettingsNode: ASDisplayNode { + private var settings: InstantPagePresentationSettings + private var theme: InstantPageSettingsItemTheme + + private let applySettings: (InstantPagePresentationSettings) -> Void + + private var sections: [[InstantPageSettingsItemNode]] = [] + private let sansFamilyNode: InstantPageSettingsFontFamilyNode + private let serifFamilyNode: InstantPageSettingsFontFamilyNode + private let themeItemNode: InstantPageSettingsThemeItemNode + private let autoNightItemNode: InstantPageSettingsSwitchNode + + private let arrowNode: ASImageNode + private let itemContainerNode: ASDisplayNode + + init(strings: PresentationStrings, settings: InstantPagePresentationSettings, applySettings: @escaping (InstantPagePresentationSettings) -> Void) { + self.settings = settings + self.theme = InstantPageSettingsItemTheme.themeFor(settings) + + self.applySettings = applySettings + + self.arrowNode = ASImageNode() + self.arrowNode.displayWithoutProcessing = true + self.arrowNode.displaysAsynchronously = false + self.arrowNode.image = generateArrowImage(color: self.theme.itemBackgroundColor) + + self.itemContainerNode = ASDisplayNode() + self.itemContainerNode.layer.masksToBounds = true + self.itemContainerNode.layer.cornerRadius = 16.0 + self.itemContainerNode.backgroundColor = self.theme.listBackgroundColor + + var updateSerifImpl: ((Bool) -> Void)? + var updateThemeTypeImpl: ((InstantPageThemeType) -> Void)? + var updateAutoNightImpl: ((Bool) -> Void)? + + self.sansFamilyNode = InstantPageSettingsFontFamilyNode(theme: self.theme, title: "San Francisco", family: nil, checked: !settings.forceSerif, tapped: { + updateSerifImpl?(false) + }) + self.serifFamilyNode = InstantPageSettingsFontFamilyNode(theme: self.theme, title: "Georgia", family: "Georgia", checked: settings.forceSerif, tapped: { + updateSerifImpl?(true) + }) + self.themeItemNode = InstantPageSettingsThemeItemNode(theme: theme, themeType: settings.themeType, update: { value in + updateThemeTypeImpl?(value) + }) + self.autoNightItemNode = InstantPageSettingsSwitchNode(theme: theme, title: strings.InstantPage_AutoNightTheme, isOn: settings.autoNightMode, isEnabled: settings.themeType != .dark, toggled: { value in + updateAutoNightImpl?(value) + }) + + super.init() + + self.addSubnode(self.arrowNode) + self.addSubnode(self.itemContainerNode) + + self.sections = [ + [ + InstantPageSettingsBacklightItemNode(theme: self.theme) + ], + [ + InstantPageSettingsFontSizeItemNode(theme: self.theme, fontSizeVariant: Int(settings.fontSize.rawValue), updated: { [weak self] value in + if let strongSelf = self { + strongSelf.updateSettings { + let size: InstantPagePresentationFontSize = InstantPagePresentationFontSize(rawValue: Int32(value)) ?? .standard + return $0.withUpdatedFontSize(size) + } + } + }), + self.sansFamilyNode, + self.serifFamilyNode + ], + [ + self.themeItemNode, + self.autoNightItemNode + ] + ] + + for section in self.sections { + for item in section { + self.itemContainerNode.addSubnode(item) + } + } + + updateSerifImpl = { [weak self] value in + if let strongSelf = self { + strongSelf.updateSettings { + return $0.withUpdatedForceSerif(value) + } + } + } + + updateThemeTypeImpl = { [weak self] value in + if let strongSelf = self { + strongSelf.updateSettings { + return $0.withUpdatedThemeType(value) + } + } + } + + updateAutoNightImpl = { [weak self] value in + if let strongSelf = self { + strongSelf.updateSettings { + return $0.withUpdatedAutoNightMode(value) + } + } + } + } + + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + let fixedWidth: CGFloat = 295.0 + let sectionSpacing: CGFloat = 4.0 + let sideInset: CGFloat = 11.0 + let topInset: CGFloat = layout.insets(options: [.statusBar]).top + 44.0 + 6.0 + + var contentHeight: CGFloat = 0.0 + var itemSizes: [[CGFloat]] = [] + for sectionIndex in 0 ..< self.sections.count { + itemSizes.append([]) + if sectionIndex != 0 { + contentHeight += sectionSpacing + } + for itemIndex in 0 ..< self.sections[sectionIndex].count { + let previousItem: InstantPageSettingsItemNodeStatus + var previousItemNode: InstantPageSettingsItemNode? + let nextItem: InstantPageSettingsItemNodeStatus + var nextItemNode: InstantPageSettingsItemNode? + if itemIndex == 0 { + if sectionIndex == 0 { + previousItem = .none + } else { + previousItem = .otherSection + } + } else { + previousItem = .sameSection + previousItemNode = self.sections[sectionIndex][itemIndex - 1] + } + if itemIndex == self.sections[sectionIndex].count - 1 { + if sectionIndex == self.sections.count - 1 { + nextItem = .none + } else { + nextItem = .otherSection + } + } else { + nextItem = .sameSection + nextItemNode = self.sections[sectionIndex][itemIndex + 1] + } + let itemHeight = self.sections[sectionIndex][itemIndex].updateLayout(width: fixedWidth, previousItem: (previousItem, previousItemNode), nextItem: (nextItem, nextItemNode)) + itemSizes[sectionIndex].append(itemHeight) + contentHeight += itemHeight + } + } + + if let image = self.arrowNode.image { + transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width, y: topInset - image.size.height + 16.0 + 8.0), size: image.size)) + } + + transition.updateFrame(node: self.itemContainerNode, frame: CGRect(origin: CGPoint(x: layout.size.width - sideInset - fixedWidth, y: topInset), size: CGSize(width: fixedWidth, height: contentHeight))) + var nextItemOffset: CGFloat = 0.0 + for sectionIndex in 0 ..< self.sections.count { + if sectionIndex != 0 { + nextItemOffset += sectionSpacing + } + for itemIndex in 0 ..< self.sections[sectionIndex].count { + let itemHeight = itemSizes[sectionIndex][itemIndex] + transition.updateFrame(node: self.sections[sectionIndex][itemIndex], frame: CGRect(origin: CGPoint(x: 0.0, y: nextItemOffset), size: CGSize(width: fixedWidth, height: itemHeight))) + nextItemOffset += itemHeight + } + } + } + + func animateIn() { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + + func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + private func updateSettings(_ f: (InstantPagePresentationSettings) -> InstantPagePresentationSettings) { + let updated = f(self.settings) + if updated != self.settings { + self.settings = updated + + self.sansFamilyNode.checked = !self.settings.forceSerif + self.serifFamilyNode.checked = self.settings.forceSerif + self.themeItemNode.themeType = self.settings.themeType + self.autoNightItemNode.isEnabled = self.settings.themeType != .dark + + let theme = InstantPageSettingsItemTheme.themeFor(self.settings) + if theme != self.theme { + self.theme = theme + + if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) { + self.view.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + self.arrowNode.image = generateArrowImage(color: self.theme.itemBackgroundColor) + self.itemContainerNode.backgroundColor = self.theme.listBackgroundColor + for section in self.sections { + for item in section { + item.updateTheme(self.theme) + } + } + + + } + + self.applySettings(settings) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.itemContainerNode.frame.contains(point) { + return super.hitTest(point, with: event) + } else { + return nil + } + } +} diff --git a/TelegramUI/InstantPageSettingsSwitchItemNode.swift b/TelegramUI/InstantPageSettingsSwitchItemNode.swift new file mode 100644 index 0000000000..c345c8ec91 --- /dev/null +++ b/TelegramUI/InstantPageSettingsSwitchItemNode.swift @@ -0,0 +1,86 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class InstantPageSettingsSwitchNode: InstantPageSettingsItemNode { + private let title: String + private let toggled: (Bool) -> Void + + private let labelNode: ASTextNode + private let switchNode: SwitchNode + + var isOn: Bool { + didSet { + if self.isEnabled && self.isOn != self.switchNode.isOn { + self.switchNode.setOn(self.isOn, animated: true) + } + } + } + + var isEnabled: Bool { + didSet { + if self.isEnabled { + self.switchNode.setOn(self.isOn, animated: true) + self.switchNode.allowsGroupOpacity = false + self.switchNode.alpha = 1.0 + } else { + self.switchNode.setOn(false, animated: true) + self.switchNode.allowsGroupOpacity = true + self.switchNode.alpha = 0.6 + } + self.switchNode.isUserInteractionEnabled = self.isEnabled + } + } + + init(theme: InstantPageSettingsItemTheme, title: String, isOn: Bool, isEnabled: Bool, toggled: @escaping (Bool) -> Void) { + self.title = title + self.toggled = toggled + + self.labelNode = ASTextNode() + + self.switchNode = SwitchNode() + if isEnabled { + self.switchNode.isOn = isOn + } else { + self.switchNode.isOn = false + self.switchNode.allowsGroupOpacity = true + self.switchNode.alpha = 0.6 + } + + self.isOn = isOn + self.isEnabled = isEnabled + + super.init(theme: theme, selectable: false) + + self.addSubnode(self.labelNode) + self.addSubnode(self.switchNode) + + self.switchNode.valueUpdated = { [weak self] value in + if let strongSelf = self { + strongSelf.isOn = value + toggled(value) + } + } + } + + override func updateTheme(_ theme: InstantPageSettingsItemTheme) { + super.updateTheme(theme) + + self.labelNode.attributedText = NSAttributedString(string: self.title, font: Font.regular(17.0), textColor: theme.primaryColor) + } + + override func updateInternalLayout(width: CGFloat, insets: UIEdgeInsets, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> (height: CGFloat, separatorInset: CGFloat?) { + + let labelSize = self.labelNode.measure(CGSize(width: width - 46.0 - 5.0, height: 44.0)) + self.labelNode.frame = CGRect(origin: CGPoint(x: 15.0, y: insets.top + floor((44.0 - labelSize.height) / 2.0)), size: labelSize) + if let switchView = self.switchNode.view as? UISwitch { + if self.switchNode.bounds.size.width.isZero { + switchView.sizeToFit() + } + let switchSize = switchView.bounds.size + + self.switchNode.frame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0, y: insets.top + 6.0), size: switchSize) + } + return (44.0 + insets.top + insets.bottom, nil) + } +} diff --git a/TelegramUI/InstantPageSettingsThemeItemNode.swift b/TelegramUI/InstantPageSettingsThemeItemNode.swift new file mode 100644 index 0000000000..a2f32fd93c --- /dev/null +++ b/TelegramUI/InstantPageSettingsThemeItemNode.swift @@ -0,0 +1,167 @@ +import Foundation +import AsyncDisplayKit +import Display + +private final class InstantPageSettingsThemeSelectorNode: ASDisplayNode { + private let selectionNode: ASImageNode + private let colorNode: ASImageNode + + private let color: UIColor + + var selected: Bool = false { + didSet { + self.selectionNode.isHidden = !self.selected + } + } + + var selectionColor: UIColor { + didSet { + if !self.selectionColor.isEqual(oldValue) { + self.selectionNode.image = generateFilledCircleImage(diameter: 46.0, color: nil, strokeColor: self.selectionColor, strokeWidth: 2.0, backgroundColor: nil) + } + } + } + + var edgeColor: UIColor { + didSet { + if !self.edgeColor.isEqual(oldValue) { + self.colorNode.image = generateFilledCircleImage(diameter: 46.0, color: self.color, strokeColor: self.edgeColor, strokeWidth: 1.0, backgroundColor: nil) + } + } + } + + init(color: UIColor, edgeColor: UIColor, selectionColor: UIColor) { + self.color = color + self.edgeColor = edgeColor + self.selectionColor = selectionColor + + self.selectionNode = ASImageNode() + self.selectionNode.isLayerBacked = true + self.selectionNode.displayWithoutProcessing = true + self.selectionNode.displaysAsynchronously = false + self.selectionNode.image = generateFilledCircleImage(diameter: 46.0, color: nil, strokeColor: self.selectionColor, strokeWidth: 2.0, backgroundColor: nil) + self.selectionNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 46.0, height: 46.0)) + + self.colorNode = ASImageNode() + self.colorNode.isLayerBacked = true + self.colorNode.displayWithoutProcessing = true + self.colorNode.displaysAsynchronously = false + self.colorNode.image = generateFilledCircleImage(diameter: 46.0, color: self.color, strokeColor: self.edgeColor, strokeWidth: 1.0, backgroundColor: nil) + self.colorNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 46.0, height: 46.0)) + + super.init() + + self.addSubnode(self.colorNode) + self.addSubnode(self.selectionNode) + } +} + +final class InstantPageSettingsThemeItemNode: InstantPageSettingsItemNode { + private let update: (InstantPageThemeType) -> Void + + private let themeNodes: [InstantPageSettingsThemeSelectorNode] + + var themeType: InstantPageThemeType { + didSet { + let selectedIndex: Int + switch self.themeType { + case .light: + selectedIndex = 0 + case .sepia: + selectedIndex = 1 + case .gray: + selectedIndex = 2 + case .dark: + selectedIndex = 3 + } + + self.themeNodes[0].edgeColor = (selectedIndex == 1 || selectedIndex == 2) ? UIColor.lightGray : UIColor.white + + for i in 0 ..< self.themeNodes.count { + self.themeNodes[i].selected = i == selectedIndex + } + } + } + + init(theme: InstantPageSettingsItemTheme, themeType: InstantPageThemeType, update: @escaping (InstantPageThemeType) -> Void) { + self.themeType = themeType + self.update = update + + let selectedIndex: Int + switch themeType { + case .light: + selectedIndex = 0 + case .sepia: + selectedIndex = 1 + case .gray: + selectedIndex = 2 + case .dark: + selectedIndex = 3 + } + + let selectionColor = UIColor(rgb: 0x007ee5) + self.themeNodes = [ + InstantPageSettingsThemeSelectorNode(color: .white, edgeColor: (selectedIndex == 1 || selectedIndex == 2) ? UIColor.lightGray : UIColor.white, selectionColor: selectionColor), + InstantPageSettingsThemeSelectorNode(color: UIColor(rgb: 0xcbb98e), edgeColor: UIColor(rgb: 0xcbb98e), selectionColor: selectionColor), + InstantPageSettingsThemeSelectorNode(color: UIColor(rgb: 0x48484a), edgeColor: UIColor(rgb: 0x48484a), selectionColor: selectionColor), + InstantPageSettingsThemeSelectorNode(color: UIColor(rgb: 0x48484a), edgeColor: UIColor(rgb: 0x48484a), selectionColor: selectionColor) + ] + + super.init(theme: theme, selectable: false) + + for i in 0 ..< self.themeNodes.count { + self.themeNodes[i].selected = i == selectedIndex + self.addSubnode(self.themeNodes[i]) + } + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + override func updateTheme(_ theme: InstantPageSettingsItemTheme) { + super.updateTheme(theme) + } + + override func updateInternalLayout(width: CGFloat, insets: UIEdgeInsets, previousItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?), nextItem: (InstantPageSettingsItemNodeStatus, InstantPageSettingsItemNode?)) -> (height: CGFloat, separatorInset: CGFloat?) { + + let sideInset: CGFloat = 26.0 + let topInset: CGFloat = 12.0 + let itemSize = CGSize(width: 46.0, height: 46.0) + let spacing: CGFloat = floor((width - CGFloat(self.themeNodes.count) * itemSize.width - sideInset * 2.0) / CGFloat(self.themeNodes.count - 1)) + + for i in 0 ..< self.themeNodes.count { + self.themeNodes[i].frame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (itemSize.width + spacing), y: insets.top + topInset), size: itemSize) + } + + return (70.0 + insets.top + insets.bottom, nil) + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let location = recognizer.location(in: self.view) + for i in 0 ..< self.themeNodes.count { + if self.themeNodes[i].frame.contains(location) { + let themeType: InstantPageThemeType + switch i { + case 0: + themeType = .light + case 1: + themeType = .sepia + case 2: + themeType = .gray + case 3: + themeType = .dark + default: + themeType = .light + } + self.update(themeType) + break + } + } + } + } +} + diff --git a/TelegramUI/InstantPageShapeItem.swift b/TelegramUI/InstantPageShapeItem.swift index 472534a801..0fb3e40477 100644 --- a/TelegramUI/InstantPageShapeItem.swift +++ b/TelegramUI/InstantPageShapeItem.swift @@ -1,4 +1,5 @@ import Foundation +import Postbox import TelegramCore enum InstantPageShape { @@ -15,7 +16,6 @@ final class InstantPageShapeItem: InstantPageItem { let medias: [InstantPageMedia] = [] let wantsNode: Bool = false - let hasLinks: Bool = false init(frame: CGRect, shapeFrame: CGRect, shape: InstantPageShape, color: UIColor) { self.frame = frame @@ -55,11 +55,11 @@ final class InstantPageShapeItem: InstantPageItem { return false } - func node(account: Account) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { return nil } - func linkSelectionViews() -> [InstantPageLinkSelectionView] { + func linkSelectionRects(at point: CGPoint) -> [CGRect] { return [] } diff --git a/TelegramUI/InstantPageSlideshowItem.swift b/TelegramUI/InstantPageSlideshowItem.swift new file mode 100644 index 0000000000..4c0937b49d --- /dev/null +++ b/TelegramUI/InstantPageSlideshowItem.swift @@ -0,0 +1,50 @@ +import Foundation +import Postbox +import TelegramCore + +final class InstantPageSlideshowItem: InstantPageItem { + var frame: CGRect + let wantsNode: Bool = true + let medias: [InstantPageMedia] + + init(frame: CGRect, medias: [InstantPageMedia]) { + self.frame = frame + self.medias = medias + } + + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { + return InstantPageSlideshowNode(account: account, medias: self.medias, openMedia: openMedia) + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func matchesNode(_ node: InstantPageNode) -> Bool { + if let node = node as? InstantPageSlideshowNode { + return self.medias == node.medias + } else { + return false + } + } + + func distanceThresholdGroup() -> Int? { + return 3 + } + + 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) { + } +} + diff --git a/TelegramUI/InstantPageSlideshowItemNode.swift b/TelegramUI/InstantPageSlideshowItemNode.swift new file mode 100644 index 0000000000..c2ef6dbea4 --- /dev/null +++ b/TelegramUI/InstantPageSlideshowItemNode.swift @@ -0,0 +1,417 @@ +import Foundation +import TelegramCore +import AsyncDisplayKit +import Display + +private final class InstantPageSlideshowItemNode: ASDisplayNode { + private var _index: Int? + var index: Int { + get { + return self._index! + } set(value) { + self._index = value + } + } + private let contentNode: ASDisplayNode + + var internalIsVisible: Bool = false { + didSet { + if self.internalParentVisible && oldValue != self.internalIsVisible && self.internalParentVisible { + (self.contentNode as? InstantPageNode)?.updateIsVisible(self.internalIsVisible && self.internalParentVisible) + } + } + } + + var internalParentVisible: Bool = false { + didSet { + if self.internalIsVisible && oldValue != self.internalIsVisible && self.internalParentVisible { + (self.contentNode as? InstantPageNode)?.updateIsVisible(self.internalIsVisible && self.internalParentVisible) + } + } + } + + init(contentNode: ASDisplayNode) { + self.contentNode = contentNode + + super.init() + + self.addSubnode(self.contentNode) + } + + override func layout() { + super.layout() + + self.contentNode.frame = self.bounds + } + + func updateHiddenMedia(_ media: InstantPageMedia?) { + if let node = self.contentNode as? InstantPageNode { + node.updateHiddenMedia(media: media) + } + } + + func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + if let node = self.contentNode as? InstantPageNode { + return node.transitionNode(media: media) + } + return nil + } +} + +private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDelegate { + private let account: Account + private let openMedia: (InstantPageMedia) -> Void + private let pageGap: CGFloat + + private let scrollView: UIScrollView + + private var items: [InstantPageMedia] = [] + private var itemNodes: [InstantPageSlideshowItemNode] = [] + private var ignoreCentralItemIndexUpdate = false + private var centralItemIndex: Int? { + didSet { + if oldValue != self.centralItemIndex && !self.ignoreCentralItemIndexUpdate { + //self.centralItemIndexUpdated(self.centralItemIndex) + } + } + } + + private var containerLayout: ContainerViewLayout? + + var centralItemIndexUpdated: (Int?) -> Void = { _ in } + + var internalIsVisible: Bool = false { + didSet { + if self.internalIsVisible != oldValue { + for node in self.itemNodes { + node.internalParentVisible = self.internalIsVisible + } + } + } + } + + init(account: Account, openMedia: @escaping (InstantPageMedia) -> Void, pageGap: CGFloat = 0.0) { + self.account = account + self.openMedia = openMedia + self.pageGap = pageGap + self.scrollView = UIScrollView() + if #available(iOSApplicationExtension 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + + super.init() + + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = !pageGap.isZero + self.scrollView.bounces = !pageGap.isZero + self.scrollView.isPagingEnabled = true + self.scrollView.delegate = self + self.scrollView.clipsToBounds = false + self.scrollView.scrollsToTop = false + self.view.addSubview(self.scrollView) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.containerLayout = layout + + var previousCentralNodeHorizontalOffset: CGFloat? + if let centralItemIndex = self.centralItemIndex, let centralNode = self.visibleItemNode(at: centralItemIndex) { + previousCentralNodeHorizontalOffset = self.scrollView.contentOffset.x - centralNode.frame.minX + } + + self.scrollView.frame = CGRect(origin: CGPoint(x: -self.pageGap, y: 0.0), size: CGSize(width: layout.size.width + self.pageGap * 2.0, height: layout.size.height)) + + for i in 0 ..< self.itemNodes.count { + self.itemNodes[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height)) + //self.itemNodes[i].containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + + if let previousCentralNodeHorizontalOffset = previousCentralNodeHorizontalOffset, let centralItemIndex = self.centralItemIndex, let centralNode = self.visibleItemNode(at: centralItemIndex) { + self.scrollView.contentOffset = CGPoint(x: centralNode.frame.minX + previousCentralNodeHorizontalOffset, y: 0.0) + } + + self.updateItemNodes() + } + + func centralItemNode() -> InstantPageSlideshowItemNode? { + if let centralItemIndex = self.centralItemIndex, let centralItemNode = self.visibleItemNode(at: centralItemIndex) { + return centralItemNode + } else { + return nil + } + } + + func replaceItems(_ items: [InstantPageMedia], centralItemIndex: Int?, keepFirst: Bool = false) { + var keptItemNode: InstantPageSlideshowItemNode? + for itemNode in self.itemNodes { + if keepFirst && itemNode.index == 0 { + keptItemNode = itemNode + } else { + itemNode.removeFromSupernode() + } + } + self.itemNodes.removeAll() + if let keptItemNode = keptItemNode { + self.itemNodes.append(keptItemNode) + } + if let centralItemIndex = centralItemIndex, centralItemIndex >= 0 && centralItemIndex < items.count { + self.centralItemIndex = centralItemIndex + } else { + self.centralItemIndex = nil + } + self.items = items + + self.updateItemNodes() + } + + private func makeNodeForItem(at index: Int) -> InstantPageSlideshowItemNode { + let media = self.items[index] + let contentNode: ASDisplayNode + if let _ = media.media as? TelegramMediaImage { + contentNode = InstantPageImageNode(account: self.account, media: media, interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia) + } else if let file = media.media as? TelegramMediaFile { + contentNode = ASDisplayNode() + } else { + contentNode = ASDisplayNode() + } + + let node = InstantPageSlideshowItemNode(contentNode: contentNode) + + node.index = index + return node + } + + private func visibleItemNode(at index: Int) -> InstantPageSlideshowItemNode? { + for itemNode in self.itemNodes { + if itemNode.index == index { + return itemNode + } + } + return nil + } + + private func addVisibleItemNode(_ node: InstantPageSlideshowItemNode) { + var added = false + for i in 0 ..< self.itemNodes.count { + if node.index < self.itemNodes[i].index { + self.itemNodes.insert(node, at: i) + added = true + break + } + } + if !added { + self.itemNodes.append(node) + } + self.scrollView.addSubview(node.view) + } + + private func removeVisibleItemNode(internalIndex: Int) { + self.itemNodes[internalIndex].view.removeFromSuperview() + self.itemNodes.remove(at: internalIndex) + } + + private func updateItemNodes() { + if self.items.isEmpty || self.containerLayout == nil { + return + } + + var resetOffsetToCentralItem = false + if self.itemNodes.isEmpty { + let node = self.makeNodeForItem(at: self.centralItemIndex ?? 0) + node.frame = CGRect(origin: CGPoint(), size: scrollView.bounds.size) + if let containerLayout = self.containerLayout { + //node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + self.addVisibleItemNode(node) + self.centralItemIndex = node.index + resetOffsetToCentralItem = true + } + + var notifyCentralItemUpdated = false + + if let centralItemIndex = self.centralItemIndex, let centralItemNode = self.visibleItemNode(at: centralItemIndex) { + if centralItemIndex != 0 { + if self.visibleItemNode(at: centralItemIndex - 1) == nil { + let node = self.makeNodeForItem(at: centralItemIndex - 1) + node.frame = centralItemNode.frame.offsetBy(dx: -centralItemNode.frame.size.width - self.pageGap, dy: 0.0) + if let containerLayout = self.containerLayout { + //node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + self.addVisibleItemNode(node) + } + } + + if centralItemIndex != items.count - 1 { + if self.visibleItemNode(at: centralItemIndex + 1) == nil { + let node = self.makeNodeForItem(at: centralItemIndex + 1) + node.frame = centralItemNode.frame.offsetBy(dx: centralItemNode.frame.size.width + self.pageGap, dy: 0.0) + if let containerLayout = self.containerLayout { + //node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + self.addVisibleItemNode(node) + } + } + + for i in 0 ..< self.itemNodes.count { + self.itemNodes[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height)) + } + + if resetOffsetToCentralItem { + self.scrollView.contentOffset = CGPoint(x: centralItemNode.frame.minX - self.pageGap, y: 0.0) + } + + if let centralItemCandidateNode = self.centralItemCandidate(), centralItemCandidateNode.index != centralItemIndex { + for i in (0 ..< self.itemNodes.count).reversed() { + let node = self.itemNodes[i] + if node.index < centralItemCandidateNode.index - 1 || node.index > centralItemCandidateNode.index + 1 { + self.removeVisibleItemNode(internalIndex: i) + } + } + + self.ignoreCentralItemIndexUpdate = true + self.centralItemIndex = centralItemCandidateNode.index + self.ignoreCentralItemIndexUpdate = false + notifyCentralItemUpdated = true + + if centralItemCandidateNode.index != 0 { + if self.visibleItemNode(at: centralItemCandidateNode.index - 1) == nil { + let node = self.makeNodeForItem(at: centralItemCandidateNode.index - 1) + node.frame = centralItemCandidateNode.frame.offsetBy(dx: -centralItemCandidateNode.frame.size.width - self.pageGap, dy: 0.0) + if let containerLayout = self.containerLayout { + //node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + self.addVisibleItemNode(node) + } + } + + if centralItemCandidateNode.index != items.count - 1 { + if self.visibleItemNode(at: centralItemCandidateNode.index + 1) == nil { + let node = self.makeNodeForItem(at: centralItemCandidateNode.index + 1) + node.frame = centralItemCandidateNode.frame.offsetBy(dx: centralItemCandidateNode.frame.size.width + self.pageGap, dy: 0.0) + if let containerLayout = self.containerLayout { + //node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + self.addVisibleItemNode(node) + } + } + + let previousCentralCandidateHorizontalOffset = self.scrollView.contentOffset.x - centralItemCandidateNode.frame.minX + + for i in 0 ..< self.itemNodes.count { + self.itemNodes[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height)) + } + + self.scrollView.contentOffset = CGPoint(x: centralItemCandidateNode.frame.minX + previousCentralCandidateHorizontalOffset, y: 0.0) + } + + self.scrollView.contentSize = CGSize(width: CGFloat(self.itemNodes.count) * self.scrollView.bounds.size.width, height: self.scrollView.bounds.size.height) + } else { + assertionFailure() + } + + for itemNode in self.itemNodes { + //itemNode.centralityUpdated(isCentral: itemNode.index == self.centralItemIndex) + //itemNode.visibilityUpdated(isVisible: self.scrollView.bounds.intersects(itemNode.frame)) + itemNode.internalIsVisible = self.scrollView.bounds.intersects(itemNode.frame) + } + + if notifyCentralItemUpdated { + self.centralItemIndexUpdated(self.centralItemIndex) + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateItemNodes() + } + + private func centralItemCandidate() -> InstantPageSlideshowItemNode? { + let hotizontlOffset = self.scrollView.contentOffset.x + self.pageGap + var closestNodeAndDistance: (Int, CGFloat)? + for i in 0 ..< self.itemNodes.count { + let node = self.itemNodes[i] + let distance = abs(node.frame.minX - hotizontlOffset) + if let currentClosestNodeAndDistance = closestNodeAndDistance { + if distance < currentClosestNodeAndDistance.1 { + closestNodeAndDistance = (node.index, distance) + } + } else { + closestNodeAndDistance = (node.index, distance) + } + } + if let closestNodeAndDistance = closestNodeAndDistance { + return self.visibleItemNode(at: closestNodeAndDistance.0) + } else { + return nil + } + } + + func updateHiddenMedia(_ media: InstantPageMedia?) { + for node in self.itemNodes { + node.updateHiddenMedia(media) + } + } + + func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + for node in self.itemNodes { + if let transitionNode = node.transitionNode(media: media) { + return transitionNode + } + } + return nil + } +} + +final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode { + var medias: [InstantPageMedia] = [] + + private let pagerNode: InstantPageSlideshowPagerNode + private let pageControlNode: PageControlNode + + init(account: Account, medias: [InstantPageMedia], openMedia: @escaping (InstantPageMedia) -> Void) { + self.medias = medias + + self.pagerNode = InstantPageSlideshowPagerNode(account: account, openMedia: openMedia) + self.pagerNode.replaceItems(medias, centralItemIndex: nil) + + self.pageControlNode = PageControlNode(dotColor: .white) + self.pageControlNode.isUserInteractionEnabled = false + + super.init() + + self.backgroundColor = .black + + self.addSubnode(self.pagerNode) + self.addSubnode(self.pageControlNode) + self.pageControlNode.pagesCount = medias.count + self.pagerNode.centralItemIndexUpdated = { [weak self] index in + if let strongSelf = self, let index = index { + strongSelf.pageControlNode.setPage(CGFloat(index)) + } + } + } + + override func layout() { + super.layout() + + self.pagerNode.frame = self.bounds + self.pagerNode.containerLayoutUpdated(ContainerViewLayout(size: self.bounds.size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) + + self.pageControlNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height - 20.0), size: CGSize(width: self.bounds.size.width, height: 20.0)) + } + + func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + return self.pagerNode.transitionNode(media: media) + } + + func updateHiddenMedia(media: InstantPageMedia?) { + self.pagerNode.updateHiddenMedia(media) + } + + func updateIsVisible(_ isVisible: Bool) { + self.pagerNode.internalIsVisible = isVisible + } + + func update(strings: PresentationStrings, theme: InstantPageTheme) { + } +} diff --git a/TelegramUI/InstantPageTextItem.swift b/TelegramUI/InstantPageTextItem.swift index 001eb1c6a4..21f89e7fcc 100644 --- a/TelegramUI/InstantPageTextItem.swift +++ b/TelegramUI/InstantPageTextItem.swift @@ -1,9 +1,15 @@ import Foundation import TelegramCore +import Postbox -struct InstantPageTextUrlItem { - let frame: CGRect - let item: AnyObject +final class InstantPageUrlItem { + let url: String + let webpageId: MediaId? + + init(url: String, webpageId: MediaId?) { + self.url = url + self.webpageId = webpageId + } } struct InstantPageTextStrikethroughItem { @@ -12,22 +18,22 @@ struct InstantPageTextStrikethroughItem { final class InstantPageTextLine { let line: CTLine + let range: NSRange let frame: CGRect - let urlItems: [InstantPageTextUrlItem] let strikethroughItems: [InstantPageTextStrikethroughItem] - init(line: CTLine, frame: CGRect, urlItems: [InstantPageTextUrlItem], strikethroughItems: [InstantPageTextStrikethroughItem]) { + init(line: CTLine, range: NSRange, frame: CGRect, strikethroughItems: [InstantPageTextStrikethroughItem]) { self.line = line + self.range = range self.frame = frame - self.urlItems = urlItems self.strikethroughItems = strikethroughItems } } final class InstantPageTextItem: InstantPageItem { + let attributedString: NSAttributedString let lines: [InstantPageTextLine] let rtlLineIndices: Set - let hasLinks: Bool var frame: CGRect var alignment: NSTextAlignment = .left let medias: [InstantPageMedia] = [] @@ -37,17 +43,13 @@ final class InstantPageTextItem: InstantPageItem { return !self.rtlLineIndices.isEmpty } - init(frame: CGRect, lines: [InstantPageTextLine]) { + init(frame: CGRect, attributedString: NSAttributedString, lines: [InstantPageTextLine]) { + self.attributedString = attributedString self.frame = frame self.lines = lines - var hasLinks = false var index = 0 var rtlLineIndices = Set() for line in lines { - if !line.urlItems.isEmpty { - hasLinks = true - } - let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray if glyphRuns.count != 0 { inner: for i in 0 ..< glyphRuns.count { @@ -61,7 +63,6 @@ final class InstantPageTextItem: InstantPageItem { } index += 1 } - self.hasLinks = hasLinks self.rtlLineIndices = rtlLineIndices } @@ -106,15 +107,140 @@ final class InstantPageTextItem: InstantPageItem { context.restoreGState() } - func linkSelectionViews() -> [InstantPageLinkSelectionView] { + private func attributesAtPoint(_ point: CGPoint) -> (Int, [String: Any])? { + let transformedPoint = CGPoint(x: point.x, y: point.y) + for line in self.lines { + let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y), size: line.frame.size) + if lineFrame.contains(transformedPoint) { + var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) + if index == attributedString.length { + index -= 1 + } else if index != 0 { + var glyphStart: CGFloat = 0.0 + CTLineGetOffsetForStringIndex(line.line, index, &glyphStart) + if transformedPoint.x < glyphStart { + index -= 1 + } + } + if index >= 0 && index < attributedString.length { + return (index, attributedString.attributes(at: index, effectiveRange: nil)) + } + break + } + } + for line in self.lines { + let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y), size: line.frame.size) + if lineFrame.insetBy(dx: -5.0, dy: -5.0).contains(transformedPoint) { + var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) + if index == attributedString.length { + index -= 1 + } else if index != 0 { + var glyphStart: CGFloat = 0.0 + CTLineGetOffsetForStringIndex(line.line, index, &glyphStart) + if transformedPoint.x < glyphStart { + index -= 1 + } + } + if index >= 0 && index < attributedString.length { + return (index, attributedString.attributes(at: index, effectiveRange: nil)) + } + break + } + } + return nil + } + + private func attributeRects(name: String, at index: Int) -> [CGRect]? { + var range = NSRange() + let _ = self.attributedString.attribute(name, at: index, effectiveRange: &range) + if range.length != 0 { + let boundsWidth = self.frame.width + var rects: [CGRect] = [] + for i in 0 ..< self.lines.count { + let line = self.lines[i] + let lineRange = NSIntersectionRange(range, line.range) + if lineRange.length != 0 { + var leftOffset: CGFloat = 0.0 + if lineRange.location != line.range.location { + leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) + } + var rightOffset: CGFloat = line.frame.width + if lineRange.location + lineRange.length != line.range.length { + rightOffset = ceil(CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, nil)) + } + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y), size: line.frame.size) + if self.alignment == .center { + lineFrame.origin.x = floor((boundsWidth - lineFrame.size.width) / 2.0) + } else if self.alignment == .right { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } else if self.alignment == .natural && self.rtlLineIndices.contains(i) { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } + + rects.append(CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset, y: lineFrame.minY), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height))) + } + } + if !rects.isEmpty { + return rects + } + } + + return nil + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + if let (index, dict) = self.attributesAtPoint(point) { + if let _ = dict[TextNode.UrlAttribute] { + if let rects = self.attributeRects(name: TextNode.UrlAttribute, at: index) { + return rects + } + } + } + return [] } + func urlAttribute(at point: CGPoint) -> InstantPageUrlItem? { + if let (_, dict) = self.attributesAtPoint(point) { + if let url = dict[TextNode.UrlAttribute] as? InstantPageUrlItem { + return url + } + } + return nil + } + + func lineRects() -> [CGRect] { + let boundsWidth = self.frame.width + var rects: [CGRect] = [] + for i in 0 ..< self.lines.count { + let line = self.lines[i] + + var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y), size: line.frame.size) + if self.alignment == .center { + lineFrame.origin.x = floor((boundsWidth - lineFrame.size.width) / 2.0) + } else if self.alignment == .right { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } else if self.alignment == .natural && self.rtlLineIndices.contains(i) { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } + + rects.append(lineFrame) + } + return rects + } + + func plainText() -> String { + if let first = self.lines.first, let last = self.lines.last { + return self.attributedString.attributedSubstring(from: NSMakeRange(first.range.location, last.range.location + last.range.length - first.range.location)).string + } + return "" + } + func matchesAnchor(_ anchor: String) -> Bool { return false } - func node(account: Account) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { return nil } @@ -131,119 +257,57 @@ final class InstantPageTextItem: InstantPageItem { } } -/* - - -static TGInstantPageLinkSelectionView *selectionViewFromFrames(NSArray *frames, CGPoint origin, id urlItem) { - CGRect frame = CGRectMake(0.0f, 0.0f, 0.0f, 0.0f); - bool first = true; - for (NSValue *rectValue in frames) { - CGRect rect = [rectValue CGRectValue]; - if (first) { - first = false; - frame = rect; - } else { - frame = CGRectUnion(rect, frame); - } - } - NSMutableArray *adjustedFrames = [[NSMutableArray alloc] init]; - for (NSValue *rectValue in frames) { - CGRect rect = [rectValue CGRectValue]; - rect.origin.x -= frame.origin.x; - rect.origin.y -= frame.origin.y; - [adjustedFrames addObject:[NSValue valueWithCGRect:rect]]; - } - return [[TGInstantPageLinkSelectionView alloc] initWithFrame:CGRectOffset(frame, origin.x, origin.y) rects:adjustedFrames urlItem:urlItem]; - } - - - (NSArray *)linkSelectionViews { - if (_hasLinks) { - NSMutableArray *views = [[NSMutableArray alloc] init]; - NSMutableArray *currentLinkFrames = [[NSMutableArray alloc] init]; - id currentUrlItem = nil; - for (TGInstantPageTextLine *line in _lines) { - if (line.urlItems != nil) { - for (TGInstantPageTextUrlItem *urlItem in line.urlItems) { - if (currentUrlItem == urlItem.item) { - } else { - if (currentLinkFrames.count != 0) { - [views addObject:selectionViewFromFrames(currentLinkFrames, self.frame.origin, currentUrlItem)]; - } - [currentLinkFrames removeAllObjects]; - currentUrlItem = urlItem.item; - } - CGPoint lineOrigin = line.frame.origin; - if (_alignment == NSTextAlignmentCenter) { - lineOrigin.x = CGFloor((self.frame.size.width - line.frame.size.width) / 2.0f); - } - [currentLinkFrames addObject:[NSValue valueWithCGRect:CGRectOffset(urlItem.frame, lineOrigin.x, 0.0)]]; - } - } else if (currentUrlItem != nil) { - if (currentLinkFrames.count != 0) { - [views addObject:selectionViewFromFrames(currentLinkFrames, self.frame.origin, currentUrlItem)]; - } - [currentLinkFrames removeAllObjects]; - currentUrlItem = nil; - } - } - if (currentLinkFrames.count != 0 && currentUrlItem != nil) { - [views addObject:selectionViewFromFrames(currentLinkFrames, self.frame.origin, currentUrlItem)]; - } - return views; - } - return nil; -} - -@end*/ - -func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextStyleStack) -> NSAttributedString { +func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextStyleStack, url: InstantPageUrlItem? = nil) -> NSAttributedString { switch text { case .empty: return NSAttributedString(string: "", attributes: styleStack.textAttributes()) case let .plain(string): - return NSAttributedString(string: string, attributes: styleStack.textAttributes()) + var attributes = styleStack.textAttributes() + if let url = url { + attributes[TextNode.UrlAttribute] = url + } + return NSAttributedString(string: string, attributes: attributes) case let .bold(text): styleStack.push(.bold) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .italic(text): styleStack.push(.italic) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .underline(text): styleStack.push(.underline) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .strikethrough(text): styleStack.push(.strikethrough) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .fixed(text): styleStack.push(.fontFixed(true)) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result - case let .url(text, url, _): - styleStack.push(.textColor(UIColor(rgb: 0x007BE8))) - let result = attributedStringForRichText(text, styleStack: styleStack) - styleStack.pop() + case let .url(text, url, webpageId): + styleStack.push(.underline) + let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: url, webpageId: webpageId)) styleStack.pop() return result - case let .email(text, _): + case let .email(text, email): styleStack.push(.bold) - styleStack.push(.textColor(UIColor(rgb: 0x007BE8))) - let result = attributedStringForRichText(text, styleStack: styleStack) + styleStack.push(.underline) + let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "mailto:\(email)", webpageId: nil)) styleStack.pop() styleStack.pop() return result case let .concat(texts): let string = NSMutableAttributedString() for text in texts { - let substring = attributedStringForRichText(text, styleStack: styleStack) + let substring = attributedStringForRichText(text, styleStack: styleStack, url: url) string.append(substring) } return string @@ -252,12 +316,12 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat) -> InstantPageTextItem { if string.length == 0 { - return InstantPageTextItem(frame: CGRect(), lines: []) + return InstantPageTextItem(frame: CGRect(), attributedString: string, lines: []) } var lines: [InstantPageTextLine] = [] guard let font = string.attribute(NSFontAttributeName, at: 0, effectiveRange: nil) as? UIFont else { - return InstantPageTextItem(frame: CGRect(), lines: []) + return InstantPageTextItem(frame: CGRect(), attributedString: string, lines: []) } var lineSpacingFactor: CGFloat = 1.12 @@ -287,10 +351,11 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo let trailingWhitespace = CGFloat(CTLineGetTrailingWhitespaceWidth(line)) let lineWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil) + Double(currentLineInset)) - var urlItems: [InstantPageTextUrlItem] = [] var strikethroughItems: [InstantPageTextStrikethroughItem] = [] - string.enumerateAttribute(NSStrikethroughStyleAttributeName, in: NSMakeRange(lastIndex, lineCharacterCount), options: [], using: { item, range, _ in + let lineRange = NSMakeRange(lastIndex, lineCharacterCount) + + string.enumerateAttribute(NSStrikethroughStyleAttributeName, in: lineRange, options: [], using: { item, range, _ in if let item = item { let lowerX = floor(CTLineGetOffsetForStringIndex(line, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(line, range.location + range.length, nil)) @@ -311,7 +376,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo } }];*/ - let textLine = InstantPageTextLine(line: line, frame: CGRect(x: currentLineOrigin.x, y: currentLineOrigin.y, width: lineWidth, height: fontLineHeight), urlItems: urlItems, strikethroughItems: strikethroughItems) + let textLine = InstantPageTextLine(line: line, range: lineRange, frame: CGRect(x: currentLineOrigin.x, y: currentLineOrigin.y, width: lineWidth, height: fontLineHeight), strikethroughItems: strikethroughItems) lines.append(textLine) @@ -332,5 +397,5 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo height = lines.last!.frame.maxY } - return InstantPageTextItem(frame: CGRect(x: 0.0, y: 0.0, width: boundingWidth, height: height), lines: lines) + return InstantPageTextItem(frame: CGRect(x: 0.0, y: 0.0, width: boundingWidth, height: height), attributedString: string, lines: lines) } diff --git a/TelegramUI/InstantPageTheme.swift b/TelegramUI/InstantPageTheme.swift index 86e2e8824c..f87385ff85 100644 --- a/TelegramUI/InstantPageTheme.swift +++ b/TelegramUI/InstantPageTheme.swift @@ -1,7 +1,191 @@ import Foundation +import Postbox -final class InstantPageTheme { - init() { - +enum InstantPageFontStyle { + case sans + case serif +} + +struct InstantPageFont { + let style: InstantPageFontStyle + let size: CGFloat + let lineSpacingFactor: CGFloat +} + +struct InstantPageTextAttributes { + let font: InstantPageFont + let color: UIColor + let underline: Bool + + init(font: InstantPageFont, color: UIColor, underline: Bool = false) { + self.font = font + self.color = color + self.underline = underline + } + + func withUnderline(_ underline: Bool) -> InstantPageTextAttributes { + return InstantPageTextAttributes(font: self.font, color: self.color, underline: underline) + } + + func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTextAttributes { + return InstantPageTextAttributes(font: InstantPageFont(style: forceSerif ? .serif : self.font.style, size: floor(self.font.size * sizeMultiplier), lineSpacingFactor: self.font.lineSpacingFactor), color: self.color, underline: self.underline) + } +} + +enum InstantPageTextCategoryType { + case header + case subheader + case paragraph + case caption +} + +struct InstantPageTextCategories { + let header: InstantPageTextAttributes + let subheader: InstantPageTextAttributes + let paragraph: InstantPageTextAttributes + let caption: InstantPageTextAttributes + + func attributes(type: InstantPageTextCategoryType, link: Bool) -> InstantPageTextAttributes { + switch type { + case .header: + return self.header.withUnderline(link) + case .subheader: + return self.subheader.withUnderline(link) + case .paragraph: + return self.paragraph.withUnderline(link) + case .caption: + return self.caption.withUnderline(link) + } + } + + func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTextCategories { + return InstantPageTextCategories(header: self.header.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), subheader: self.subheader.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), paragraph: self.paragraph.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), caption: self.caption.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif)) + } +} + +final class InstantPageTheme { + let pageBackgroundColor: UIColor + + let textCategories: InstantPageTextCategories + + let textHighlightColor: UIColor + let linkHighlightColor: UIColor + + let panelBackgroundColor: UIColor + let panelHighlightedBackgroundColor: UIColor + let panelPrimaryColor: UIColor + let panelSecondaryColor: UIColor + let panelAccentColor: UIColor + + init(pageBackgroundColor: UIColor, textCategories: InstantPageTextCategories, textHighlightColor: UIColor, linkHighlightColor: UIColor, panelBackgroundColor: UIColor, panelHighlightedBackgroundColor: UIColor, panelPrimaryColor: UIColor, panelSecondaryColor: UIColor, panelAccentColor: UIColor) { + self.pageBackgroundColor = pageBackgroundColor + self.textCategories = textCategories + self.textHighlightColor = textHighlightColor + self.linkHighlightColor = linkHighlightColor + self.panelBackgroundColor = panelBackgroundColor + self.panelHighlightedBackgroundColor = panelHighlightedBackgroundColor + self.panelPrimaryColor = panelPrimaryColor + self.panelSecondaryColor = panelSecondaryColor + self.panelAccentColor = panelAccentColor + } + + func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTheme { + return InstantPageTheme(pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor) + } +} + +private let lightTheme = InstantPageTheme( + pageBackgroundColor: .white, + textCategories: InstantPageTextCategories( + header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: .black), + subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: .black), + paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: .black), + caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x79828b)) + ), + textHighlightColor: UIColor(rgb: 0, alpha: 0.12), + linkHighlightColor: UIColor(rgb: 0, alpha: 0.12), + panelBackgroundColor: UIColor(rgb: 0xf3f4f5), + panelHighlightedBackgroundColor: UIColor(rgb: 0xe7e7e7), + panelPrimaryColor: .black, + panelSecondaryColor: UIColor(rgb: 0x79828b), + panelAccentColor: UIColor(rgb: 0x007ee5) +) + +private let sepiaTheme = InstantPageTheme( + pageBackgroundColor: UIColor(rgb: 0xf8f1e2), + textCategories: InstantPageTextCategories( + header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0x4f321d)), + subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0x4f321d)), + paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x4f321d)), + caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x927e6b)) + ), + textHighlightColor: UIColor(rgb: 0, alpha: 0.1), + linkHighlightColor: UIColor(rgb: 0, alpha: 0.1), + panelBackgroundColor: UIColor(rgb: 0xefe7d6), + panelHighlightedBackgroundColor: UIColor(rgb: 0xe3dccb), + panelPrimaryColor: .black, + panelSecondaryColor: UIColor(rgb: 0x927e6b), + panelAccentColor: UIColor(rgb: 0xd19601) +) + +private let grayTheme = InstantPageTheme( + pageBackgroundColor: UIColor(rgb: 0x5a5a5c), + textCategories: InstantPageTextCategories( + header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xcecece)), + subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xcecece)), + paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xcecece)), + caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xa0a0a0)) + ), + textHighlightColor: UIColor(rgb: 0, alpha: 0.16), + linkHighlightColor: UIColor(rgb: 0, alpha: 0.16), + panelBackgroundColor: UIColor(rgb: 0x555556), + panelHighlightedBackgroundColor: UIColor(rgb: 0x505051), + panelPrimaryColor: UIColor(rgb: 0xcecece), + panelSecondaryColor: UIColor(rgb: 0xa0a0a0), + panelAccentColor: UIColor(rgb: 0x54b9f8) +) + +private let darkTheme = InstantPageTheme( + pageBackgroundColor: UIColor(rgb: 0x000000), + textCategories: InstantPageTextCategories( + header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xb0b0b0)), + subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xb0b0b0)), + paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xb0b0b0)), + caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x6a6a6a)) + ), + textHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.1), + linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.1), + panelBackgroundColor: UIColor(rgb: 0x131313), + panelHighlightedBackgroundColor: UIColor(rgb: 0x1f1f1f), + panelPrimaryColor: UIColor(rgb: 0xb0b0b0), + panelSecondaryColor: UIColor(rgb: 0x6a6a6a), + panelAccentColor: UIColor(rgb: 0x50b6f3) +) + +private func fontSizeMultiplierForVariant(_ variant: InstantPagePresentationFontSize) -> CGFloat { + switch variant { + case .small: + return 0.85 + case .standard: + return 1.0 + case .large: + return 1.15 + case .xlarge: + return 1.3 + case .xxlarge: + return 1.5 + } +} + +func instantPageThemeForSettings(_ settings: InstantPagePresentationSettings) -> InstantPageTheme { + switch settings.themeType { + case .light: + return lightTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) + case .sepia: + return sepiaTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) + case .gray: + return grayTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) + case .dark: + return darkTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) } } diff --git a/TelegramUI/InstantPageTileNode.swift b/TelegramUI/InstantPageTileNode.swift index 0a41c26767..3d35a943be 100644 --- a/TelegramUI/InstantPageTileNode.swift +++ b/TelegramUI/InstantPageTileNode.swift @@ -3,9 +3,11 @@ import AsyncDisplayKit private final class InstantPageTileNodeParameters: NSObject { let tile: InstantPageTile + let backgroundColor: UIColor - init(tile: InstantPageTile) { + init(tile: InstantPageTile, backgroundColor: UIColor) { self.tile = tile + self.backgroundColor = backgroundColor super.init() } @@ -14,31 +16,31 @@ private final class InstantPageTileNodeParameters: NSObject { final class InstantPageTileNode: ASDisplayNode { private let tile: InstantPageTile - init(tile: InstantPageTile) { + init(tile: InstantPageTile, backgroundColor: UIColor) { self.tile = tile super.init() self.isLayerBacked = true self.isOpaque = true - self.backgroundColor = UIColor.white + self.backgroundColor = backgroundColor } override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return InstantPageTileNodeParameters(tile: self.tile) + return InstantPageTileNodeParameters(tile: self.tile, backgroundColor: self.backgroundColor ?? UIColor.white) } @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { let context = UIGraphicsGetCurrentContext()! - if !isRasterizing { - context.setBlendMode(.copy) - context.setFillColor(UIColor.white.cgColor) - context.fill(bounds) - } - if let parameters = parameters as? InstantPageTileNodeParameters { + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(parameters.backgroundColor.cgColor) + context.fill(bounds) + } + parameters.tile.draw(context: context) } } diff --git a/TelegramUI/InstantPageWebEmbedItem.swift b/TelegramUI/InstantPageWebEmbedItem.swift index d942b186f2..ff998c2d74 100644 --- a/TelegramUI/InstantPageWebEmbedItem.swift +++ b/TelegramUI/InstantPageWebEmbedItem.swift @@ -1,9 +1,9 @@ import Foundation +import Postbox import TelegramCore final class InstantPageWebEmbedItem: InstantPageItem { var frame: CGRect - let hasLinks: Bool = false let wantsNode: Bool = true let medias: [InstantPageMedia] = [] @@ -18,7 +18,7 @@ final class InstantPageWebEmbedItem: InstantPageItem { self.enableScrolling = enableScrolling } - func node(account: Account) -> InstantPageNode? { + func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void) -> InstantPageNode? { return instantPageWebEmbedNode(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling) } @@ -46,7 +46,7 @@ final class InstantPageWebEmbedItem: InstantPageItem { } } - func linkSelectionViews() -> [InstantPageLinkSelectionView] { + func linkSelectionRects(at point: CGPoint) -> [CGRect] { return [] } diff --git a/TelegramUI/InstantPageWebEmbedNode.swift b/TelegramUI/InstantPageWebEmbedNode.swift index 1e93ea2e35..2f2de22d9d 100644 --- a/TelegramUI/InstantPageWebEmbedNode.swift +++ b/TelegramUI/InstantPageWebEmbedNode.swift @@ -33,6 +33,16 @@ final class instantPageWebEmbedNode: ASDisplayNode, InstantPageNode { self.webView.frame = self.bounds } + func transitionNode(media: InstantPageMedia) -> ASDisplayNode? { + return nil + } + + func updateHiddenMedia(media: InstantPageMedia?) { + } + func updateIsVisible(_ isVisible: Bool) { } + + func update(strings: PresentationStrings, theme: InstantPageTheme) { + } } diff --git a/TelegramUI/InstantVideoNode.swift b/TelegramUI/InstantVideoNode.swift index 6a5256b1b0..aabaab8921 100644 --- a/TelegramUI/InstantVideoNode.swift +++ b/TelegramUI/InstantVideoNode.swift @@ -168,6 +168,16 @@ final class InstantVideoNode: OverlayMediaItemNode { } }) + + self.manager.sharedVideoContextManager.withSharedVideoContext(id: self.source.id, { [weak self] context in + if let strongSelf = self, let context = context as? SharedInstantVideoContext { + context.addPlaybackCompleted { + if let strongSelf = self { + strongSelf.playbackEnded?() + } + } + } + }) } deinit { diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index a515165b69..6e1fb31853 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -189,6 +189,14 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV self.transitionDisposable.dispose() } + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + } + func animateIn() { self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } @@ -352,7 +360,7 @@ class ItemListControllerNode: ASDisplayNode, UIScrollV } 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 }) } func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/TelegramUI/LegacyController.swift b/TelegramUI/LegacyController.swift index 8adde9434b..7fdb3e0b7a 100644 --- a/TelegramUI/LegacyController.swift +++ b/TelegramUI/LegacyController.swift @@ -31,6 +31,13 @@ private final class LegacyComponentsOverlayWindowManagerImpl: NSObject, LegacyCo self.controller = LegacyController(presentation: .custom) super.init() + + if let parentController = parentController { + if parentController.statusBar.statusBarStyle == .Hide { + self.controller?.statusBar.statusBarStyle = parentController.statusBar.statusBarStyle + } + self.controller?.view.frame = parentController.view.bounds + } } func managesWindow() -> Bool { @@ -275,10 +282,13 @@ public class LegacyController: ViewController { self.controllerNode.animateModalIn(completion: { [weak self] in self?.presentationCompleted?() }) + } else { + self.presentationCompleted?() } self.legacyController.viewDidAppear(animated && animateIn) case .custom: self.legacyController.viewDidAppear(animated) + self.presentationCompleted?() } } diff --git a/TelegramUI/LegacyInstantVideoController.swift b/TelegramUI/LegacyInstantVideoController.swift new file mode 100644 index 0000000000..8c6e285010 --- /dev/null +++ b/TelegramUI/LegacyInstantVideoController.swift @@ -0,0 +1,156 @@ +import Foundation +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +import LegacyComponents + +final class InstantVideoControllerRecordingStatus { + let micLevel: Signal + + init(micLevel: Signal) { + self.micLevel = micLevel + } +} + +final class InstantVideoController: LegacyController { + private var captureController: TGVideoMessageCaptureController? + + var onDismiss: (() -> Void)? + + private let micLevelValue = ValuePromise(0.0) + let audioStatus: InstantVideoControllerRecordingStatus + + private var dismissedVideo = false + + override init(presentation: LegacyControllerPresentation) { + self.audioStatus = InstantVideoControllerRecordingStatus(micLevel: self.micLevelValue.get()) + + super.init(presentation: presentation) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bindCaptureController(_ captureController: TGVideoMessageCaptureController?) { + self.captureController = captureController + if let captureController = captureController { + captureController.micLevel = { [weak self] (level: CGFloat) -> Void in + self?.micLevelValue.set(Float(level)) + } + captureController.onDismiss = { [weak self] _ in + if let strongSelf = self { + strongSelf.onDismiss?() + } + } + } + } + + func dismissVideo() { + if let captureController = self.captureController, !self.dismissedVideo { + self.dismissedVideo = true + captureController.dismiss() + } + } + + func completeVideo() { + if let captureController = self.captureController, !self.dismissedVideo { + self.dismissedVideo = true + captureController.complete() + } + } + + func stopVideo() -> Bool { + if let captureController = self.captureController { + return captureController.stop() + } + return false + } + + func lockVideo() { + if let captureController = self.captureController { + return captureController.setLocked() + } + } + + func updateRecordButtonInteraction(_ value: CGFloat) { + if let captureController = self.captureController { + captureController.buttonInteractionUpdate(CGPoint(x: value, y: 0.0)) + } + } +} + +func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, account: Account, peerId: PeerId) -> InstantVideoController { + let legacyController = InstantVideoController(presentation: .custom) + legacyController.statusBar.statusBarStyle = .Hide + let baseController = TGViewController(context: legacyController.context)! + legacyController.bind(controller: baseController) + legacyController.presentationCompleted = { [weak legacyController, weak baseController] in + if let legacyController = legacyController, let baseController = baseController { + let controller = TGVideoMessageCaptureController(context: legacyController.context, assets: TGVideoMessageCaptureControllerAssets(send: PresentationResourcesChat.chatInputPanelSendButtonImage(theme)!, slideToCancel:PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)!, actionDelete: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor))!, transitionInView: { + return nil + }, parentController: baseController, controlsFrame: panelFrame, isAlreadyLocked: { + return false + }, liveUploadInterface: nil)! + /*controller.finishedWithVideo = ^(NSURL *videoURL, UIImage *previewImage, __unused NSUInteger fileSize, NSTimeInterval duration, CGSize dimensions, TGLiveUploadActorData *liveUploadData, TGVideoEditAdjustments *adjustments) + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf != nil) + { + NSDictionary *desc = [strongSelf->_companion videoDescriptionFromVideoURL:videoURL previewImage:previewImage dimensions:dimensions duration:duration adjustments:adjustments stickers:nil caption:nil roundMessage:true liveUploadData:liveUploadData timer:0]; + [strongSelf->_companion controllerWantsToSendImagesWithDescriptions:@[ desc ] asReplyToMessageId:[strongSelf currentReplyMessageId] botReplyMarkup:nil]; + } + }*/ + controller.finishedWithVideo = { videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments in + guard let videoUrl = videoUrl else { + return + } + + var finalDimensions: CGSize = dimensions + var finalDuration: Double = duration + + var previewRepresentations: [TelegramMediaImageRepresentation] = [] + if let previewImage = previewImage { + let resource = LocalFileMediaResource(fileId: arc4random64()) + let thumbnailSize = finalDimensions.aspectFitted(CGSize(width: 90.0, height: 90.0)) + let thumbnailImage = TGScaleImageToPixelSize(previewImage, thumbnailSize)! + if let thumbnailData = UIImageJPEGRepresentation(thumbnailImage, 0.4) { + account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: thumbnailSize, resource: resource)) + } + } + + finalDimensions = TGMediaVideoConverter.dimensions(for: finalDimensions, adjustments: adjustments, preset: TGMediaVideoConversionPresetVideoMessage) + + var resourceAdjustments: VideoMediaResourceAdjustments? + if let adjustments = adjustments { + if adjustments.trimApplied() { + finalDuration = adjustments.trimEndValue - adjustments.trimStartValue + } + + let adjustmentsData = MemoryBuffer(data: NSKeyedArchiver.archivedData(withRootObject: adjustments.dictionary())) + let digest = MemoryBuffer(data: adjustmentsData.md5Digest()) + resourceAdjustments = VideoMediaResourceAdjustments(data: adjustmentsData, digest: digest) + } + + let resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: videoUrl.path, adjustments: resourceAdjustments) + + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: finalDimensions, flags: [.instantRoundVideo])]) + var attributes: [MessageAttribute] = [] + /*if let timer = item.timer, timer > 0 && timer <= 60 { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) + }*/ + let _ = enqueueMessages(account: account, peerId: peerId, messages: [.message(text: "", attributes: attributes, media: media, replyToMessageId: nil)]).start() + } + controller.didDismiss = { [weak legacyController] in + if let legacyController = legacyController { + legacyController.dismiss() + } + } + legacyController.bindCaptureController(controller) + } + } + return legacyController +} diff --git a/TelegramUI/MapResources.swift b/TelegramUI/MapResources.swift index 0ebd14f662..593defb554 100644 --- a/TelegramUI/MapResources.swift +++ b/TelegramUI/MapResources.swift @@ -40,14 +40,14 @@ public class MapSnapshotMediaResource: TelegramMediaResource { self.height = height } - public required init(decoder: Decoder) { + public required init(decoder: PostboxDecoder) { self.latitude = decoder.decodeDoubleForKey("lt", orElse: 0.0) self.longitude = decoder.decodeDoubleForKey("ln", orElse: 0.0) self.width = decoder.decodeInt32ForKey("w", orElse: 0) self.height = decoder.decodeInt32ForKey("h", orElse: 0) } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeDouble(self.latitude, forKey: "lt") encoder.encodeDouble(self.longitude, forKey: "ln") encoder.encodeInt32(self.width, forKey: "w") diff --git a/TelegramUI/MediaPlayerScrubbingNode.swift b/TelegramUI/MediaPlayerScrubbingNode.swift index cf5b45efc6..fc6c0c4c5c 100644 --- a/TelegramUI/MediaPlayerScrubbingNode.swift +++ b/TelegramUI/MediaPlayerScrubbingNode.swift @@ -85,6 +85,7 @@ private final class MediaPlayerScrubbingForegroundNode: ASDisplayNode { } final class MediaPlayerScrubbingNode: ASDisplayNode { + private let lineCap: MediaPlayerScrubbingNodeCap private let lineHeight: CGFloat private let backgroundNode: ASImageNode @@ -120,20 +121,21 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { } private var statusDisposable: Disposable? - private var statusValuePromise = Promise() + private var statusValuePromise = Promise() var status: Signal? { didSet { if let status = self.status { - self.statusValuePromise.set(status) + self.statusValuePromise.set(status |> map { $0 }) } else { - self.statusValuePromise.set(.never()) + self.statusValuePromise.set(.single(nil)) } } } init(lineHeight: CGFloat, lineCap: MediaPlayerScrubbingNodeCap, scrubberHandle: Bool, backgroundColor: UIColor, foregroundColor: UIColor) { self.lineHeight = lineHeight + self.lineCap = lineCap self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true @@ -234,6 +236,20 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { } } + func updateColors(backgroundColor: UIColor, foregroundColor: UIColor) { + switch lineCap { + case .round: + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: backgroundColor) + self.foregroundContentNode.image = generateStretchableFilledCircleImage(diameter: lineHeight, color: foregroundColor) + case .square: + self.backgroundNode.backgroundColor = backgroundColor + self.foregroundContentNode.backgroundColor = foregroundColor + } + if let handleNode = self.handleNode as? ASImageNode { + handleNode.image = generateHandleBackground(color: foregroundColor) + } + } + private func preparedAnimation(keyPath: String, from: NSValue, to: NSValue, duration: Double, beginTime: Double?, offset: Double, speed: Float, repeatForever: Bool = false) -> CAAnimation { let animation = CABasicAnimation(keyPath: keyPath) animation.fromValue = from @@ -244,7 +260,7 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { animation.speed = speed animation.timeOffset = offset animation.isAdditive = false - animation.repeatCount = Float.infinity + //animation.repeatCount = Float.infinity if let beginTime = beginTime { animation.beginTime = beginTime } diff --git a/TelegramUI/MediaResources.swift b/TelegramUI/MediaResources.swift index afb13d2d58..a199c71b49 100644 --- a/TelegramUI/MediaResources.swift +++ b/TelegramUI/MediaResources.swift @@ -2,7 +2,7 @@ import Foundation import Postbox import TelegramCore -public final class VideoMediaResourceAdjustments: Coding, Equatable { +public final class VideoMediaResourceAdjustments: PostboxCoding, Equatable { let data: MemoryBuffer let digest: MemoryBuffer @@ -11,12 +11,12 @@ public final class VideoMediaResourceAdjustments: Coding, Equatable { self.digest = digest } - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { self.data = decoder.decodeBytesForKey("d")! self.digest = decoder.decodeBytesForKey("h")! } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeBytes(self.data, forKey: "d") encoder.encodeBytes(self.digest, forKey: "h") } @@ -64,12 +64,12 @@ public final class VideoLibraryMediaResource: TelegramMediaResource { self.adjustments = adjustments } - public required init(decoder: Decoder) { + public required init(decoder: PostboxDecoder) { self.localIdentifier = decoder.decodeStringForKey("i", orElse: "") self.adjustments = decoder.decodeObjectForKey("a", decoder: { VideoMediaResourceAdjustments(decoder: $0) }) as? VideoMediaResourceAdjustments } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeString(self.localIdentifier, forKey: "i") if let adjustments = self.adjustments { encoder.encodeObject(adjustments, forKey: "a") @@ -126,13 +126,13 @@ public final class LocalFileVideoMediaResource: TelegramMediaResource { self.adjustments = adjustments } - public required init(decoder: Decoder) { + public required init(decoder: PostboxDecoder) { self.randomId = decoder.decodeInt64ForKey("i", orElse: 0) self.path = decoder.decodeStringForKey("p", orElse: "") self.adjustments = decoder.decodeObjectForKey("a", decoder: { VideoMediaResourceAdjustments(decoder: $0) }) as? VideoMediaResourceAdjustments } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt64(self.randomId, forKey: "i") encoder.encodeString(self.path, forKey: "p") if let adjustments = self.adjustments { @@ -182,11 +182,11 @@ public class PhotoLibraryMediaResource: TelegramMediaResource { self.localIdentifier = localIdentifier } - public required init(decoder: Decoder) { + public required init(decoder: PostboxDecoder) { self.localIdentifier = decoder.decodeStringForKey("i", orElse: "") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeString(self.localIdentifier, forKey: "i") } diff --git a/TelegramUI/MultiplexedSoftwareVideoNode.swift b/TelegramUI/MultiplexedSoftwareVideoNode.swift index 15858d3fba..45f8c53d5b 100644 --- a/TelegramUI/MultiplexedSoftwareVideoNode.swift +++ b/TelegramUI/MultiplexedSoftwareVideoNode.swift @@ -109,6 +109,9 @@ final class MultiplexedSoftwareVideoNode: UIView { init(account: Account, scrollView: UIScrollView) { self.account = account self.scrollView = scrollView + if #available(iOSApplicationExtension 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } self.trackingNode = MultiplexedSoftwareVideoTrackingNode() self.sourceManager = MultiplexedSoftwareVideoSourceManager(queue: self.videoSourceQueue, account: account) diff --git a/TelegramUI/Notices.swift b/TelegramUI/Notices.swift index 0d66096b1c..34e26bc141 100644 --- a/TelegramUI/Notices.swift +++ b/TelegramUI/Notices.swift @@ -2,14 +2,14 @@ import Foundation import Postbox import SwiftSignalKit -final class ApplicationSpecificBoolNotice: Coding { +final class ApplicationSpecificBoolNotice: PostboxCoding { init() { } - init(decoder: Decoder) { + init(decoder: PostboxDecoder) { } - func encode(_ encoder: Encoder) { + func encode(_ encoder: PostboxEncoder) { } } diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 972051c164..1e4c5dc899 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -317,8 +317,11 @@ public class PeerMediaCollectionController: ViewController { }, sendBotCommand: { _ in }, sendBotStart: { _ in }, botSwitchChatWithPayload: { _ in - }, beginAudioRecording: { - }, finishAudioRecording: { _ in + }, beginMediaRecording: { _ in + }, finishMediaRecording: { _ in + }, stopMediaRecording: { + }, lockMediaRecording: { + }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { }, sendSticker: { _ in }, unblockPeer: { @@ -329,6 +332,7 @@ public class PeerMediaCollectionController: ViewController { }, deleteChat: { }, beginCall: { }, toggleMessageStickerStarred: { _ in + }, presentController: { _ in }, statuses: nil) self.updateInterfaceState(animated: false, { return $0 }) diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 21b48d2e19..7767f633a9 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -1620,10 +1620,6 @@ func chatWebFileImage(account: Account, file: TelegramMediaWebFile) -> Signal<(T c.draw(fullSizeImage, in: fittedRect) c.setBlendMode(.normal) - - if let locationPinImage = locationPinImage { - c.draw(locationPinImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - locationPinImage.size.width) / 2.0), y: floor((arguments.drawingSize.height - locationPinImage.size.height) / 2.0) - 5.0), size: locationPinImage.size)) - } } } else { context.withFlippedContext { c in @@ -1632,10 +1628,6 @@ func chatWebFileImage(account: Account, file: TelegramMediaWebFile) -> Signal<(T c.fill(arguments.drawingRect) c.setBlendMode(.normal) - - if let locationPinImage = locationPinImage { - c.draw(locationPinImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - locationPinImage.size.width) / 2.0), y: floor((arguments.drawingSize.height - locationPinImage.size.height) / 2.0) - 5.0), size: locationPinImage.size)) - } } } } diff --git a/TelegramUI/PreferencesKeys.swift b/TelegramUI/PreferencesKeys.swift index b6c2612ac9..b7662fafd0 100644 --- a/TelegramUI/PreferencesKeys.swift +++ b/TelegramUI/PreferencesKeys.swift @@ -9,6 +9,7 @@ private enum ApplicationSpecificPreferencesKeyValues: Int32 { case generatedMediaStoreSettings = 3 case voiceCallSettings = 4 case presentationThemeSettings = 5 + case instantPagePresentationSettings = 6 } public struct ApplicationSpecificPreferencesKeys { @@ -18,4 +19,5 @@ public struct ApplicationSpecificPreferencesKeys { public static let generatedMediaStoreSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.generatedMediaStoreSettings.rawValue) public static let voiceCallSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.voiceCallSettings.rawValue) public static let presentationThemeSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.presentationThemeSettings.rawValue) + public static let instantPagePresentationSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.instantPagePresentationSettings.rawValue) } diff --git a/TelegramUI/PreparedChatHistoryViewTransition.swift b/TelegramUI/PreparedChatHistoryViewTransition.swift index 6d0296dc86..1a2bd3f780 100644 --- a/TelegramUI/PreparedChatHistoryViewTransition.swift +++ b/TelegramUI/PreparedChatHistoryViewTransition.swift @@ -4,7 +4,7 @@ import Postbox import TelegramCore import Display -func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?) -> Signal { +func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, readStateData: ChatHistoryCombinedInitialReadStateData?) -> Signal { return Signal { subscriber in let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) @@ -106,64 +106,84 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie if let scrollPosition = scrollPosition { switch scrollPosition { - case let .Unread(unreadIndex): - var index = toView.filteredEntries.count - 1 - for entry in toView.filteredEntries { - if case .UnreadEntry = entry { - scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down) - break - } - index -= 1 - } - - if scrollToItem == nil { + case let .unread(unreadIndex): var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { - if entry.index >= unreadIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down) + if case .UnreadEntry = entry { + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default, directionHint: .Down) break } index -= 1 } - } - - if scrollToItem == nil { - var index = 0 - for entry in toView.filteredEntries.reversed() { - if entry.index < unreadIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down) + + if scrollToItem == nil { + var index = toView.filteredEntries.count - 1 + for entry in toView.filteredEntries { + if entry.index >= unreadIndex { + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default, directionHint: .Down) + break + } + index -= 1 + } + } + + if scrollToItem == nil { + var index = 0 + for entry in toView.filteredEntries.reversed() { + if entry.index < unreadIndex { + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: false, curve: .Default, directionHint: .Down) + break + } + index += 1 + } + } + case let .positionRestoration(scrollIndex, relativeOffset): + var index = toView.filteredEntries.count - 1 + for entry in toView.filteredEntries { + if entry.index >= scrollIndex { + scrollToItem = ListViewScrollToItem(index: index, position: .top(relativeOffset), animated: false, curve: .Default, directionHint: .Down) break } - index += 1 + index -= 1 } - } - case let .Index(scrollIndex, position, directionHint, animated): - if case .Center = position { - scrolledToIndex = scrollIndex - } - var index = toView.filteredEntries.count - 1 - for entry in toView.filteredEntries { - if entry.index >= scrollIndex { - scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) - break + + if scrollToItem == nil { + var index = 0 + for entry in toView.filteredEntries.reversed() { + if entry.index < scrollIndex { + scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default, directionHint: .Down) + break + } + index += 1 + } } - index -= 1 - } - - if scrollToItem == nil { - var index = 0 - for entry in toView.filteredEntries.reversed() { - if entry.index < scrollIndex { + case let .index(scrollIndex, position, directionHint, animated): + if case .center = position { + scrolledToIndex = scrollIndex + } + var index = toView.filteredEntries.count - 1 + for entry in toView.filteredEntries { + if entry.index >= scrollIndex { scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) break } - index += 1 + index -= 1 + } + + if scrollToItem == nil { + var index = 0 + for entry in toView.filteredEntries.reversed() { + if entry.index < scrollIndex { + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) + break + } + index += 1 + } } - } } } - subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: cachedData, scrolledToIndex: scrolledToIndex)) + subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: cachedData, readStateData: readStateData, scrolledToIndex: scrolledToIndex)) subscriber.putCompletion() return EmptyDisposable diff --git a/TelegramUI/PresentationPasscodeSettings.swift b/TelegramUI/PresentationPasscodeSettings.swift index f1bb88a00a..51cf7e2305 100644 --- a/TelegramUI/PresentationPasscodeSettings.swift +++ b/TelegramUI/PresentationPasscodeSettings.swift @@ -15,12 +15,12 @@ public struct PresentationPasscodeSettings: PreferencesEntry, Equatable { self.autolockTimeout = autolockTimeout } - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { self.enableBiometrics = decoder.decodeInt32ForKey("b", orElse: 0) != 0 self.autolockTimeout = decoder.decodeOptionalInt32ForKey("al") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.enableBiometrics ? 1 : 0, forKey: "s") if let autolockTimeout = self.autolockTimeout { encoder.encodeInt32(autolockTimeout, forKey: "al") diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index 43821e4c52..72cae11e18 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -98,6 +98,9 @@ enum PresentationResourceKey: Int32 { case chatInputTextFieldClearImage case chatInputPanelSendButtonImage case chatInputPanelVoiceButtonImage + case chatInputPanelVideoButtonImage + case chatInputPanelVoiceActiveButtonImage + case chatInputPanelVideoActiveButtonImage case chatInputPanelAttachmentButtonImage case chatInputPanelMediaRecordingDotImage case chatInputPanelMediaRecordingCancelArrowImage diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index 18a079502b..a315d38367 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -339,6 +339,24 @@ struct PresentationResourcesChat { }) } + static func chatInputPanelVideoButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelVideoButtonImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconVideo"), color: theme.chat.inputPanel.panelControlColor) + }) + } + + static func chatInputPanelVoiceActiveButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelVoiceActiveButtonImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: .white) + }) + } + + static func chatInputPanelVideoActiveButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelVideoActiveButtonImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconVideo"), color: .white) + }) + } + static func chatInputPanelAttachmentButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputPanelAttachmentButtonImage.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconAttachment"), color: theme.chat.inputPanel.panelControlColor) diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index ae57ac3b64..a0c2d8fc74 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -7,7 +7,7 @@ public enum PresentationThemeParsingError: Error { case generic } -private func parseColor(_ decoder: Decoder, _ key: String) throws -> UIColor { +private func parseColor(_ decoder: PostboxDecoder, _ key: String) throws -> UIColor { if let value = decoder.decodeOptionalInt32ForKey(key) { return UIColor(argb: UInt32(bitPattern: value)) } else { @@ -36,7 +36,7 @@ public final class PresentationThemeRootTabBar { self.badgeTextColor = badgeTextColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.backgroundColor = try parseColor(decoder, "backgroundColor") self.separatorColor = try parseColor(decoder, "separatorColor") self.iconColor = try parseColor(decoder, "iconColor") @@ -47,7 +47,7 @@ public final class PresentationThemeRootTabBar { self.badgeTextColor = try parseColor(decoder, "badgeTextColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -81,7 +81,7 @@ public final class PresentationThemeRootNavigationStatusBar { self.style = style } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { if let styleValue = decoder.decodeOptionalInt32ForKey("style"), let style = PresentationThemeStatusBarStyle(rawValue: styleValue) { self.style = style } else { @@ -89,7 +89,7 @@ public final class PresentationThemeRootNavigationStatusBar { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.style.rawValue, forKey: "style") } } @@ -102,8 +102,10 @@ public final class PresentationThemeRootNavigationBar { public let accentTextColor: UIColor public let backgroundColor: UIColor public let separatorColor: UIColor + public let badgeBackgroundColor: UIColor + public let badgeTextColor: UIColor - public init(buttonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor) { + public init(buttonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeTextColor: UIColor) { self.buttonColor = buttonColor self.primaryTextColor = primaryTextColor self.secondaryTextColor = secondaryTextColor @@ -111,9 +113,11 @@ public final class PresentationThemeRootNavigationBar { self.accentTextColor = accentTextColor self.backgroundColor = backgroundColor self.separatorColor = separatorColor + self.badgeBackgroundColor = badgeBackgroundColor + self.badgeTextColor = badgeTextColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.buttonColor = try parseColor(decoder, "buttonColor") self.primaryTextColor = try parseColor(decoder, "primaryTextColor") self.secondaryTextColor = try parseColor(decoder, "secondaryTextColor") @@ -121,9 +125,11 @@ public final class PresentationThemeRootNavigationBar { self.accentTextColor = try parseColor(decoder, "accentTextColor") self.backgroundColor = try parseColor(decoder, "backgroundColor") self.separatorColor = try parseColor(decoder, "separatorColor") + self.badgeBackgroundColor = try parseColor(decoder, "badgeBackgroundColor") + self.badgeTextColor = try parseColor(decoder, "badgeTextColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -155,7 +161,7 @@ public final class PresentationThemeActiveNavigationSearchBar { self.separatorColor = separatorColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.backgroundColor = try parseColor(decoder, "backgroundColor") self.accentColor = try parseColor(decoder, "accentColor") self.inputFillColor = try parseColor(decoder, "inputFillColor") @@ -165,7 +171,7 @@ public final class PresentationThemeActiveNavigationSearchBar { self.separatorColor = try parseColor(decoder, "separatorColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -191,7 +197,7 @@ public final class PresentationThemeRootController { self.activeNavigationSearchBar = activeNavigationSearchBar } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { if let statusBar = (try? decoder.decodeObjectForKeyThrowing("statusBar", decoder: { try PresentationThemeRootNavigationStatusBar(decoder: $0) })) as? PresentationThemeRootNavigationStatusBar { self.statusBar = statusBar } else { @@ -214,7 +220,7 @@ public final class PresentationThemeRootController { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeObjectWithEncoder(self.statusBar, encoder: { self.statusBar.encode($0) }, forKey: "statusBar") encoder.encodeObjectWithEncoder(self.tabBar, encoder: { self.tabBar.encode($0) }, forKey: "tabBar") encoder.encodeObjectWithEncoder(self.navigationBar, encoder: { self.navigationBar.encode($0) }, forKey: "navigationBar") @@ -233,13 +239,13 @@ public final class PresentationThemeSwitch { self.contentColor = contentColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.frameColor = try parseColor(decoder, "frameColor") self.handleColor = try parseColor(decoder, "handleColor") self.contentColor = try parseColor(decoder, "contentColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -291,7 +297,7 @@ public final class PresentationThemeList { self.itemSwitchColors = itemSwitchColors } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.blocksBackgroundColor = try parseColor(decoder, "blocksBackgroundColor") self.plainBackgroundColor = try parseColor(decoder, "plainBackgroundColor") self.itemPrimaryTextColor = try parseColor(decoder, "itemPrimaryTextColor") @@ -315,7 +321,7 @@ public final class PresentationThemeList { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -379,7 +385,7 @@ public final class PresentationThemeChatList { self.searchBarKeyboardColor = searchBarKeyboardColor } - init(decoder: Decoder) throws { + init(decoder: PostboxDecoder) throws { self.backgroundColor = try parseColor(decoder, "backgroundColor") self.itemSeparatorColor = try parseColor(decoder, "itemSeparatorColor") self.itemBackgroundColor = try parseColor(decoder, "itemBackgroundColor") @@ -408,7 +414,7 @@ public final class PresentationThemeChatList { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -528,7 +534,7 @@ public final class PresentationThemeChatBubble { self.actionButtonsTextColor = actionButtonsTextColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.incomingFillColor = try parseColor(decoder, "incomingFillColor") self.incomingFillHighlightedColor = try parseColor(decoder, "incomingFillHighlightedColor") self.incomingStrokeColor = try parseColor(decoder, "incomingStrokeColor") @@ -578,7 +584,7 @@ public final class PresentationThemeChatBubble { self.actionButtonsTextColor = try parseColor(decoder, "actionButtonsTextColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -616,7 +622,7 @@ public final class PresentationThemeServiceMessage { self.dateTextColor = dateTextColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.serviceMessageFillColor = try parseColor(decoder, "serviceMessageFillColor") self.serviceMessagePrimaryTextColor = try parseColor(decoder, "serviceMessagePrimaryTextColor") self.unreadBarFillColor = try parseColor(decoder, "unreadBarFillColor") @@ -627,7 +633,7 @@ public final class PresentationThemeServiceMessage { self.dateTextColor = try parseColor(decoder, "dateTextColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -689,7 +695,7 @@ public final class PresentationThemeChatInputPanel { self.keyboardColor = keyboardColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.panelBackgroundColor = try parseColor(decoder, "panelBackgroundColor") self.panelStrokeColor = try parseColor(decoder, "panelStrokeColor") self.panelControlAccentColor = try parseColor(decoder, "panelControlAccentColor") @@ -710,7 +716,7 @@ public final class PresentationThemeChatInputPanel { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -744,7 +750,7 @@ public final class PresentationThemeInputMediaPanel { self.gifsBackgroundColor = gifsBackgroundColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.panelSerapatorColor = try parseColor(decoder, "panelSerapatorColor") self.panelIconColor = try parseColor(decoder, "panelIconColor") self.panelHighlightedIconBackgroundColor = try parseColor(decoder, "panelHighlightedIconBackgroundColor") @@ -753,7 +759,7 @@ public final class PresentationThemeInputMediaPanel { self.gifsBackgroundColor = try parseColor(decoder, "gifsBackgroundColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -787,7 +793,7 @@ public final class PresentationThemeInputButtonPanel { self.buttonTextColor = buttonTextColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.panelSerapatorColor = try parseColor(decoder, "panelSerapatorColor") self.panelBackgroundColor = try parseColor(decoder, "panelBackgroundColor") self.buttonFillColor = try parseColor(decoder, "buttonFillColor") @@ -797,7 +803,7 @@ public final class PresentationThemeInputButtonPanel { self.buttonTextColor = try parseColor(decoder, "buttonTextColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -827,7 +833,7 @@ public final class PresentationThemeChatHistoryNavigation { self.badgeTextColor = badgeTextColor } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { self.fillColor = try parseColor(decoder, "fillColor") self.strokeColor = try parseColor(decoder, "strokeColor") self.foregroundColor = try parseColor(decoder, "foregroundColor") @@ -835,7 +841,7 @@ public final class PresentationThemeChatHistoryNavigation { self.badgeTextColor = try parseColor(decoder, "badgeTextColor") } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { for child in Mirror(reflecting: self).children { if let label = child.label { if let value = child.value as? UIColor { @@ -869,7 +875,7 @@ public final class PresentationThemeChat { self.historyNavigation = historyNavigation } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { if let bubble = (try? decoder.decodeObjectForKeyThrowing("bubble", decoder: { try PresentationThemeChatBubble(decoder: $0) })) as? PresentationThemeChatBubble { self.bubble = bubble } else { @@ -902,7 +908,7 @@ public final class PresentationThemeChat { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeObjectWithEncoder(self.bubble, encoder: { self.bubble.encode($0) }, forKey: "bubble") encoder.encodeObjectWithEncoder(self.serviceMessage, encoder: { self.serviceMessage.encode($0) }, forKey: "serviceMessage") encoder.encodeObjectWithEncoder(self.inputPanel, encoder: { self.inputPanel.encode($0) }, forKey: "inputPanel") @@ -927,7 +933,7 @@ public final class PresentationTheme: Equatable { self.chat = chat } - public init(decoder: Decoder) throws { + public init(decoder: PostboxDecoder) throws { if let rootController = (try? decoder.decodeObjectForKeyThrowing("rootController", decoder: { try PresentationThemeRootController(decoder: $0) })) as? PresentationThemeRootController { self.rootController = rootController } else { @@ -950,7 +956,7 @@ public final class PresentationTheme: Equatable { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeObjectWithEncoder(self.rootController, encoder: { self.rootController.encode($0) }, forKey: "list") encoder.encodeObjectWithEncoder(self.list, encoder: { self.list.encode($0) }, forKey: "list") encoder.encodeObjectWithEncoder(self.chatList, encoder: { self.chatList.encode($0) }, forKey: "chatList") diff --git a/TelegramUI/PresentationThemeSettings.swift b/TelegramUI/PresentationThemeSettings.swift index c9f66e0c94..d266130cf4 100644 --- a/TelegramUI/PresentationThemeSettings.swift +++ b/TelegramUI/PresentationThemeSettings.swift @@ -7,10 +7,10 @@ public enum PresentationBuilinThemeReference: Int32 { case dark } -public enum PresentationThemeReference: Coding, Equatable { +public enum PresentationThemeReference: PostboxCoding, Equatable { case builtin(PresentationBuilinThemeReference) - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { case 0: self = .builtin(PresentationBuilinThemeReference(rawValue: decoder.decodeInt32ForKey("t", orElse: 0))!) @@ -20,7 +20,7 @@ public enum PresentationThemeReference: Coding, Equatable { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { switch self { case let .builtin(reference): encoder.encodeInt32(0, forKey: "v") @@ -53,12 +53,12 @@ public struct PresentationThemeSettings: PreferencesEntry { self.theme = theme } - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { self.chatWallpaper = (decoder.decodeObjectForKey("w", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper) ?? .builtin self.theme = decoder.decodeObjectForKey("t", decoder: { PresentationThemeReference(decoder: $0) }) as! PresentationThemeReference } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeObject(self.chatWallpaper, forKey: "w") encoder.encodeObject(self.theme, forKey: "t") } diff --git a/TelegramUI/RadialStatusNode.swift b/TelegramUI/RadialStatusNode.swift index 6f051f59f3..f2482496b8 100644 --- a/TelegramUI/RadialStatusNode.swift +++ b/TelegramUI/RadialStatusNode.swift @@ -102,35 +102,50 @@ final class RadialStatusNode: ASControlNode { super.init() } - func transitionToState(_ state: RadialStatusNodeState, completion: @escaping () -> Void) { + func transitionToState(_ state: RadialStatusNodeState, animated: Bool = true, completion: @escaping () -> Void) { if self.state != state { self.state = state let contentNode = state.contentNode(current: self.contentNode) if contentNode !== self.contentNode { - self.transitionToContentNode(contentNode, backgroundColor: state.backgroundColor(color: self.backgroundNodeColor), completion: completion) + self.transitionToContentNode(contentNode, backgroundColor: state.backgroundColor(color: self.backgroundNodeColor), animated: animated, completion: completion) } else { - self.transitionToBackgroundColor(state.backgroundColor(color: self.backgroundNodeColor), completion: completion) + self.transitionToBackgroundColor(state.backgroundColor(color: self.backgroundNodeColor), animated: animated, completion: completion) } } } - private func transitionToContentNode(_ node: RadialStatusContentNode?, backgroundColor: UIColor?, completion: @escaping () -> Void) { + private func transitionToContentNode(_ node: RadialStatusContentNode?, backgroundColor: UIColor?, animated: Bool, completion: @escaping () -> Void) { if let contentNode = self.contentNode { self.nextContentNode = node contentNode.enqueueReadyForTransition { [weak contentNode, weak self] in if let strongSelf = self, let contentNode = contentNode, strongSelf.contentNode === contentNode { - contentNode.animateOut { [weak contentNode] in - contentNode?.removeFromSupernode() + if animated { + contentNode.animateOut { [weak contentNode] in + contentNode?.removeFromSupernode() + } + strongSelf.contentNode = strongSelf.nextContentNode + if let contentNode = strongSelf.contentNode { + strongSelf.addSubnode(contentNode) + contentNode.frame = strongSelf.bounds + if strongSelf.isNodeLoaded { + contentNode.layout() + contentNode.animateIn() + } + } + strongSelf.transitionToBackgroundColor(backgroundColor, animated: animated, completion: completion) + } else { + contentNode.removeFromSupernode() + strongSelf.contentNode = strongSelf.nextContentNode + if let contentNode = strongSelf.contentNode { + strongSelf.addSubnode(contentNode) + contentNode.frame = strongSelf.bounds + if strongSelf.isNodeLoaded { + contentNode.layout() + } + } + strongSelf.transitionToBackgroundColor(backgroundColor, animated: animated, completion: completion) } - strongSelf.contentNode = strongSelf.nextContentNode - if let contentNode = strongSelf.contentNode { - strongSelf.addSubnode(contentNode) - contentNode.frame = strongSelf.bounds - contentNode.layout() - contentNode.animateIn() - } - strongSelf.transitionToBackgroundColor(backgroundColor, completion: completion) } } } else { @@ -139,11 +154,11 @@ final class RadialStatusNode: ASControlNode { contentNode.frame = self.bounds self.addSubnode(contentNode) } - self.transitionToBackgroundColor(backgroundColor, completion: completion) + self.transitionToBackgroundColor(backgroundColor, animated: animated, completion: completion) } } - private func transitionToBackgroundColor(_ color: UIColor?, completion: @escaping () -> Void) { + private func transitionToBackgroundColor(_ color: UIColor?, animated: Bool, completion: @escaping () -> Void) { let currentColor = self.backgroundNode?.color var updated = false @@ -167,10 +182,15 @@ final class RadialStatusNode: ASControlNode { } } else if let backgroundNode = self.backgroundNode { self.backgroundNode = nil - backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak backgroundNode] _ in - backgroundNode?.removeFromSupernode() + if animated { + backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak backgroundNode] _ in + backgroundNode?.removeFromSupernode() + completion() + }) + } else { + backgroundNode.removeFromSupernode() completion() - }) + } } } else { completion() diff --git a/TelegramUI/ShareControllerNode.swift b/TelegramUI/ShareControllerNode.swift index c3638d1216..27bf5d16a7 100644 --- a/TelegramUI/ShareControllerNode.swift +++ b/TelegramUI/ShareControllerNode.swift @@ -223,6 +223,14 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) diff --git a/TelegramUI/StickerPackPreviewControllerNode.swift b/TelegramUI/StickerPackPreviewControllerNode.swift index 1d04de5800..ee38910aa1 100644 --- a/TelegramUI/StickerPackPreviewControllerNode.swift +++ b/TelegramUI/StickerPackPreviewControllerNode.swift @@ -188,6 +188,14 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol self.contentGridNode.view.addGestureRecognizer(longTapRecognizer) } + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) diff --git a/TelegramUI/TelegramInitializeLegacyComponents.swift b/TelegramUI/TelegramInitializeLegacyComponents.swift index 6d8d672a32..6359485b50 100644 --- a/TelegramUI/TelegramInitializeLegacyComponents.swift +++ b/TelegramUI/TelegramInitializeLegacyComponents.swift @@ -39,6 +39,22 @@ private final class LegacyComponentsAccessCheckerImpl: NSObject, LegacyComponent } } +private func encodeText(_ string: String, _ key: Int) -> String { + var result = "" + for c in string.unicodeScalars { + result.append(Character(UnicodeScalar(UInt32(Int(c.value) + key))!)) + } + return result +} + +private let keyboardWindowClass: AnyClass? = { + if #available(iOS 9.0, *) { + return NSClassFromString(encodeText("VJSfnpufLfzcpbseXjoepx", -1)) + } else { + return NSClassFromString(encodeText("VJUfyuFggfdutXjoepx", -1)) + } +}() + private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyComponentsGlobalsProvider { func log(_ string: String!) { print(string) @@ -57,6 +73,15 @@ private final class LegacyComponentsGlobalsProviderImpl: NSObject, LegacyCompone } public func applicationKeyboardWindow() -> UIWindow! { + guard let keyboardWindowClass = keyboardWindowClass else { + return nil + } + + for window in legacyComponentsApplication.windows { + if window.isKind(of: keyboardWindowClass) { + return window + } + } return nil } diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index 0ce80a8099..aedecda1f3 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -355,7 +355,7 @@ private func stringForBlockAction(strings: PresentationStrings, action: Destruct } } -private func userInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, state: UserInfoState, peerChatState: Coding?) -> [UserInfoEntry] { +private func userInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, state: UserInfoState, peerChatState: PostboxCoding?) -> [UserInfoEntry] { var entries: [UserInfoEntry] = [] guard let peer = view.peers[view.peerId], let user = peerViewMainPeer(view) as? TelegramUser else { diff --git a/TelegramUI/VoiceCallSettings.swift b/TelegramUI/VoiceCallSettings.swift index 6733407f59..ac5383dc5d 100644 --- a/TelegramUI/VoiceCallSettings.swift +++ b/TelegramUI/VoiceCallSettings.swift @@ -19,11 +19,11 @@ public struct VoiceCallSettings: PreferencesEntry, Equatable { self.dataSaving = dataSaving } - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { self.dataSaving = VoiceCallDataSaving(rawValue: decoder.decodeInt32ForKey("ds", orElse: 0))! } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.dataSaving.rawValue, forKey: "ds") } diff --git a/TelegramUI/Wallpapers.swift b/TelegramUI/Wallpapers.swift index 5edebc8e06..4b05ce1c4c 100644 --- a/TelegramUI/Wallpapers.swift +++ b/TelegramUI/Wallpapers.swift @@ -8,7 +8,7 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { case color(Int32) case image([TelegramMediaImageRepresentation]) - public init(decoder: Decoder) { + public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { case 0: self = .builtin @@ -22,7 +22,7 @@ public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { } } - public func encode(_ encoder: Encoder) { + public func encode(_ encoder: PostboxEncoder) { switch self { case .builtin: encoder.encodeInt32(0, forKey: "v") diff --git a/TelegramUI/ZoomableContentGalleryItemNode.swift b/TelegramUI/ZoomableContentGalleryItemNode.swift index 53b98918e2..e7ec7fb5a7 100644 --- a/TelegramUI/ZoomableContentGalleryItemNode.swift +++ b/TelegramUI/ZoomableContentGalleryItemNode.swift @@ -23,6 +23,9 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate { override init() { self.scrollView = UIScrollView() + if #available(iOSApplicationExtension 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } super.init() diff --git a/third-party/opus/include/opus/opus.h b/third-party/opus/include/opus/opus.h index b0bdf6f2df..027e09935b 100644 --- a/third-party/opus/include/opus/opus.h +++ b/third-party/opus/include/opus/opus.h @@ -198,7 +198,7 @@ OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_encoder_get_size(int channels); * This must be one of 8000, 12000, 16000, * 24000, or 48000. * @param [in] channels int: Number of channels (1 or 2) in input signal - * @param [in] application int: Coding mode (@ref OPUS_APPLICATION_VOIP/@ref OPUS_APPLICATION_AUDIO/@ref OPUS_APPLICATION_RESTRICTED_LOWDELAY) + * @param [in] application int: PostboxCoding mode (@ref OPUS_APPLICATION_VOIP/@ref OPUS_APPLICATION_AUDIO/@ref OPUS_APPLICATION_RESTRICTED_LOWDELAY) * @param [out] error int*: @ref opus_errorcodes * @note Regardless of the sampling rate and number channels selected, the Opus encoder * can switch to a lower audio bandwidth or number of channels if the bitrate @@ -222,7 +222,7 @@ OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusEncoder *opus_encoder_create( * This must be one of 8000, 12000, 16000, * 24000, or 48000. * @param [in] channels int: Number of channels (1 or 2) in input signal - * @param [in] application int: Coding mode (OPUS_APPLICATION_VOIP/OPUS_APPLICATION_AUDIO/OPUS_APPLICATION_RESTRICTED_LOWDELAY) + * @param [in] application int: PostboxCoding mode (OPUS_APPLICATION_VOIP/OPUS_APPLICATION_AUDIO/OPUS_APPLICATION_RESTRICTED_LOWDELAY) * @retval #OPUS_OK Success or @ref opus_errorcodes */ OPUS_EXPORT int opus_encoder_init(