From 1fa79c3c2deea4242c578a92e8348d5c1cdbac57 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 12 Mar 2018 22:53:22 +0400 Subject: [PATCH] no message --- .../Chat List/AddIcon.imageset/Contents.json | 21 + .../ModernNavigationAddButtonIcon@2x.png | Bin 0 -> 112 bytes TelegramUI.xcodeproj/project.pbxproj | 48 ++ TelegramUI/AvatarNode.swift | 6 +- TelegramUI/ChatBotStartInputPanelNode.swift | 2 +- .../ChatChannelSubscriberInputPanelNode.swift | 6 +- TelegramUI/ChatController.swift | 58 +- TelegramUI/ChatControllerInteraction.swift | 6 +- TelegramUI/ChatControllerNode.swift | 20 +- TelegramUI/ChatEmptyNode.swift | 8 +- TelegramUI/ChatInfoTitlePanelNode.swift | 2 +- .../ChatInstantVideoMessageDurationNode.swift | 208 ++++++ .../ChatInterfaceInputContextPanels.swift | 4 +- TelegramUI/ChatInterfaceInputContexts.swift | 6 +- .../ChatInterfaceStateContextMenus.swift | 6 +- .../ChatInterfaceStateContextQueries.swift | 4 +- .../ChatInterfaceStateInputPanels.swift | 6 +- .../ChatInterfaceStateNavigationButtons.swift | 4 +- TelegramUI/ChatInterfaceTitlePanelNodes.swift | 2 +- TelegramUI/ChatMediaInputGifPane.swift | 6 +- TelegramUI/ChatMessageActionItemNode.swift | 126 ++-- TelegramUI/ChatMessageBubbleContentNode.swift | 15 +- TelegramUI/ChatMessageBubbleItemNode.swift | 101 ++- .../ChatMessageCallBubbleContentNode.swift | 2 +- .../ChatMessageContactBubbleContentNode.swift | 2 +- ...entLogPreviousDescriptionContentNode.swift | 2 +- ...ssageEventLogPreviousLinkContentNode.swift | 2 +- ...geEventLogPreviousMessageContentNode.swift | 2 +- .../ChatMessageFileBubbleContentNode.swift | 2 +- .../ChatMessageGameBubbleContentNode.swift | 2 +- .../ChatMessageInstantVideoItemNode.swift | 64 +- .../ChatMessageInteractiveMediaNode.swift | 38 +- .../ChatMessageInvoiceBubbleContentNode.swift | 2 +- TelegramUI/ChatMessageItem.swift | 47 +- .../ChatMessageMapBubbleContentNode.swift | 2 +- .../ChatMessageMediaBubbleContentNode.swift | 16 +- TelegramUI/ChatMessageReplyInfoNode.swift | 5 +- .../ChatMessageTextBubbleContentNode.swift | 2 +- ...hatMessageThrottledProcessingManager.swift | 2 +- .../ChatMessageWebpageBubbleContentNode.swift | 2 +- .../ChatPresentationInterfaceState.swift | 56 +- .../ChatRecentActionsControllerNode.swift | 2 +- TelegramUI/ChatReportPeerTitlePanelNode.swift | 2 +- TelegramUI/ChatSearchInputPanelNode.swift | 4 +- TelegramUI/ChatTextInputPanelNode.swift | 2 +- TelegramUI/ContactListNode.swift | 36 +- TelegramUI/ContactsController.swift | 11 + TelegramUI/ContactsControllerNode.swift | 11 +- TelegramUI/ContactsPeerItem.swift | 24 +- TelegramUI/CreateContactController.swift | 380 +++++++++++ TelegramUI/DebugController.swift | 35 +- TelegramUI/DeclareEncodables.swift | 1 + .../DefaultDarkAccentPresentationTheme.swift | 4 +- TelegramUI/DefaultDarkPresentationTheme.swift | 4 +- TelegramUI/DefaultPresentationTheme.swift | 4 +- TelegramUI/DeviceContactsManager.swift | 158 ++++- TelegramUI/FetchVideoMediaResource.swift | 2 +- TelegramUI/GalleryController.swift | 4 +- TelegramUI/GalleryControllerNode.swift | 7 +- TelegramUI/InviteContactsController.swift | 169 +++++ TelegramUI/InviteContactsControllerNode.swift | 615 ++++++++++++++++++ TelegramUI/InviteContactsCountPanelNode.swift | 103 +++ .../ItemListEditableDeleteControlNode.swift | 2 +- TelegramUI/ItemListTextWithLabelItem.swift | 18 +- TelegramUI/LegacyInstantVideoController.swift | 10 +- TelegramUI/LegacyLiveUploadInterface.swift | 67 ++ TelegramUI/MediaInputSettings.swift | 53 ++ ...MediaNavigationAccessoryItemListNode.swift | 2 +- TelegramUI/NativeVideoContent.swift | 10 +- TelegramUI/OpenChatMessage.swift | 24 +- TelegramUI/OpenUrl.swift | 17 + TelegramUI/OverlayPlayerControllerNode.swift | 2 +- .../PeerMediaCollectionController.swift | 2 +- TelegramUI/PhoneInputNode.swift | 12 +- TelegramUI/PreferencesKeys.swift | 2 + TelegramUI/PresentationData.swift | 17 +- TelegramUI/PresentationResourceKey.swift | 2 + .../PresentationResourcesItemList.swift | 15 + .../PresentationResourcesRootController.swift | 6 + TelegramUI/PresentationTheme.swift | 4 +- TelegramUI/RadialStatusNode.swift | 11 +- ...RadialStatusSecretTimeoutContentNode.swift | 221 +++++++ TelegramUI/ReplyAccessoryPanelNode.swift | 5 +- ...retChatHandshakeStatusInputPanelNode.swift | 11 +- TelegramUI/SecretMediaPreviewController.swift | 323 ++++++++- .../SecretMediaPreviewControllerNode.swift | 71 +- .../SecretMediaPreviewFooterContentNode.swift | 44 ++ TelegramUI/SinglePhoneInputNode.swift | 155 +++++ TelegramUI/TelegramApplicationContext.swift | 18 +- TelegramUI/ThemeSettingsChatPreviewItem.swift | 2 +- TelegramUI/UniversalVideoCalleryItem.swift | 22 +- TelegramUI/UserInfoController.swift | 76 ++- .../UserInfoEditingPhoneActionItem.swift | 226 +++++++ TelegramUI/UserInfoEditingPhoneItem.swift | 277 ++++++++ 94 files changed, 3724 insertions(+), 496 deletions(-) create mode 100644 Images.xcassets/Chat List/AddIcon.imageset/Contents.json create mode 100644 Images.xcassets/Chat List/AddIcon.imageset/ModernNavigationAddButtonIcon@2x.png create mode 100644 TelegramUI/ChatInstantVideoMessageDurationNode.swift create mode 100644 TelegramUI/CreateContactController.swift create mode 100644 TelegramUI/InviteContactsController.swift create mode 100644 TelegramUI/InviteContactsControllerNode.swift create mode 100644 TelegramUI/InviteContactsCountPanelNode.swift create mode 100644 TelegramUI/LegacyLiveUploadInterface.swift create mode 100644 TelegramUI/MediaInputSettings.swift create mode 100644 TelegramUI/RadialStatusSecretTimeoutContentNode.swift create mode 100644 TelegramUI/SecretMediaPreviewFooterContentNode.swift create mode 100644 TelegramUI/SinglePhoneInputNode.swift create mode 100644 TelegramUI/UserInfoEditingPhoneActionItem.swift create mode 100644 TelegramUI/UserInfoEditingPhoneItem.swift diff --git a/Images.xcassets/Chat List/AddIcon.imageset/Contents.json b/Images.xcassets/Chat List/AddIcon.imageset/Contents.json new file mode 100644 index 0000000000..b796aac945 --- /dev/null +++ b/Images.xcassets/Chat List/AddIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ModernNavigationAddButtonIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat List/AddIcon.imageset/ModernNavigationAddButtonIcon@2x.png b/Images.xcassets/Chat List/AddIcon.imageset/ModernNavigationAddButtonIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4554c428c93eb6f50fb99bb108e9aa381788270d GIT binary patch literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^${@_h3?x;=6{Ua_TYyi9E0AWWdwOK|ZwDZQu_VYZ zn8D%MjWi%f!qdeuq=GRyA)$id$QI@Q%mo}gQu&X%Q~lo FCII=e9033T literal 0 HcmV?d00001 diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 169e803849..dfc0bdc8f5 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -65,6 +65,10 @@ D02F4AE91FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F4AE81FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift */; }; D02F4AF01FD4C46D004DFBAE /* SystemVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F4AEF1FD4C46D004DFBAE /* SystemVideoContent.swift */; }; D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033C60A1F0D306E0044EABA /* TelegramVideoNode.swift */; }; + D0380DA9204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0380DA8204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift */; }; + D0380DAB204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0380DAA204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift */; }; + D0380DAD204ED434000414AB /* LegacyLiveUploadInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0380DAC204ED434000414AB /* LegacyLiveUploadInterface.swift */; }; + D0380DB8204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0380DB7204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift */; }; D03AA4DF202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */; }; D03AA4E5202DF8840056C405 /* StickerPreviewPeekContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E4202DF8840056C405 /* StickerPreviewPeekContent.swift */; }; D03AA4E7202DFB160056C405 /* ItemListEditableReorderControlNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03AA4E6202DFB160056C405 /* ItemListEditableReorderControlNode.swift */; }; @@ -198,6 +202,14 @@ 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 */; }; + D0B2F76220506E2A00D3BFB9 /* MediaInputSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B2F76120506E2A00D3BFB9 /* MediaInputSettings.swift */; }; + D0B2F7642052739100D3BFB9 /* CreateContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B2F7632052739100D3BFB9 /* CreateContactController.swift */; }; + D0B2F76820528E3D00D3BFB9 /* UserInfoEditingPhoneActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B2F76720528E3D00D3BFB9 /* UserInfoEditingPhoneActionItem.swift */; }; + D0B2F76A2052920D00D3BFB9 /* UserInfoEditingPhoneItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B2F7692052920D00D3BFB9 /* UserInfoEditingPhoneItem.swift */; }; + D0B2F76C2052A7D600D3BFB9 /* SinglePhoneInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B2F76B2052A7D600D3BFB9 /* SinglePhoneInputNode.swift */; }; + D0B2F76E2052B59F00D3BFB9 /* InviteContactsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B2F76D2052B59F00D3BFB9 /* InviteContactsController.swift */; }; + D0B2F7702052B5A800D3BFB9 /* InviteContactsControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B2F76F2052B5A800D3BFB9 /* InviteContactsControllerNode.swift */; }; + D0B2F7722052D0DD00D3BFB9 /* InviteContactsCountPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B2F7712052D0DD00D3BFB9 /* InviteContactsCountPanelNode.swift */; }; D0B37C5C1F8D22AE004252DF /* ThemeSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B37C5B1F8D22AE004252DF /* ThemeSettingsController.swift */; }; D0B37C5E1F8D26A8004252DF /* ThemeSettingsChatPreviewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B37C5D1F8D26A8004252DF /* ThemeSettingsChatPreviewItem.swift */; }; D0B37C601F8D286E004252DF /* ThemeSettingsFontSizeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B37C5F1F8D286E004252DF /* ThemeSettingsFontSizeItem.swift */; }; @@ -1037,6 +1049,10 @@ D03120F51DA534C1006A2A60 /* ItemListActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListActionItem.swift; sourceTree = ""; }; D033C60A1F0D306E0044EABA /* TelegramVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramVideoNode.swift; sourceTree = ""; }; D033FEAA1E61BFC100644997 /* GroupAdminsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupAdminsController.swift; sourceTree = ""; }; + D0380DA8204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretMediaPreviewFooterContentNode.swift; sourceTree = ""; }; + D0380DAA204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialStatusSecretTimeoutContentNode.swift; sourceTree = ""; }; + D0380DAC204ED434000414AB /* LegacyLiveUploadInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyLiveUploadInterface.swift; sourceTree = ""; }; + D0380DB7204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInstantVideoMessageDurationNode.swift; sourceTree = ""; }; D03922A61DF70E3F000F2CE9 /* MediaPlayerScrubbingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerScrubbingNode.swift; sourceTree = ""; }; D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = ""; }; D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingTimeNode.swift; sourceTree = ""; }; @@ -1376,6 +1392,14 @@ 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 = ""; }; + D0B2F76120506E2A00D3BFB9 /* MediaInputSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInputSettings.swift; sourceTree = ""; }; + D0B2F7632052739100D3BFB9 /* CreateContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateContactController.swift; sourceTree = ""; }; + D0B2F76720528E3D00D3BFB9 /* UserInfoEditingPhoneActionItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoEditingPhoneActionItem.swift; sourceTree = ""; }; + D0B2F7692052920D00D3BFB9 /* UserInfoEditingPhoneItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoEditingPhoneItem.swift; sourceTree = ""; }; + D0B2F76B2052A7D600D3BFB9 /* SinglePhoneInputNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SinglePhoneInputNode.swift; sourceTree = ""; }; + D0B2F76D2052B59F00D3BFB9 /* InviteContactsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteContactsController.swift; sourceTree = ""; }; + D0B2F76F2052B5A800D3BFB9 /* InviteContactsControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteContactsControllerNode.swift; sourceTree = ""; }; + D0B2F7712052D0DD00D3BFB9 /* InviteContactsCountPanelNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteContactsCountPanelNode.swift; sourceTree = ""; }; D0B37C5B1F8D22AE004252DF /* ThemeSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingsController.swift; sourceTree = ""; }; D0B37C5D1F8D26A8004252DF /* ThemeSettingsChatPreviewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingsChatPreviewItem.swift; sourceTree = ""; }; D0B37C5F1F8D286E004252DF /* ThemeSettingsFontSizeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingsFontSizeItem.swift; sourceTree = ""; }; @@ -1873,6 +1897,7 @@ children = ( D00C7CDB1E3776E50080C3D5 /* SecretMediaPreviewController.swift */, D00C7CDD1E37770A0080C3D5 /* SecretMediaPreviewControllerNode.swift */, + D0380DA8204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift */, ); name = "Secret Preview"; sourceTree = ""; @@ -1921,6 +1946,7 @@ D01776B71F1D6FB30044446D /* RadialProgressContentNode.swift */, D0A723531FC3B40E0094D167 /* RadialCheckContentNode.swift */, D01776B91F1D704F0044446D /* RadialStatusIconContentNode.swift */, + D0380DAA204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift */, ); name = "Radial Status"; sourceTree = ""; @@ -2466,6 +2492,7 @@ D007019B2029E8F2006B9E34 /* LegqacyICloudFileController.swift */, D07ABBA4202A14BC003671DE /* LegacyImagePicker.swift */, D07ABBAA202A1BD1003671DE /* LegacyWallpaperEditor.swift */, + D0380DAC204ED434000414AB /* LegacyLiveUploadInterface.swift */, ); name = "Legacy Components"; sourceTree = ""; @@ -2543,6 +2570,7 @@ D073D2DA1FB61DA9009E1DA2 /* CallListSettings.swift */, D09250031FE5363D003F693F /* ExperimentalSettings.swift */, D056CD711FF1569800880D28 /* MusicPlaybackSettings.swift */, + D0B2F76120506E2A00D3BFB9 /* MediaInputSettings.swift */, D048B33A203C777500038D05 /* RenderedTotalUnreadCount.swift */, ); name = Settings; @@ -3341,6 +3369,7 @@ D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */, D0BC38621E3F9EFA0044D6FE /* EditableTokenListNode.swift */, D050F2121E48B61500988324 /* PhoneInputNode.swift */, + D0B2F76B2052A7D600D3BFB9 /* SinglePhoneInputNode.swift */, D0DA44531E4E7302005FDCA7 /* ProgressNavigationButtonNode.swift */, D0C0B58F1EDB505E000F4D2C /* ActivityIndicator.swift */, D0C0B5911EDC5A3B000F4D2C /* LinkHighlightingNode.swift */, @@ -3531,6 +3560,7 @@ D0E8174B2011F8A300B82BBB /* ChatMessageEventLogPreviousMessageContentNode.swift */, D0E8174D2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift */, D0E8174F2012027900B82BBB /* ChatMessageEventLogPreviousLinkContentNode.swift */, + D0380DB7204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift */, ); name = Items; sourceTree = ""; @@ -3627,6 +3657,12 @@ D0F69E6D1D6B8C340046BCD6 /* ContactsController.swift */, D0F69E6E1D6B8C340046BCD6 /* ContactsControllerNode.swift */, D0F69E701D6B8C340046BCD6 /* ContactsSearchContainerNode.swift */, + D0B2F7632052739100D3BFB9 /* CreateContactController.swift */, + D0B2F76720528E3D00D3BFB9 /* UserInfoEditingPhoneActionItem.swift */, + D0B2F7692052920D00D3BFB9 /* UserInfoEditingPhoneItem.swift */, + D0B2F76D2052B59F00D3BFB9 /* InviteContactsController.swift */, + D0B2F76F2052B5A800D3BFB9 /* InviteContactsControllerNode.swift */, + D0B2F7712052D0DD00D3BFB9 /* InviteContactsCountPanelNode.swift */, ); name = Contacts; sourceTree = ""; @@ -4060,9 +4096,11 @@ D0F0AAE21EC20EF8005EE2A5 /* CallControllerStatusNode.swift in Sources */, D0EC6CB41EB9F58800EBF1C3 /* timing.c in Sources */, D0EC6CB51EB9F58800EBF1C3 /* platform_log.c in Sources */, + D0B2F76C2052A7D600D3BFB9 /* SinglePhoneInputNode.swift in Sources */, D04281F6200E5AC2009DDE36 /* ChatRecentActionsControllerNode.swift in Sources */, D0EC6CB61EB9F58800EBF1C3 /* RMGeometry.m in Sources */, D079FCDD1F05C4F20038FADE /* LocalAuth.swift in Sources */, + D0B2F76820528E3D00D3BFB9 /* UserInfoEditingPhoneActionItem.swift in Sources */, D0EC6CB71EB9F58800EBF1C3 /* RMIntroPageView.m in Sources */, D0EC6CB81EB9F58800EBF1C3 /* RMIntroViewController.m in Sources */, D0EC6CB91EB9F58800EBF1C3 /* RMLoginViewController.m in Sources */, @@ -4126,6 +4164,7 @@ D0EC6CDA1EB9F58800EBF1C3 /* NumericFormat.swift in Sources */, D0EC6CDB1EB9F58800EBF1C3 /* Markdown.swift in Sources */, D0471B641EFEB5CB0074D609 /* BotPaymentItemNode.swift in Sources */, + D0380DB8204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift in Sources */, D01C7F001EF9D45B008305F1 /* DeviceContactsManager.swift in Sources */, D0EC6CDC1EB9F58800EBF1C3 /* TelegramAccountAuxiliaryMethods.swift in Sources */, D01BAA1A1ECC8E0D00295217 /* CallListControllerNode.swift in Sources */, @@ -4312,9 +4351,11 @@ D0EC6D4B1EB9F58800EBF1C3 /* ChatListNode.swift in Sources */, D0EC6D4D1EB9F58800EBF1C3 /* ChatListHoleItem.swift in Sources */, D0EC6D4E1EB9F58800EBF1C3 /* ChatListItem.swift in Sources */, + D0B2F76A2052920D00D3BFB9 /* UserInfoEditingPhoneItem.swift in Sources */, D0EC6D4F1EB9F58800EBF1C3 /* ChatListSearchItem.swift in Sources */, D0EC6D501EB9F58800EBF1C3 /* ChatListNodeEntries.swift in Sources */, D0EC6D511EB9F58800EBF1C3 /* ChatListViewTransition.swift in Sources */, + D0380DAB204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift in Sources */, D0EC6D521EB9F58800EBF1C3 /* ChatListNodeLocation.swift in Sources */, D0EC6D531EB9F58800EBF1C3 /* ChatHistoryViewForLocation.swift in Sources */, D06BB8821F58994B0084FC30 /* LegacyInstantVideoController.swift in Sources */, @@ -4327,6 +4368,7 @@ D0EC6D561EB9F58800EBF1C3 /* ChatHistoryNode.swift in Sources */, D0EC6D571EB9F58800EBF1C3 /* ChatHistoryListNode.swift in Sources */, D0EC6D581EB9F58800EBF1C3 /* ChatHistoryGridNode.swift in Sources */, + D0B2F76E2052B59F00D3BFB9 /* InviteContactsController.swift in Sources */, D0EC6D591EB9F58800EBF1C3 /* ChatMessageThrottledProcessingManager.swift in Sources */, D06E0F8E1F79ABFB003CF3DD /* ChatLoadingNode.swift in Sources */, D0EC6D5A1EB9F58800EBF1C3 /* ListMessageItem.swift in Sources */, @@ -4375,6 +4417,7 @@ D0EC6D761EB9F58800EBF1C3 /* ChatListController.swift in Sources */, D0EC6D771EB9F58800EBF1C3 /* ChatListControllerNode.swift in Sources */, D0EC6D781EB9F58800EBF1C3 /* NetworkStatusTitleView.swift in Sources */, + D0B2F7642052739100D3BFB9 /* CreateContactController.swift in Sources */, D048EA8D1F4F299A00188713 /* InstantPageSettingsSwitchItemNode.swift in Sources */, D0E9B9F41F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift in Sources */, D0F6800A1EE750EE000E5906 /* ChannelBannedMemberController.swift in Sources */, @@ -4428,6 +4471,7 @@ D0EC6D951EB9F58900EBF1C3 /* ChatMessageInteractiveFileNode.swift in Sources */, D01A21B11F3A050E00DDA104 /* InstantPageNavigationBar.swift in Sources */, D0EC6D961EB9F58900EBF1C3 /* ChatMessageInteractiveMediaNode.swift in Sources */, + D0B2F7722052D0DD00D3BFB9 /* InviteContactsCountPanelNode.swift in Sources */, D0EC6D971EB9F58900EBF1C3 /* ChatMessageItem.swift in Sources */, D0E8175720122DAD00B82BBB /* ChatRecentActionsSearchNavigationContentNode.swift in Sources */, D0E8174E2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift in Sources */, @@ -4531,6 +4575,7 @@ D0EC6DD71EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */, D0EC6DD81EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelButtonItem.swift in Sources */, D04281F4200E5AB0009DDE36 /* ChatRecentActionsController.swift in Sources */, + D0B2F76220506E2A00D3BFB9 /* MediaInputSettings.swift in Sources */, D064EF871F69A06F00AC0398 /* MessageContentKind.swift in Sources */, D020A9DA1FEAE675008C66F7 /* OverlayPlayerController.swift in Sources */, D0E817472010E62F00B82BBB /* MergeLists.swift in Sources */, @@ -4614,6 +4659,7 @@ D0EC6E0F1EB9F58900EBF1C3 /* MapInputController.swift in Sources */, D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */, D0EC6E101EB9F58900EBF1C3 /* MapInputControllerNode.swift in Sources */, + D0380DA9204E9C81000414AB /* SecretMediaPreviewFooterContentNode.swift in Sources */, D0AFCC7B1F4C8D39000720C6 /* InstantPageSlideshowItemNode.swift in Sources */, D0E9BA211F05577700F079A4 /* STPCardParams.m in Sources */, D0EC6E111EB9F58900EBF1C3 /* InstantPageNode.swift in Sources */, @@ -4641,6 +4687,7 @@ D0EC6E1E1EB9F58900EBF1C3 /* InstantPageShapeItem.swift in Sources */, D0EC6E1F1EB9F58900EBF1C3 /* InstantPageTile.swift in Sources */, D0EC6E201EB9F58900EBF1C3 /* InstantPageTileNode.swift in Sources */, + D0B2F7702052B5A800D3BFB9 /* InviteContactsControllerNode.swift in Sources */, D0EC6E211EB9F58900EBF1C3 /* InstantPageController.swift in Sources */, D0EC6E221EB9F58900EBF1C3 /* InstantPageControllerNode.swift in Sources */, D0EC6E231EB9F58900EBF1C3 /* StickerPackPreviewController.swift in Sources */, @@ -4703,6 +4750,7 @@ D0EC6E4B1EB9F58900EBF1C3 /* ItemListControllerSegmentedTitleView.swift in Sources */, D0EC6E4D1EB9F58900EBF1C3 /* PeerInfoController.swift in Sources */, D0EC6E4E1EB9F58900EBF1C3 /* GroupInfoController.swift in Sources */, + D0380DAD204ED434000414AB /* LegacyLiveUploadInterface.swift in Sources */, D0E9BA331F05583A00F079A4 /* STPPostalCodeValidator.m in Sources */, D0EC6E4F1EB9F58900EBF1C3 /* ChannelVisibilityController.swift in Sources */, D09250061FE5371D003F693F /* GlobalExperimentalSettings.swift in Sources */, diff --git a/TelegramUI/AvatarNode.swift b/TelegramUI/AvatarNode.swift index 07980684f2..d3641e70f8 100644 --- a/TelegramUI/AvatarNode.swift +++ b/TelegramUI/AvatarNode.swift @@ -211,7 +211,11 @@ public final class AvatarNode: ASDisplayNode { let colorIndex: Int if let parameters = parameters as? AvatarNodeParameters { if let accountPeerId = parameters.accountPeerId, let peerId = parameters.peerId { - colorIndex = abs(Int(accountPeerId.id + peerId.id)) + if peerId.namespace == -1 { + colorIndex = -1 + } else { + colorIndex = abs(Int(accountPeerId.id + peerId.id)) + } } else { colorIndex = -1 } diff --git a/TelegramUI/ChatBotStartInputPanelNode.swift b/TelegramUI/ChatBotStartInputPanelNode.swift index 53fceb98da..375b886efc 100644 --- a/TelegramUI/ChatBotStartInputPanelNode.swift +++ b/TelegramUI/ChatBotStartInputPanelNode.swift @@ -79,7 +79,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { } @objc func buttonPressed() { - guard let account = self.account, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.peer else { + guard let account = self.account, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else { return } diff --git a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift index af393280a4..4bab37cc9a 100644 --- a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift +++ b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift @@ -82,7 +82,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } @objc func buttonPressed() { - guard let account = self.account, let action = self.action, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.peer?.peer else { + guard let account = self.account, let action = self.action, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else { return } @@ -102,7 +102,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { case .kicked: break case .muteNotifications, .unmuteNotifications: - if let account = self.account, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.peer?.peer { + if let account = self.account, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer { self.actionDisposable.set(togglePeerMuted(account: account, peerId: peer.id).start()) } } @@ -115,7 +115,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState - if let peer = interfaceState.peer?.peer, previousState?.peer?.peer == nil || !peer.isEqual(previousState!.peer!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted { + if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted { if let action = actionForPeer(peer: peer, isMuted: interfaceState.peerIsMuted) { self.action = action let (title, color) = titleAndColorForAction(action, theme: interfaceState.theme, strings: interfaceState.strings) diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index fd3072cb71..4ccc0da920 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -100,7 +100,6 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private let temporaryHiddenGalleryMediaDisposable = MetaDisposable() - private weak var secretMediaPreviewController: SecretMediaPreviewController? private var controllerInteraction: ChatControllerInteraction? private var interfaceInteraction: ChatPanelInterfaceInteraction? @@ -217,7 +216,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.scrollToTop = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.historyNode.scrollScreenToTop() + strongSelf.chatDisplayNode.scrollToTop() } } @@ -276,30 +275,6 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } return false - }, openSecretMessagePreview: { [weak self] messageId in - if let strongSelf = self { - var galleryMedia: Media? - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { - for media in message.media { - if let file = media as? TelegramMediaFile, file.isVideo { - galleryMedia = file - } else if let image = media as? TelegramMediaImage { - galleryMedia = image - } - } - } - if let _ = galleryMedia { - let gallery = SecretMediaPreviewController(account: strongSelf.account, messageId: messageId) - strongSelf.secretMediaPreviewController = gallery - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(gallery, in: .window(.root)) - } - } - }, closeSecretMessagePreview: { [weak self] in - if let strongSelf = self { - strongSelf.secretMediaPreviewController?.dismiss() - strongSelf.secretMediaPreviewController = nil - } }, openPeer: { [weak self] id, navigation, fromMessage in if let strongSelf = self { strongSelf.openPeer(peerId: id, navigation: navigation, fromMessage: fromMessage) @@ -1436,7 +1411,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin return entry ?? GeneratedMediaStoreSettings.defaultSettings } |> deliverOnMainQueue).start(next: { [weak self] settings in - if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.peer?.peer { + if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { strongSelf.chatDisplayNode.dismissInput() let legacyController = LegacyController(presentation: .custom, theme: strongSelf.presentationData.theme) @@ -1450,7 +1425,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin let controller = legacyAttachmentMenu(account: strongSelf.account, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, openGallery: { self?.presentMediaPicker(fileMode: false) }, openCamera: { cameraView, menuController in - if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.peer?.peer { + if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { presentedLegacyCamera(account: strongSelf.account, peer: peer, cameraView: cameraView, menuController: menuController, parentController: strongSelf, sendMessagesWithSignals: { signals in self?.enqueueMediaMessages(signals: signals) }) @@ -1517,7 +1492,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin return entry ?? GeneratedMediaStoreSettings.defaultSettings } |> deliverOnMainQueue).start(next: { [weak self] settings in - if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.peer?.peer { + if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { let controller = legacyPasteMenu(account: strongSelf.account, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, images: images, sendMessagesWithSignals: { signals in self?.enqueueMediaMessages(signals: signals) }) @@ -1923,7 +1898,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self?.enqueueChatContextResult(results, result) }, sendBotCommand: { [weak self] botPeer, command in if let strongSelf = self { - if let peer = strongSelf.presentationInterfaceState.peer, let addressName = botPeer.addressName { + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let addressName = botPeer.addressName { let messageText: String if peer is TelegramUser { messageText = command @@ -2042,7 +2017,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { strongSelf.chatDisplayNode.dismissInput() - if let peer = strongSelf.presentationInterfaceState.peer as? TelegramSecretChat { + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat { let controller = ChatSecretAutoremoveTimerActionSheetController(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, currentValue: peer.messageAutoremoveTimeout == nil ? 0 : peer.messageAutoremoveTimeout!, applyValue: { value in if let strongSelf = self { let _ = setSecretChatMessageAutoremoveTimeoutInteractively(account: strongSelf.account, peerId: peer.id, timeout: value == 0 ? nil : value).start() @@ -2074,7 +2049,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self?.unblockPeer() }, pinMessage: { [weak self] messageId in if let strongSelf = self, case let .peer(currentPeerId) = strongSelf.chatLocation { - if let peer = strongSelf.presentationInterfaceState.peer { + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { if let channel = peer as? TelegramChannel { var canManagePin = false if case .broadcast = channel.info { @@ -2117,7 +2092,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } }, unpinMessage: { [weak self] in if let strongSelf = self { - if let peer = strongSelf.presentationInterfaceState.peer?.peer { + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { if let channel = peer as? TelegramChannel { var canManagePin = false if case .broadcast = channel.info { @@ -2410,6 +2385,10 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin return false } + if !strongSelf.account.telegramApplicationContext.currentMediaInputSettings.with { $0.enableRaiseToSpeak } { + return false + } + return true } } @@ -2871,7 +2850,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } |> deliverOnMainQueue).start(next: { [weak self] settings in if let strongSelf = self { - if let peer = strongSelf.presentationInterfaceState.peer?.peer { + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { let _ = legacyAssetPicker(theme: strongSelf.presentationData.theme, fileMode: fileMode, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true).start(next: { generator in if let strongSelf = self { let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) @@ -2903,7 +2882,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } private func presentMapPicker() { - guard let peer = self.presentationInterfaceState.peer?.peer else { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } let selfPeerId: PeerId @@ -3583,7 +3562,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } private func reportPeer() { - if let peer = self.presentationInterfaceState.peer { + if let peer = self.presentationInterfaceState.renderedPeer?.peer { let title: String if let _ = peer as? TelegramGroup { title = self.presentationData.strings.Conversation_ReportSpam @@ -3684,7 +3663,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin @available(iOSApplicationExtension 9.0, *) public func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { if previewingContext.sourceView === (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.view { - if let peer = self.presentationInterfaceState.peer?.peer { + if let peer = self.presentationInterfaceState.renderedPeer?.peer { let galleryController = AvatarGalleryController(account: self.account, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in }, synchronousLoad: true) galleryController.setHintWillBePresentedInPreviewingContext(true) @@ -3989,9 +3968,9 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin var items: [ActionSheetItem] = [] var personalPeerName: String? var isChannel = false - if let user = self.presentationInterfaceState.peer as? TelegramUser { + if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser { personalPeerName = user.compactDisplayTitle - } else if let channel = self.presentationInterfaceState.peer as? TelegramChannel, case .broadcast = channel.info { + } else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info { isChannel = true } @@ -4034,6 +4013,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin actionSheet?.dismissAnimated() }) ])]) + self.chatDisplayNode.dismissInput() self.present(actionSheet, in: .window(.root)) } } diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 4eeb56ddbb..c35017e2e7 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -38,8 +38,6 @@ public enum ChatControllerInteractionLongTapAction { public final class ChatControllerInteraction { let openMessage: (Message) -> Bool - let openSecretMessagePreview: (MessageId) -> Void - let closeSecretMessagePreview: () -> Void let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void let openPeerMention: (String) -> Void let openMessageContextMenu: (Message, ASDisplayNode, CGRect) -> Void @@ -75,10 +73,8 @@ public final class ChatControllerInteraction { var contextHighlightedState: ChatInterfaceHighlightedState? var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings - public init(openMessage: @escaping (Message) -> Bool, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { + public init(openMessage: @escaping (Message) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { self.openMessage = openMessage - self.openSecretMessagePreview = openSecretMessagePreview - self.closeSecretMessagePreview = closeSecretMessagePreview self.openPeer = openPeer self.openPeerMention = openPeerMention self.openMessageContextMenu = openMessageContextMenu diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 82e37c2da1..79942bf6b1 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -886,9 +886,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { listViewTransaction(ListViewUpdateSizeAndInsets(size: contentBounds.size, insets: listInsets, duration: duration, curve: listViewCurve, ensureTopInsetForOverlayHighlightedItems: ensureTopInsetForOverlayHighlightedItems), additionalScrollDistance, scrollToTop) let navigateButtonsSize = self.navigateButtons.updateLayout(transition: transition) - var navigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - navigateButtonsSize.width - 6.0, y: layout.size.height - containerInsets.bottom - inputPanelsHeight - navigateButtonsSize.height - 6.0), size: navigateButtonsSize) + var navigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - navigateButtonsSize.width - 6.0, y: layout.size.height - containerInsets.bottom - inputPanelsHeight - navigateButtonsSize.height - 6.0 - bottomOverflowOffset), size: navigateButtonsSize) if case .overlay = self.chatPresentationInterfaceState.mode { - navigateButtonsFrame = navigateButtonsFrame.offsetBy(dx: -8.0, dy: -8.0 - bottomOverflowOffset) + navigateButtonsFrame = navigateButtonsFrame.offsetBy(dx: -8.0, dy: -8.0) } transition.updateFrame(node: self.inputPanelBackgroundNode, frame: inputBackgroundFrame) @@ -1157,7 +1157,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { textInputPanelNode?.updateKeepSendButtonEnabled(keepSendButtonEnabled: keepSendButtonEnabled, extendedSearchLayout: extendedSearchLayout, animated: animated) } - if let peer = chatPresentationInterfaceState.peer?.peer, let restrictionText = peer.restrictionText { + if let peer = chatPresentationInterfaceState.renderedPeer?.peer, let restrictionText = peer.restrictionText { if self.restrictedNode == nil { let restrictedNode = ChatRecentActionsEmptyNode(theme: chatPresentationInterfaceState.theme) self.historyNodeContainer.supernode?.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer) @@ -1547,4 +1547,18 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } } + + func scrollToTop() { + if case .media(_, true) = self.chatPresentationInterfaceState.inputMode { + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { state in + if case let .media(mode, true) = state.inputMode { + return (.media(mode: mode, expanded: false), nil) + } else { + return (state.inputMode, nil) + } + } + } else { + self.historyNode.scrollScreenToTop() + } + } } diff --git a/TelegramUI/ChatEmptyNode.swift b/TelegramUI/ChatEmptyNode.swift index b3de619dfd..dd6d9debea 100644 --- a/TelegramUI/ChatEmptyNode.swift +++ b/TelegramUI/ChatEmptyNode.swift @@ -89,12 +89,12 @@ private final class ChatEmptyNodeSecretChatContent: ASDisplayNode, ChatEmptyNode var title = " " var incoming = false - if let peer = interfaceState.peer { - if let chatPeer = peer.peers[peer.peerId] as? TelegramSecretChat { + if let renderedPeer = interfaceState.renderedPeer { + if let chatPeer = renderedPeer.peers[renderedPeer.peerId] as? TelegramSecretChat { if case .participant = chatPeer.role { incoming = true } - if let user = peer.peers[chatPeer.regularPeerId] { + if let user = renderedPeer.peers[chatPeer.regularPeerId] { title = user.compactDisplayTitle } } @@ -220,7 +220,7 @@ final class ChatEmptyNode: ASDisplayNode { } let contentType: ChatEmptyNodeContentType - if let peer = interfaceState.peer?.peer { + if let peer = interfaceState.renderedPeer?.peer { if let _ = peer as? TelegramSecretChat { contentType = .secret } else { diff --git a/TelegramUI/ChatInfoTitlePanelNode.swift b/TelegramUI/ChatInfoTitlePanelNode.swift index 50451bb3c8..a055692c7e 100644 --- a/TelegramUI/ChatInfoTitlePanelNode.swift +++ b/TelegramUI/ChatInfoTitlePanelNode.swift @@ -144,7 +144,7 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { let updatedButtons: [ChatInfoTitleButton] switch interfaceState.chatLocation { case .peer: - if let peer = interfaceState.peer?.peer { + if let peer = interfaceState.renderedPeer?.peer { updatedButtons = peerButtons(peer, isMuted: interfaceState.peerIsMuted) } else { updatedButtons = [] diff --git a/TelegramUI/ChatInstantVideoMessageDurationNode.swift b/TelegramUI/ChatInstantVideoMessageDurationNode.swift new file mode 100644 index 0000000000..baf7a10c44 --- /dev/null +++ b/TelegramUI/ChatInstantVideoMessageDurationNode.swift @@ -0,0 +1,208 @@ +import Foundation +import AsyncDisplayKit +import SwiftSignalKit +import Display + +private let textFont = Font.regular(11.0) + +private struct ChatInstantVideoMessageDurationNodeState: Equatable { + let hours: Int32? + let minutes: Int32? + let seconds: Int32? + + init() { + self.hours = nil + self.minutes = nil + self.seconds = nil + } + + init(hours: Int32, minutes: Int32, seconds: Int32) { + self.hours = hours + self.minutes = minutes + self.seconds = seconds + } + + static func ==(lhs: ChatInstantVideoMessageDurationNodeState, rhs: ChatInstantVideoMessageDurationNodeState) -> Bool { + if lhs.hours != rhs.hours || lhs.minutes != rhs.minutes || lhs.seconds != rhs.seconds { + return false + } + return true + } +} + +private final class ChatInstantVideoMessageDurationNodeParameters: NSObject { + let state: ChatInstantVideoMessageDurationNodeState + let isSeen: Bool + let backgroundColor: UIColor + let textColor: UIColor + + init(state: ChatInstantVideoMessageDurationNodeState, isSeen: Bool, backgroundColor: UIColor, textColor: UIColor) { + self.state = state + self.isSeen = isSeen + self.backgroundColor = backgroundColor + self.textColor = textColor + + super.init() + } +} + +final class ChatInstantVideoMessageDurationNode: ASDisplayNode { + private var textColor: UIColor + private var fillColor: UIColor + + var defaultDuration: Double? { + didSet { + if self.defaultDuration != oldValue { + self.updateTimestamp() + self.setNeedsDisplay() + } + } + } + + var isSeen: Bool = false { + didSet { + if self.isSeen != oldValue { + self.setNeedsDisplay() + } + } + } + + private var updateTimer: SwiftSignalKit.Timer? + + private var statusValue: MediaPlayerStatus? { + didSet { + if self.statusValue != oldValue { + if let statusValue = statusValue, case .playing = statusValue.status { + self.ensureHasTimer() + } else { + self.stopTimer() + } + self.updateTimestamp() + } + } + } + + private var state = ChatInstantVideoMessageDurationNodeState() { + didSet { + if self.state != oldValue { + self.setNeedsDisplay() + } + } + } + + private var statusDisposable: Disposable? + private var statusValuePromise = Promise() + + var status: Signal? { + didSet { + if let status = self.status { + self.statusValuePromise.set(status) + } else { + self.statusValuePromise.set(.never()) + } + } + } + + init(textColor: UIColor, fillColor: UIColor) { + self.textColor = textColor + self.fillColor = fillColor + + super.init() + + self.isOpaque = false + self.contentsScale = UIScreenScale + self.contentMode = .topRight + + self.statusDisposable = (self.statusValuePromise.get() + |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + strongSelf.statusValue = status + } + }) + } + + deinit { + self.statusDisposable?.dispose() + self.updateTimer?.invalidate() + } + + private func ensureHasTimer() { + if self.updateTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.updateTimestamp() + }, queue: Queue.mainQueue()) + self.updateTimer = timer + timer.start() + } + } + + private func stopTimer() { + self.updateTimer?.invalidate() + self.updateTimer = nil + } + + func updateTimestamp() { + if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) { + let timestampSeconds: Double + if !statusValue.generationTimestamp.isZero { + timestampSeconds = statusValue.timestamp + (CACurrentMediaTime() - statusValue.generationTimestamp) + } else { + timestampSeconds = statusValue.timestamp + } + let timestamp = Int32(timestampSeconds) + self.state = ChatInstantVideoMessageDurationNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60) + } else if let defaultDuration = self.defaultDuration { + let timestamp = Int32(defaultDuration) + self.state = ChatInstantVideoMessageDurationNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60) + } else { + self.state = ChatInstantVideoMessageDurationNodeState() + } + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return ChatInstantVideoMessageDurationNodeParameters(state: self.state, isSeen: self.isSeen, backgroundColor: self.fillColor, textColor: self.textColor) + } + + @objc override public class func display(withParameters: Any?, isCancelled: () -> Bool) -> UIImage? { + guard let parameters = withParameters as? ChatInstantVideoMessageDurationNodeParameters else { + return nil + } + + let text: String + if let hours = parameters.state.hours, let minutes = parameters.state.minutes, let seconds = parameters.state.seconds { + if hours != 0 { + text = String(format: "%d:%02d:%02d", hours, minutes, seconds) + } else { + text = String(format: "%d:%02d", minutes, seconds) + } + } else { + text = "-:--" + } + let string = NSAttributedString(string: text, font: textFont, textColor: parameters.textColor) + let textRect = string.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil) + + let unseenInset: CGFloat = (parameters.isSeen ? 0.0 : 10.0) + let imageSize = CGSize(width: ceil(textRect.width) + 10.0 + unseenInset, height: 18.0) + + return generateImage(imageSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + context.setBlendMode(.copy) + context.setFillColor(parameters.backgroundColor.cgColor) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.height, height: size.height))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height))) + context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height))) + + if !parameters.isSeen { + context.setFillColor(parameters.textColor.cgColor) + let diameter: CGFloat = 4.0 + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height + floor((size.height - diameter) / 2.0), y: floor((size.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))) + } + + context.setBlendMode(.normal) + UIGraphicsPushContext(context) + string.draw(at: CGPoint(x: floor((size.width - unseenInset - textRect.size.width) / 2.0) + textRect.origin.x, y: 2.0 + textRect.origin.y + UIScreenPixel)) + UIGraphicsPopContext() + }) + } +} diff --git a/TelegramUI/ChatInterfaceInputContextPanels.swift b/TelegramUI/ChatInterfaceInputContextPanels.swift index b528d2938b..d9ace85294 100644 --- a/TelegramUI/ChatInterfaceInputContextPanels.swift +++ b/TelegramUI/ChatInterfaceInputContextPanels.swift @@ -17,7 +17,7 @@ private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult } func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputContextPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputContextPanelNode? { - guard let _ = chatPresentationInterfaceState.peer else { + guard let _ = chatPresentationInterfaceState.renderedPeer?.peer else { return nil } @@ -111,7 +111,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa } func chatOverlayContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputContextPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputContextPanelNode? { - guard let searchQuerySuggestionResult = chatPresentationInterfaceState.searchQuerySuggestionResult, let _ = chatPresentationInterfaceState.peer else { + guard let searchQuerySuggestionResult = chatPresentationInterfaceState.searchQuerySuggestionResult, let _ = chatPresentationInterfaceState.renderedPeer?.peer else { return nil } diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index 9c5f951733..d53dca077c 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -196,13 +196,13 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte } else { if chatPresentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 { var accessoryItems: [ChatTextInputAccessoryItem] = [] - if let peer = chatPresentationInterfaceState.peer as? TelegramSecretChat { + if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat { accessoryItems.append(.messageAutoremoveTimeout(peer.messageAutoremoveTimeout)) } - if let peer = chatPresentationInterfaceState.peer as? TelegramChannel, case .broadcast = peer.info, canSendMessagesToPeer(peer) { + if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = peer.info, canSendMessagesToPeer(peer) { accessoryItems.append(.silentPost(chatPresentationInterfaceState.interfaceState.silentPosting)) } - if let peer = chatPresentationInterfaceState.peer as? TelegramUser { + if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser { if let _ = peer.botInfo { accessoryItems.append(.commands) } diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index 97066f0320..1edf289a35 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -19,7 +19,7 @@ private let starIconEmpty = UIImage(bundleImageName: "Chat/Context Menu/StarIcon private let starIconFilled = UIImage(bundleImageName: "Chat/Context Menu/StarIconFilled")?.precomposed() func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool { - guard let peer = chatPresentationInterfaceState.peer else { + guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else { return false } @@ -85,7 +85,9 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } else if let _ = media as? TelegramMediaAction { isAction = true } else if let image = media as? TelegramMediaImage { - loadCopyMediaResource = largestImageRepresentation(image.representations)?.resource + if !messages[0].containsSecretMedia { + loadCopyMediaResource = largestImageRepresentation(image.representations)?.resource + } } } } diff --git a/TelegramUI/ChatInterfaceStateContextQueries.swift b/TelegramUI/ChatInterfaceStateContextQueries.swift index 6de46b10d1..cd0db0f686 100644 --- a/TelegramUI/ChatInterfaceStateContextQueries.swift +++ b/TelegramUI/ChatInterfaceStateContextQueries.swift @@ -9,7 +9,7 @@ enum ChatContextQueryUpdate { } func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] { - guard let peer = chatPresentationInterfaceState.peer?.peer else { + guard let peer = chatPresentationInterfaceState.renderedPeer?.peer else { return [:] } let inputQueries = inputContextQueriesForChatPresentationIntefaceState(chatPresentationInterfaceState) @@ -250,7 +250,7 @@ func searchQuerySuggestionResultStateForChatInterfacePresentationState(_ chatPre } else { switch inputQuery { case let .mention(query, _): - if let peer = chatPresentationInterfaceState.peer?.peer { + if let peer = chatPresentationInterfaceState.renderedPeer?.peer { var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() if let currentQuery = currentQuery { switch currentQuery { diff --git a/TelegramUI/ChatInterfaceStateInputPanels.swift b/TelegramUI/ChatInterfaceStateInputPanels.swift index a6db9299c8..34e5e901e8 100644 --- a/TelegramUI/ChatInterfaceStateInputPanels.swift +++ b/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -3,7 +3,7 @@ import AsyncDisplayKit import TelegramCore func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputPanelNode? { - if let peer = chatPresentationInterfaceState.peer, peer.peer?.restrictionText != nil { + if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText != nil { return nil } @@ -71,7 +71,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } - if let peer = chatPresentationInterfaceState.peer { + if let peer = chatPresentationInterfaceState.renderedPeer?.peer { if let secretChat = peer as? TelegramSecretChat { switch secretChat.embeddedState { case .handshake: @@ -154,7 +154,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if let _ = chatPresentationInterfaceState.botStartPayload { displayBotStartPanel = true } else if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(true) = chatHistoryState { - if let user = chatPresentationInterfaceState.peer as? TelegramUser, user.botInfo != nil { + if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { displayBotStartPanel = true } } diff --git a/TelegramUI/ChatInterfaceStateNavigationButtons.swift b/TelegramUI/ChatInterfaceStateNavigationButtons.swift index e7b5a6f9b4..5bf201c229 100644 --- a/TelegramUI/ChatInterfaceStateNavigationButtons.swift +++ b/TelegramUI/ChatInterfaceStateNavigationButtons.swift @@ -23,7 +23,7 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha if let _ = presentationInterfaceState.interfaceState.selectionState { if let currentButton = currentButton, currentButton.action == .clearHistory { return currentButton - } else if let peer = presentationInterfaceState.peer { + } else if let peer = presentationInterfaceState.renderedPeer?.peer { let canClear: Bool if peer is TelegramUser || peer is TelegramGroup || peer is TelegramSecretChat { canClear = true @@ -49,7 +49,7 @@ func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Ch } } - if let peer = presentationInterfaceState.peer?.peer { + if let peer = presentationInterfaceState.renderedPeer?.peer { if presentationInterfaceState.accountPeerId == peer.id { return ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)) } diff --git a/TelegramUI/ChatInterfaceTitlePanelNodes.swift b/TelegramUI/ChatInterfaceTitlePanelNodes.swift index 26ff84b7bc..c940af820e 100644 --- a/TelegramUI/ChatInterfaceTitlePanelNodes.swift +++ b/TelegramUI/ChatInterfaceTitlePanelNodes.swift @@ -5,7 +5,7 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat if case .overlay = chatPresentationInterfaceState.mode { return nil } - if chatPresentationInterfaceState.peer?.peer?.restrictionText != nil { + if chatPresentationInterfaceState.renderedPeer?.peer?.restrictionText != nil { return nil } if chatPresentationInterfaceState.search != nil { diff --git a/TelegramUI/ChatMediaInputGifPane.swift b/TelegramUI/ChatMediaInputGifPane.swift index 840a9be03e..791be445e7 100644 --- a/TelegramUI/ChatMediaInputGifPane.swift +++ b/TelegramUI/ChatMediaInputGifPane.swift @@ -37,7 +37,11 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { } func fileAt(point: CGPoint) -> TelegramMediaFile? { - return self.multiplexedNode?.fileAt(point: point) + if let multiplexedNode = self.multiplexedNode { + return multiplexedNode.fileAt(point: point.offsetBy(dx: -multiplexedNode.frame.minX, dy: -multiplexedNode.frame.minY)) + } else { + return nil + } } override func willEnterHierarchy() { diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index 04dd2efa94..2f7f450279 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -377,15 +377,13 @@ private func universalServiceMessageString(theme: PresentationTheme?, strings: P return attributedString } -class ChatMessageActionItemNode: ChatMessageItemView { +class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { let labelNode: TextNode let filledBackgroundNode: LinkHighlightingNode var linkHighlightingNode: LinkHighlightingNode? private let fetchDisposable = MetaDisposable() - private var appliedItem: ChatMessageItem? - required init() { self.labelNode = TextNode() self.labelNode.isLayerBacked = true @@ -393,7 +391,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { self.filledBackgroundNode = LinkHighlightingNode(color: .clear) - super.init(layerBacked: false) + super.init() self.addSubnode(self.filledBackgroundNode) self.addSubnode(self.labelNode) @@ -410,7 +408,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { override func didLoad() { super.didLoad() - let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + /*let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) recognizer.tapActionAtPoint = { _ in return .waitForSingleTap } @@ -419,91 +417,65 @@ class ChatMessageActionItemNode: ChatMessageItemView { strongSelf.updateTouchesAtPoint(point) } } - self.view.addGestureRecognizer(recognizer) + self.view.addGestureRecognizer(recognizer)*/ } - override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) - let layoutConstants = self.layoutConstants let backgroundLayout = self.filledBackgroundNode.asyncLayout() - return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in - let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, message: item.message, accountPeerId: item.account.peerId) + return { item, layoutConstants, _, _, _ in + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) - let (labelLayout, apply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in + let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, message: item.message, accountPeerId: item.account.peerId) - var labelRects = labelLayout.linesRects() - if labelRects.count > 1 { - let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) - for i in 0 ..< sortedIndices.count { - let index = sortedIndices[i] - for j in -1 ... 1 { - if j != 0 && index + j >= 0 && index + j < sortedIndices.count { - if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 { - labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width) + let (labelLayout, apply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + var labelRects = labelLayout.linesRects() + if labelRects.count > 1 { + let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) + for i in 0 ..< sortedIndices.count { + let index = sortedIndices[i] + for j in -1 ... 1 { + if j != 0 && index + j >= 0 && index + j < sortedIndices.count { + if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 { + labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width) + } } } } } - } - for i in 0 ..< labelRects.count { - /*if i != 0 && i != labelRects.count - 1 { - if labelRects[i - 1].width > labelRects[i].width && labelRects[i + 1].width > labelRects[i].width { - if abs(labelRects[i - 1].width - labelRects[i].width) < abs(labelRects[i + 1].width - labelRects[i].width) { - labelRects[i].size.width = labelRects[i - 1].width - } else { - labelRects[i].size.width = labelRects[i + 1].width - } - } - }*/ - - labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0)) - labelRects[i].size.height = 20.0 - labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) - } - - let backgroundApply = backgroundLayout(item.presentationData.theme.chat.serviceMessage.serviceMessageFillColor, labelRects, 10.0, 10.0, 0.0) - - let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) - var layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) - if dateHeaderAtBottom { - layoutInsets.top += layoutConstants.timestampHeaderHeight - } - - return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: labelLayout.size.height + 4.0), insets: layoutInsets), { [weak self] animation in - if let strongSelf = self { - strongSelf.appliedItem = item - - let _ = apply() - let _ = backgroundApply() - - let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - labelLayout.size.width) / 2.0), y: floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) - strongSelf.labelNode.frame = labelFrame - strongSelf.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) + for i in 0 ..< labelRects.count { + labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0)) + labelRects[i].size.height = 20.0 + labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) } + + let backgroundApply = backgroundLayout(item.presentationData.theme.chat.serviceMessage.serviceMessageFillColor, labelRects, 10.0, 10.0, 0.0) + + let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) + var layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) + + return (backgroundSize.width, { boundingWidth in + return (backgroundSize, { [weak self] animation in + if let strongSelf = self { + strongSelf.item = item + + let _ = apply() + let _ = backgroundApply() + + let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) + strongSelf.labelNode.frame = labelFrame + strongSelf.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) + } + }) + }) }) } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) - - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - override func animateAdded(_ currentTimestamp: Double, duration: Double) { - super.animateAdded(currentTimestamp, duration: duration) - - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - super.animateRemoved(currentTimestamp, duration: duration) - - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - } - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .began: @@ -539,8 +511,6 @@ class ChatMessageActionItemNode: ChatMessageItemView { if let item = self.item { item.controllerInteraction.openInstantPage(item.message) } - case .holdToPreviewSecretMedia: - foundTapAction = true case let .call(peerId): foundTapAction = true self.item?.controllerInteraction.callPeer(peerId) @@ -583,8 +553,6 @@ class ChatMessageActionItemNode: ChatMessageItemView { item.controllerInteraction.longTap(.hashtag(hashtag)) case .instantPage: break - case .holdToPreviewSecretMedia: - break case .call: break } @@ -604,7 +572,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { } } - private func updateTouchesAtPoint(_ point: CGPoint?) { + override func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { var rects: [(CGRect, CGRect)]? let textNodeFrame = self.labelNode.frame @@ -655,7 +623,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { } } - private func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { + override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.labelNode.frame if let (_, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)) { if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { diff --git a/TelegramUI/ChatMessageBubbleContentNode.swift b/TelegramUI/ChatMessageBubbleContentNode.swift index 35cd0d219a..8b7268c18d 100644 --- a/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageBubbleContentNode.swift @@ -4,11 +4,23 @@ import Display import Postbox import TelegramCore +enum ChatMessageBubbleContentBackgroundHiding { + case none + case emptyWallpaper + case always +} + +enum ChatMessageBubbleContentAlignment { + case none + case center +} + struct ChatMessageBubbleContentProperties { let hidesSimpleAuthorHeader: Bool let headerSpacing: CGFloat - let hidesBackgroundForEmptyWallpapers: Bool + let hidesBackground: ChatMessageBubbleContentBackgroundHiding let forceFullCorners: Bool + let forceAlignment: ChatMessageBubbleContentAlignment } enum ChatMessageBubbleNoneMergeStatus { @@ -58,7 +70,6 @@ enum ChatMessageBubbleContentTapAction { case botCommand(String) case hashtag(String?, String) case instantPage - case holdToPreviewSecretMedia case call(PeerId) case ignore } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 1348560bb1..c3068942c3 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -19,8 +19,12 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [( } else { result.append((message, ChatMessageFileBubbleContentNode.self)) } - } else if let action = media as? TelegramMediaAction, case .phoneCall = action.action { - result.append((message, ChatMessageCallBubbleContentNode.self)) + } else if let action = media as? TelegramMediaAction { + if case .phoneCall = action.action { + result.append((message, ChatMessageCallBubbleContentNode.self)) + } else { + result.append((message, ChatMessageActionBubbleContentNode.self)) + } } else if let _ = media as? TelegramMediaMap { result.append((message, ChatMessageMapBubbleContentNode.self)) } else if let _ = media as? TelegramMediaGame { @@ -33,6 +37,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [( break inner } else if let _ = media as? TelegramMediaContact { result.append((message, ChatMessageContactBubbleContentNode.self)) + } else if let _ = media as? TelegramMediaExpiredContent { + result.append((message, ChatMessageActionBubbleContentNode.self)) } } @@ -217,8 +223,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { return .fail case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .call: return .waitForSingleTap - case .holdToPreviewSecretMedia: - return .waitForHold(timeout: 0.12, acceptTap: false) } } if !strongSelf.backgroundNode.frame.contains(point) { @@ -434,10 +438,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let topNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedTop.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) let bottomNodeMergeStatus: ChatMessageBubbleMergeStatus = mergedBottom.merged ? (incoming ? .Left : .Right) : .None(incoming ? .Incoming : .Outgoing) - var canPossiblyHideBackground = false + var backgroundHiding: ChatMessageBubbleContentBackgroundHiding = .none + var hasSolidWallpaper = false if case .color = item.presentationData.wallpaper { - canPossiblyHideBackground = true + hasSolidWallpaper = true } + var alignment: ChatMessageBubbleContentAlignment = .none var maximumNodeWidth = maximumContentWidth @@ -519,17 +525,38 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { contentPropertiesAndLayouts.append((unboundSize, properties, prepareContentPosition, nodeLayout)) - if !properties.hidesBackgroundForEmptyWallpapers { - canPossiblyHideBackground = false + switch properties.hidesBackground { + case .none: + break + case .emptyWallpaper: + switch backgroundHiding { + case .none: + backgroundHiding = properties.hidesBackground + default: + break + } + case .always: + backgroundHiding = .always + } + + switch properties.forceAlignment { + case .none: + break + case .center: + alignment = .center } index += 1 } var initialDisplayHeader = true - if inlineBotNameString == nil && (ignoreForward || firstMessage.forwardInfo == nil) && replyMessage == nil { - if let first = contentPropertiesAndLayouts.first, first.1.hidesSimpleAuthorHeader { - initialDisplayHeader = false + if case .always = backgroundHiding { + initialDisplayHeader = false + } else { + if inlineBotNameString == nil && (ignoreForward || firstMessage.forwardInfo == nil) && replyMessage == nil { + if let first = contentPropertiesAndLayouts.first, first.1.hidesSimpleAuthorHeader { + initialDisplayHeader = false + } } } @@ -730,7 +757,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } - let hideBackground = canPossiblyHideBackground && !displayHeader + let hideBackground: Bool + switch backgroundHiding { + case .none: + hideBackground = false + case .emptyWallpaper: + hideBackground = hasSolidWallpaper && !displayHeader + case .always: + hideBackground = true + } var removedContentNodeIndices: [Int]? findRemoved: for i in 0 ..< currentContentClassesPropertiesAndLayouts.count { @@ -940,11 +975,25 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth) } - let layoutBubbleSize = CGSize(width: max(contentSize.width, headerSize.width) + layoutConstants.bubble.contentInsets.left + layoutConstants.bubble.contentInsets.right, height: max(layoutConstants.bubble.minimumSize.height, headerSize.height + contentSize.height + layoutConstants.bubble.contentInsets.top + layoutConstants.bubble.contentInsets.bottom)) + let minimalContentSize: CGSize + if hideBackground { + minimalContentSize = CGSize(width: 1.0, height: 1.0) + } else { + minimalContentSize = layoutConstants.bubble.minimumSize + } + let layoutBubbleSize = CGSize(width: max(contentSize.width, headerSize.width) + layoutConstants.bubble.contentInsets.left + layoutConstants.bubble.contentInsets.right, height: max(minimalContentSize.height, headerSize.height + contentSize.height + layoutConstants.bubble.contentInsets.top + layoutConstants.bubble.contentInsets.bottom)) - let backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset) : (params.width - params.rightInset - layoutBubbleSize.width - layoutConstants.bubble.edgeInset), y: 0.0), size: layoutBubbleSize) - - let contentOrigin = CGPoint(x: backgroundFrame.origin.x + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height) + let backgroundFrame: CGRect + let contentOrigin: CGPoint + switch alignment { + case .none: + backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset) : (params.width - params.rightInset - layoutBubbleSize.width - layoutConstants.bubble.edgeInset), y: 0.0), size: layoutBubbleSize) + contentOrigin = CGPoint(x: backgroundFrame.origin.x + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height) + case .center: + let availableWidth = params.width - params.leftInset - params.rightInset + backgroundFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((availableWidth - layoutBubbleSize.width) / 2.0), y: 0.0), size: layoutBubbleSize) + contentOrigin = CGPoint(x: backgroundFrame.minX + floor(layoutConstants.bubble.contentInsets.right + layoutConstants.bubble.contentInsets.left) / 2.0, y: backgroundFrame.minY + layoutConstants.bubble.contentInsets.top + headerSize.height) + } var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height) if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { @@ -1312,12 +1361,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { - case .began: - if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture { - if let item = self.item, item.message.containsSecretMedia { - item.controllerInteraction.openSecretMessagePreview(item.message.id) - } - } case .ended: if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { switch gesture { @@ -1404,8 +1447,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { item.controllerInteraction.openInstantPage(item.message) } break loop - case .holdToPreviewSecretMedia: - foundTapAction = true case let .call(peerId): foundTapAction = true self.item?.controllerInteraction.callPeer(peerId) @@ -1450,8 +1491,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { break loop case .instantPage: break - case .holdToPreviewSecretMedia: - break case .call: break } @@ -1460,16 +1499,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { item.controllerInteraction.openMessageContextMenu(tapMessage, self, self.backgroundNode.frame) } } - case .hold: - if let item = self.item, item.message.containsSecretMedia { - item.controllerInteraction.closeSecretMessagePreview() - } + default: + break } } - case .cancelled: - if let item = self.item, item.message.containsSecretMedia { - item.controllerInteraction.closeSecretMessagePreview() - } default: break } diff --git a/TelegramUI/ChatMessageCallBubbleContentNode.swift b/TelegramUI/ChatMessageCallBubbleContentNode.swift index 2a745d43fd..ed7ff494ea 100644 --- a/TelegramUI/ChatMessageCallBubbleContentNode.swift +++ b/TelegramUI/ChatMessageCallBubbleContentNode.swift @@ -59,7 +59,7 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) return { item, layoutConstants, _, _, _ in - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .none, forceFullCorners: false, forceAlignment: .none) return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in let message = item.message diff --git a/TelegramUI/ChatMessageContactBubbleContentNode.swift b/TelegramUI/ChatMessageContactBubbleContentNode.swift index 6471f995fc..d944d77b3b 100644 --- a/TelegramUI/ChatMessageContactBubbleContentNode.swift +++ b/TelegramUI/ChatMessageContactBubbleContentNode.swift @@ -86,7 +86,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { updatedPhone = nil } - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .none, forceFullCorners: false, forceAlignment: .none) return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in let avatarSize = CGSize(width: 40.0, height: 40.0) diff --git a/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift b/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift index e38e5807ec..7f15e074b2 100644 --- a/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift +++ b/TelegramUI/ChatMessageEventLogPreviousDescriptionContentNode.swift @@ -45,7 +45,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, true, title, subtitle, text, messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .none, forceFullCorners: false, forceAlignment: .none) return (contentProperties, nil, initialWidth, { constrainedSize, position in let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position) diff --git a/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift b/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift index 1f3b93b447..5ce3131dfd 100644 --- a/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift +++ b/TelegramUI/ChatMessageEventLogPreviousLinkContentNode.swift @@ -40,7 +40,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, true, title, subtitle, text, messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .none, forceFullCorners: false, forceAlignment: .none) return (contentProperties, nil, initialWidth, { constrainedSize, position in let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position) diff --git a/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift b/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift index 12198652d0..62db4ffe20 100644 --- a/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift +++ b/TelegramUI/ChatMessageEventLogPreviousMessageContentNode.swift @@ -45,7 +45,7 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, true, title, subtitle, text, messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .none, forceFullCorners: false, forceAlignment: .none) return (contentProperties, nil, initialWidth, { constrainedSize, position in let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position) diff --git a/TelegramUI/ChatMessageFileBubbleContentNode.swift b/TelegramUI/ChatMessageFileBubbleContentNode.swift index 0d8933300b..a4aa49f5c4 100644 --- a/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -65,7 +65,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item.presentationData, item.message, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.account.peerId), statusType, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height)) - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .none, forceFullCorners: false, forceAlignment: .none) return (contentProperties, nil, initialWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { constrainedSize, position in let (refinedWidth, finishLayout) = refineLayout(CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height)) diff --git a/TelegramUI/ChatMessageGameBubbleContentNode.swift b/TelegramUI/ChatMessageGameBubbleContentNode.swift index e5e8e89768..124a23f145 100644 --- a/TelegramUI/ChatMessageGameBubbleContentNode.swift +++ b/TelegramUI/ChatMessageGameBubbleContentNode.swift @@ -67,7 +67,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, true, layoutConstants, constrainedSize) - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .none, forceFullCorners: false, forceAlignment: .none) return (contentProperties, nil, initialWidth, { constrainedSize, position in let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position) diff --git a/TelegramUI/ChatMessageInstantVideoItemNode.swift b/TelegramUI/ChatMessageInstantVideoItemNode.swift index 8cd45e838a..32059ead07 100644 --- a/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -28,11 +28,11 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { private var replyInfoNode: ChatMessageReplyInfoNode? private var replyBackgroundNode: ASImageNode? + private var durationNode: ChatInstantVideoMessageDurationNode? private let dateAndStatusNode: ChatMessageDateAndStatusNode private let infoBackgroundNode: ASImageNode private let muteIconNode: ASImageNode - private let consumableContentNode: ASImageNode private var status: FileMediaResourceStatus? private let playbackStatusDisposable = MetaDisposable() @@ -69,18 +69,11 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { self.muteIconNode.displayWithoutProcessing = true self.muteIconNode.displaysAsynchronously = false - self.consumableContentNode = ASImageNode() - self.consumableContentNode.isLayerBacked = true - self.consumableContentNode.displayWithoutProcessing = true - self.consumableContentNode.displaysAsynchronously = false - self.consumableContentNode.alpha = 0.0 - super.init(layerBacked: false) self.addSubnode(self.dateAndStatusNode) self.addSubnode(self.infoBackgroundNode) self.infoBackgroundNode.addSubnode(self.muteIconNode) - self.infoBackgroundNode.addSubnode(self.consumableContentNode) } required init?(coder aDecoder: NSCoder) { @@ -134,12 +127,10 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { var updatedInfoBackgroundImage: UIImage? var updatedMuteIconImage: UIImage? - var updatedConsumableContentIcon: UIImage? if item.presentationData.theme !== currentItem?.presentationData.theme { updatedTheme = item.presentationData.theme updatedInfoBackgroundImage = PresentationResourcesChat.chatInstantMessageInfoBackgroundImage(item.presentationData.theme) updatedMuteIconImage = PresentationResourcesChat.chatInstantMessageMuteIconImage(item.presentationData.theme) - updatedConsumableContentIcon = PresentationResourcesChat.chatMediaConsumableContentIcon(item.presentationData.theme) } let instantVideoBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(item.presentationData.theme) @@ -319,17 +310,10 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { strongSelf.muteIconNode.image = updatedMuteIconImage } - if let updatedConsumableContentIcon = updatedConsumableContentIcon { - strongSelf.consumableContentNode.image = updatedConsumableContentIcon - } - strongSelf.telegramFile = updatedFile - if let infoBackgroundImage = strongSelf.infoBackgroundNode.image, let muteImage = strongSelf.muteIconNode.image, let consumableContentImage = strongSelf.consumableContentNode.image { - var infoWidth = muteImage.size.width - if notConsumed { - infoWidth += infoBackgroundImage.size.height - 6.0 - } + if let infoBackgroundImage = strongSelf.infoBackgroundNode.image, let muteImage = strongSelf.muteIconNode.image { + let infoWidth = muteImage.size.width let transition: ContainedViewLayoutTransition if animation.isAnimated { transition = .animated(duration: 0.2, curve: .spring) @@ -340,9 +324,6 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { transition.updateFrame(node: strongSelf.infoBackgroundNode, frame: infoBackgroundFrame) let muteIconFrame = CGRect(origin: CGPoint(x: infoBackgroundFrame.width - muteImage.size.width, y: 0.0), size: muteImage.size) transition.updateFrame(node: strongSelf.muteIconNode, frame: muteIconFrame) - let consumableContentFrame = CGRect(origin: CGPoint(x: floor((infoBackgroundFrame.height - consumableContentImage.size.width) / 2.0), y: floor((infoBackgroundFrame.height - consumableContentImage.size.width) / 2.0)), size: consumableContentImage.size) - transition.updateFrame(node: strongSelf.consumableContentNode, frame: consumableContentFrame) - transition.updateAlpha(node: strongSelf.consumableContentNode, alpha: notConsumed ? 1.0 : 0.0) } if let updatedPlaybackStatus = updatedPlaybackStatus { @@ -452,24 +433,42 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } playbackStatusNode.frame = videoFrame.insetBy(dx: 1.5, dy: 1.5) if let updatedFile = updatedFile { - playbackStatusNode.status = messageFileMediaPlaybackStatus(account: item.account, file: updatedFile, message: item.message) + let status = messageFileMediaPlaybackStatus(account: item.account, file: updatedFile, message: item.message) + playbackStatusNode.status = status + strongSelf.durationNode?.status = status |> map(Optional.init) } - } else if let playbackStatusNode = strongSelf.playbackStatusNode { - strongSelf.playbackStatusNode = nil - playbackStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackStatusNode] _ in - playbackStatusNode?.removeFromSupernode() - }) + } else { + if let playbackStatusNode = strongSelf.playbackStatusNode { + strongSelf.playbackStatusNode = nil + playbackStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackStatusNode] _ in + playbackStatusNode?.removeFromSupernode() + }) + } + + strongSelf.durationNode?.status = .single(nil) } } })) } dateAndStatusApply(false) - strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floor(videoFrame.midX) + 70.0, params.width - params.rightInset - dateAndStatusSize.width - 4.0), y: videoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floor(videoFrame.midX) + 55.0, params.width - params.rightInset - dateAndStatusSize.width - 4.0), y: videoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) if let telegramFile = updatedFile, updatedMedia { + let durationNode: ChatInstantVideoMessageDurationNode + if let current = strongSelf.durationNode { + durationNode = current + } else { + durationNode = ChatInstantVideoMessageDurationNode(textColor: theme.chat.serviceMessage.serviceMessagePrimaryTextColor, fillColor: theme.chat.serviceMessage.serviceMessageFillColor) + strongSelf.durationNode = durationNode + strongSelf.addSubnode(durationNode) + } + durationNode.defaultDuration = telegramFile.duration.flatMap(Double.init) + if let videoNode = strongSelf.videoNode { - videoNode.removeFromSupernode() + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in + videoNode?.removeFromSupernode() + }) } let videoNode = UniversalVideoNode(postbox: item.account.postbox, audioSession: item.account.telegramApplicationContext.mediaManager.audioSession, manager: item.account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(diameter: 214.0, backgroundImage: instantVideoBackgroundImage, tapped: { if let strongSelf = self { @@ -487,6 +486,11 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { videoNode.canAttachContent = strongSelf.shouldAcquireVideoContext } + if let durationNode = strongSelf.durationNode { + durationNode.frame = CGRect(origin: CGPoint(x: videoFrame.midX - 56.0, y: videoFrame.maxY - 18.0), size: CGSize(width: 1.0, height: 1.0)) + durationNode.isSeen = !notConsumed + } + if let videoNode = strongSelf.videoNode { videoNode.frame = videoFrame videoNode.updateLayout(size: arguments.boundingSize, transition: .immediate) diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 4ec39ecc99..c916453781 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -20,7 +20,6 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { private var videoNode: UniversalVideoNode? private var statusNode: RadialStatusNode? private var badgeNode: ChatMessageInteractiveMediaBadge? - private var timeoutNode: RadialTimeoutNode? private var labelNode: ChatMessageInteractiveMediaLabelNode? private var tapRecognizer: UITapGestureRecognizer? @@ -364,7 +363,6 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { strongSelf.themeAndStrings = (theme, strings) transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) strongSelf.statusNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) - strongSelf.timeoutNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) if let replaceVideoNode = replaceVideoNode { if let videoNode = strongSelf.videoNode { @@ -400,27 +398,13 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { strongSelf.imageNode.setSignal(updateImageSignal) } - if let secretBeginTimeAndTimeout = secretBeginTimeAndTimeout { - if strongSelf.timeoutNode == nil { - let timeoutNode = RadialTimeoutNode(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor) - timeoutNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: radialStatusSize, height: radialStatusSize)) - timeoutNode.position = strongSelf.imageNode.position - strongSelf.timeoutNode = timeoutNode - strongSelf.addSubnode(timeoutNode) - timeoutNode.setTimeout(beginTimestamp: secretBeginTimeAndTimeout.0, timeout: secretBeginTimeAndTimeout.1) - } else if let updatedTheme = updatedTheme { - strongSelf.timeoutNode?.updateTheme(backgroundColor: updatedTheme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: updatedTheme.chat.bubble.mediaOverlayControlForegroundColor) + if let _ = secretBeginTimeAndTimeout { + if updatedStatusSignal == nil, let fetchStatus = strongSelf.fetchStatus, case .Local = fetchStatus { + if let statusNode = strongSelf.statusNode, case .secretTimeout = statusNode.state { + } else { + updatedStatusSignal = .single(fetchStatus) + } } - - if let statusNode = strongSelf.statusNode { - statusNode.transitionToState(.none, completion: { [weak statusNode] in - statusNode?.removeFromSupernode() - }) - strongSelf.statusNode = nil - } - } else if let timeoutNode = strongSelf.timeoutNode { - timeoutNode.removeFromSupernode() - strongSelf.timeoutNode = nil } if let updatedStatusSignal = updatedStatusSignal { @@ -430,7 +414,9 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { strongSelf.fetchStatus = status var progressRequired = false - if secretBeginTimeAndTimeout == nil { + if let _ = secretBeginTimeAndTimeout { + progressRequired = true + } else { if case .Local = status { if let file = media as? TelegramMediaFile, file.isVideo { progressRequired = true @@ -486,8 +472,10 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } case .Local: state = .none - if isSecretMedia && secretProgressIcon != nil { - state = .customIcon(secretProgressIcon!) + if isSecretMedia, let (beginTime, timeout) = secretBeginTimeAndTimeout { + state = .secretTimeout(color: bubbleTheme.mediaOverlayControlForegroundColor, icon: secretProgressIcon, beginTime: beginTime, timeout: timeout) + } else if isSecretMedia, let secretProgressIcon = secretProgressIcon { + state = .customIcon(secretProgressIcon) } else if let file = media as? TelegramMediaFile { if !isInlinePlayableVideo && file.isVideo { state = .play(bubbleTheme.mediaOverlayControlForegroundColor) diff --git a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift index c4abc3c60b..8e3e935b47 100644 --- a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift +++ b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift @@ -56,7 +56,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, nil, nil, false, layoutConstants, constrainedSize) - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .none, forceFullCorners: false, forceAlignment: .none) return (contentProperties, nil, initialWidth, { constrainedSize, position in let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position) diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index 2dd77fe208..07f931ed45 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -62,30 +62,30 @@ public enum ChatMessageItemContent: Sequence { } } -private func mediaIsNotMergeable(_ media: Media) -> Bool { +private func mediaMergeableStyle(_ media: Media) -> ChatMessageMerge { if let file = media as? TelegramMediaFile { for attribute in file.attributes { switch attribute { case .Sticker: - return false + return .semanticallyMerged case let .Video(_, _, flags): if flags.contains(.instantRoundVideo) { - return false + return .semanticallyMerged } default: break } } - return true + return .fullyMerged } if let _ = media as? TelegramMediaAction { - return true + return .none } if let _ = media as? TelegramMediaExpiredContent { - return true + return .none } - return false + return .fullyMerged } private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs: Message) -> ChatMessageMerge { @@ -103,26 +103,31 @@ private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs } if abs(lhs.timestamp - rhs.timestamp) < Int32(5 * 60) && lhsEffectiveAuthor?.id == rhsEffectiveAuthor?.id { + var upperStyle: Int32 = ChatMessageMerge.fullyMerged.rawValue + var lowerStyle: Int32 = ChatMessageMerge.fullyMerged.rawValue for media in lhs.media { - if mediaIsNotMergeable(media) { - return .semanticallyMerged + let style = mediaMergeableStyle(media).rawValue + if style < upperStyle { + upperStyle = style } } for media in rhs.media { - if mediaIsNotMergeable(media) { - return .semanticallyMerged + let style = mediaMergeableStyle(media).rawValue + if style < lowerStyle { + lowerStyle = style } } for attribute in lhs.attributes { if let attribute = attribute as? ReplyMarkupMessageAttribute { if attribute.flags.contains(.inline) && !attribute.rows.isEmpty { - return .semanticallyMerged + upperStyle = ChatMessageMerge.semanticallyMerged.rawValue } break } } - return .fullyMerged + let style = min(upperStyle, lowerStyle) + return ChatMessageMerge(rawValue: style)! } return .none @@ -168,10 +173,10 @@ public enum ChatMessageItemAdditionalContent { case eventLogPreviousLink(Message) } -enum ChatMessageMerge { - case none - case fullyMerged - case semanticallyMerged +enum ChatMessageMerge: Int32 { + case none = 0 + case fullyMerged = 1 + case semanticallyMerged = 2 var merged: Bool { if case .none = self { @@ -292,13 +297,9 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } } } else if let action = media as? TelegramMediaAction { - if case .phoneCall = action.action { - viewClassName = ChatMessageBubbleItemNode.self - } else { - viewClassName = ChatMessageActionItemNode.self - } + viewClassName = ChatMessageBubbleItemNode.self } else if let _ = media as? TelegramMediaExpiredContent { - viewClassName = ChatMessageActionItemNode.self + viewClassName = ChatMessageBubbleItemNode.self } } diff --git a/TelegramUI/ChatMessageMapBubbleContentNode.swift b/TelegramUI/ChatMessageMapBubbleContentNode.swift index c8cab4b36e..e5095e0606 100644 --- a/TelegramUI/ChatMessageMapBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMapBubbleContentNode.swift @@ -134,7 +134,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { maximumWidth = imageSize.width + bubbleInsets.left + bubbleInsets.right } - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 5.0, hidesBackgroundForEmptyWallpapers: activeLiveBroadcastingTimeout == nil && selectedMedia?.venue == nil, forceFullCorners: false) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 5.0, hidesBackground: (activeLiveBroadcastingTimeout == nil && selectedMedia?.venue == nil) ? .emptyWallpaper : .none, forceFullCorners: false, forceAlignment: .none) var pinPeer: Peer? var pinLiveLocationActive: Bool? diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index 2e3f8545cb..109c505557 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -32,8 +32,8 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { self.interactiveImageNode.activateLocalContent = { [weak self] in if let strongSelf = self { - if let item = strongSelf.item, !item.message.containsSecretMedia { - item.controllerInteraction.openMessage(item.message) + if let item = strongSelf.item { + let _ = item.controllerInteraction.openMessage(item.message) } } } @@ -86,7 +86,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { forceFullCorners = true } - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 7.0, hidesBackgroundForEmptyWallpapers: true, forceFullCorners: forceFullCorners) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 7.0, hidesBackground: .emptyWallpaper, forceFullCorners: forceFullCorners, forceAlignment: .none) return (contentProperties, unboundSize, initialWidth + bubbleInsets.left + bubbleInsets.right, { constrainedSize, position in var updatedPosition: ChatMessageBubbleContentPosition = position @@ -123,7 +123,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { let dateText = stringForMessageTimestampStatus(message: item.message, timeFormat: item.presentationData.timeFormat, strings: item.presentationData.strings) let statusType: ChatMessageDateAndStatusType? - var statusHorizontalOffset: CGFloat = 0.0 switch position { case .linear(_, .None): if item.message.effectivelyIncoming(item.account.peerId) { @@ -184,7 +183,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } statusApply(hasAnimation) - let dateAndStatusFrame = CGRect(origin: CGPoint(x: layoutSize.width - bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width + statusHorizontalOffset, y: layoutSize.height - bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) + let dateAndStatusFrame = CGRect(origin: CGPoint(x: layoutSize.width - bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) strongSelf.dateAndStatusNode.frame = dateAndStatusFrame strongSelf.dateAndStatusNode.bounds = CGRect(origin: CGPoint(), size: dateAndStatusFrame.size) @@ -238,7 +237,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { - if let message = self.item?.message, let currentMedia = self.media { + if let message = self.item?.message, let currentMedia = self.media, !message.containsSecretMedia { if self.interactiveImageNode.frame.contains(point) { return (message, .media(currentMedia)) } @@ -262,11 +261,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { - if self.interactiveImageNode.frame.contains(point) { - if let item = self.item, item.message.containsSecretMedia { - return .holdToPreviewSecretMedia - } - } return .none } diff --git a/TelegramUI/ChatMessageReplyInfoNode.swift b/TelegramUI/ChatMessageReplyInfoNode.swift index e508249ee1..a98c9002c9 100644 --- a/TelegramUI/ChatMessageReplyInfoNode.swift +++ b/TelegramUI/ChatMessageReplyInfoNode.swift @@ -111,10 +111,13 @@ class ChatMessageReplyInfoNode: ASDisplayNode { leftInset += 36.0 let boundingSize = CGSize(width: 30.0, height: 30.0) var radius: CGFloat = 2.0 + var imageSize = imageDimensions.aspectFilled(boundingSize) if hasRoundImage { radius = boundingSize.width / 2.0 + imageSize.width += 2.0 + imageSize.height += 2.0 } - applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) } var mediaUpdated = false diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index 2f0d08984e..dbd355d1f2 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -61,7 +61,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let currentCachedChatMessageText = self.cachedChatMessageText return { item, layoutConstants, _, _, _ in - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .none, forceFullCorners: false, forceAlignment: .none) return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in let message = item.message diff --git a/TelegramUI/ChatMessageThrottledProcessingManager.swift b/TelegramUI/ChatMessageThrottledProcessingManager.swift index 3f407488c4..3badd01138 100644 --- a/TelegramUI/ChatMessageThrottledProcessingManager.swift +++ b/TelegramUI/ChatMessageThrottledProcessingManager.swift @@ -3,7 +3,7 @@ import Postbox import SwiftSignalKit final class ChatMessageThrottledProcessingManager { - private let queue = Queue(target: Queue.concurrentBackgroundQueue()) + private let queue = Queue() private let delay: Double diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index c898b6d159..15e624a1d2 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -229,7 +229,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.account, item.message, item.read, title, subtitle, text, entities, mediaAndFlags, actionIcon, actionTitle, true, layoutConstants, constrainedSize) - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackgroundForEmptyWallpapers: false, forceFullCorners: false) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .none, forceFullCorners: false, forceAlignment: .none) return (contentProperties, nil, initialWidth, { constrainedSize, position in let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position) diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index ca835abd16..e3547fbc65 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -400,7 +400,7 @@ final class ChatRecordedMediaPreview: Equatable { struct ChatPresentationInterfaceState: Equatable { let interfaceState: ChatInterfaceState let chatLocation: ChatLocation - let peer: RenderedPeer? + let renderedPeer: RenderedPeer? let inputTextPanelState: ChatTextInputPanelState let recordedMediaPreview: ChatRecordedMediaPreview? let inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] @@ -430,7 +430,7 @@ struct ChatPresentationInterfaceState: Equatable { self.inputTextPanelState = ChatTextInputPanelState() self.recordedMediaPreview = nil self.chatLocation = chatLocation - self.peer = nil + self.renderedPeer = nil self.inputQueryResults = [:] self.inputMode = .none self.titlePanelContexts = [] @@ -454,10 +454,10 @@ struct ChatPresentationInterfaceState: Equatable { self.mode = mode } - init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, peer: RenderedPeer?, inputTextPanelState: ChatTextInputPanelState, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: Message?, peerIsBlocked: Bool, peerIsMuted: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, editingUrlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, accountPeerId: PeerId, mode: ChatControllerPresentationMode) { + init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, inputTextPanelState: ChatTextInputPanelState, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: Message?, peerIsBlocked: Bool, peerIsMuted: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, editingUrlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, accountPeerId: PeerId, mode: ChatControllerPresentationMode) { self.interfaceState = interfaceState self.chatLocation = chatLocation - self.peer = peer + self.renderedPeer = renderedPeer self.inputTextPanelState = inputTextPanelState self.recordedMediaPreview = recordedMediaPreview self.inputQueryResults = inputQueryResults @@ -487,11 +487,7 @@ struct ChatPresentationInterfaceState: Equatable { if lhs.interfaceState != rhs.interfaceState { return false } - if let lhsPeer = lhs.peer, let rhsPeer = rhs.peer { - if lhsPeer != rhsPeer { - return false - } - } else if (lhs.peer == nil) != (rhs.peer == nil) { + if lhs.renderedPeer != rhs.renderedPeer { return false } @@ -619,11 +615,11 @@ struct ChatPresentationInterfaceState: Equatable { } func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPeer(_ f: (RenderedPeer?) -> RenderedPeer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: f(self.peer), inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedInputQueryResult(queryKind: ChatPresentationInputQueryKind, _ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { @@ -634,80 +630,80 @@ struct ChatPresentationInterfaceState: Equatable { } else { inputQueryResults.removeValue(forKey: queryKind) } - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: f(self.inputTextPanelState), recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: f(self.inputTextPanelState), recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedRecordedMediaPreview(_ recordedMediaPreview: ChatRecordedMediaPreview?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedTitlePanelContext(_ f: ([ChatTitlePanelContext]) -> [ChatTitlePanelContext]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPinnedMessage(_ pinnedMessage: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPeerIsBlocked(_ peerIsBlocked: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedPeerIsMuted(_ peerIsMuted: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedCanReportPeer(_ canReportPeer: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedBotStartPayload(_ botStartPayload: String?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedChatHistoryState(_ chatHistoryState: ChatHistoryNodeHistoryState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedUrlPreview(_ urlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedEditingUrlPreview(_ editingUrlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedSearch(_ search: ChatSearchData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedSearchQuerySuggestionResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: self.mode) } func updatedMode(_ mode: ChatControllerPresentationMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, peer: self.peer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: mode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, inputTextPanelState: self.inputTextPanelState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, fontSize: self.fontSize, accountPeerId: self.accountPeerId, mode: mode) } } func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bool { - if let peer = state.peer?.peer { + if let peer = state.renderedPeer?.peer { if canSendMessagesToPeer(peer) { return true } else { diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index 128ba9bdf5..8f2c9a35ec 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -166,7 +166,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }) } return false - }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { [weak self] peerId, _, message in + }, openPeer: { [weak self] peerId, _, message in if let peerId = peerId { self?.openPeer(peerId: peerId, peer: message?.peers[peerId]) } diff --git a/TelegramUI/ChatReportPeerTitlePanelNode.swift b/TelegramUI/ChatReportPeerTitlePanelNode.swift index 38d52ddd0a..f1cbdef9e8 100644 --- a/TelegramUI/ChatReportPeerTitlePanelNode.swift +++ b/TelegramUI/ChatReportPeerTitlePanelNode.swift @@ -60,7 +60,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: 14.0), size: closeButtonSize)) let updatedButtons: [ChatReportPeerTitleButton] - if let _ = interfaceState.peer { + if let _ = interfaceState.renderedPeer?.peer { updatedButtons = peerButtons(interfaceState) } else { updatedButtons = [] diff --git a/TelegramUI/ChatSearchInputPanelNode.swift b/TelegramUI/ChatSearchInputPanelNode.swift index 6b76247fbc..ca12126a45 100644 --- a/TelegramUI/ChatSearchInputPanelNode.swift +++ b/TelegramUI/ChatSearchInputPanelNode.swift @@ -131,9 +131,9 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { var canSearchMembers = false if let search = interfaceState.search { if case .everything = search.domain { - if let _ = interfaceState.peer as? TelegramGroup { + if let _ = interfaceState.renderedPeer?.peer as? TelegramGroup { canSearchMembers = true - } else if let peer = interfaceState.peer as? TelegramChannel, case .group = peer.info { + } else if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel, case .group = peer.info { canSearchMembers = true } } else { diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index ae018cdd25..27bae6a9ea 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -544,7 +544,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - if let peer = interfaceState.peer?.peer, previousState?.peer?.peer == nil || !peer.isEqual(previousState!.peer!.peer!) { + if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) { let placeholder: String if let channel = peer as? TelegramChannel, case .broadcast = channel.info { placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index 758211ce6c..b611783dd3 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -64,7 +64,7 @@ private final class ContactListNodeInteraction { private enum ContactListNodeEntry: Comparable, Identifiable { case search(PresentationTheme, PresentationStrings) case option(Int, ContactListAdditionalOption, PresentationTheme, PresentationStrings) - case peer(Int, Peer, PeerPresence?, ContactListNameIndexHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings) + case peer(Int, Peer, PeerPresence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings) var stableId: ContactListNodeEntryId { switch self { @@ -83,7 +83,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { return ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { interaction.activateSearch() }) - case let .option(_, option, theme, strings): + case let .option(_, option, theme, _): return ContactListActionItem(theme: theme, title: option.title, icon: option.icon, action: option.action) case let .peer(_, peer, presence, header, selection, theme, strings): let status: ContactsPeerItemStatus @@ -128,7 +128,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } else if (lhsPresence != nil) != (rhsPresence != nil) { return false } - if lhsHeader != rhsHeader { + if lhsHeader?.id != rhsHeader?.id { return false } if lhsSelection != rhsSelection { @@ -214,7 +214,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences var headers: [PeerId: ContactListNameIndexHeader] = [:] switch presentation { - case .orderedByPresence: + case let .orderedByPresence(options): entries.append(.search(theme, strings)) orderedPeers = peers.sorted(by: { lhs, rhs in let lhsPresence = presences[lhs.id] @@ -232,6 +232,9 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences } return lhs.id < rhs.id }) + for i in 0 ..< options.count { + entries.append(.option(i, options[i], theme, strings)) + } case let .natural(displaySearch, options): orderedPeers = peers.sorted(by: { lhs, rhs in let result = lhs.indexName.isLessThan(other: rhs.indexName) @@ -294,6 +297,14 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences } } + var commonHeader: ListViewItemHeader? + switch presentation { + case .orderedByPresence: + commonHeader = ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil) + default: + break + } + for i in 0 ..< orderedPeers.count { let selection: ContactsPeerItemSelection if let selectionState = selectionState { @@ -301,7 +312,14 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences } else { selection = .none } - entries.append(.peer(i, orderedPeers[i], presences[orderedPeers[i].id], headers[orderedPeers[i].id], selection, theme, strings)) + let header: ListViewItemHeader? + switch presentation { + case .orderedByPresence: + header = commonHeader + default: + header = headers[orderedPeers[i].id] + } + entries.append(.peer(i, orderedPeers[i], presences[orderedPeers[i].id], header, selection, theme, strings)) } return entries } @@ -335,7 +353,7 @@ public struct ContactListAdditionalOption: Equatable { } enum ContactListPresentation { - case orderedByPresence + case orderedByPresence(options: [ContactListAdditionalOption]) case natural(displaySearch: Bool, options: [ContactListAdditionalOption]) case search(Signal) } @@ -401,9 +419,9 @@ final class ContactListNode: ASDisplayNode { if value != self.enableUpdatesValue { self.enableUpdatesValue = value if value { - self.contactPeersViewPromise.set(self.account.postbox.contactPeersView(accountPeerId: self.account.peerId)) + self.contactPeersViewPromise.set(self.account.postbox.contactPeersView(accountPeerId: self.account.peerId, includePresences: true)) } else { - self.contactPeersViewPromise.set(self.account.postbox.contactPeersView(accountPeerId: self.account.peerId) |> take(1)) + self.contactPeersViewPromise.set(self.account.postbox.contactPeersView(accountPeerId: self.account.peerId, includePresences: true) |> take(1)) } } } @@ -584,7 +602,7 @@ final class ContactListNode: ASDisplayNode { options.insert(.Synchronous) options.insert(.LowLatency) } else if transition.animated { - options.insert(.AnimateInsertion) + options.insert(.AnimateCrossfade) } self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self { diff --git a/TelegramUI/ContactsController.swift b/TelegramUI/ContactsController.swift index 8bddbbdf9b..c8ff48d7b1 100644 --- a/TelegramUI/ContactsController.swift +++ b/TelegramUI/ContactsController.swift @@ -37,6 +37,7 @@ public class ContactsController: ViewController { self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconContacts") self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationAddIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.addPressed)) self.scrollToTop = { [weak self] in if let strongSelf = self { @@ -102,6 +103,12 @@ public class ContactsController: ViewController { } } + self.contactsNode.openInvite = { [weak self] in + if let strongSelf = self { + (strongSelf.navigationController as? NavigationController)?.pushViewController(InviteContactsController(account: strongSelf.account)) + } + } + self.displayNodeDidLoad() } @@ -139,4 +146,8 @@ public class ContactsController: ViewController { self.contactsNode.deactivateSearch() } } + + @objc func addPressed() { + (self.navigationController as? NavigationController)?.pushViewController(createContactController(account: self.account)) + } } diff --git a/TelegramUI/ContactsControllerNode.swift b/TelegramUI/ContactsControllerNode.swift index 8f3805ba79..ca27ecbea9 100644 --- a/TelegramUI/ContactsControllerNode.swift +++ b/TelegramUI/ContactsControllerNode.swift @@ -17,16 +17,21 @@ final class ContactsControllerNode: ASDisplayNode { var requestDeactivateSearch: (() -> Void)? var requestOpenPeerFromSearch: ((PeerId) -> Void)? + var openInvite: (() -> Void)? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? init(account: Account) { self.account = account - self.contactListNode = ContactListNode(account: account, presentation: .orderedByPresence) self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + var inviteImpl: (() -> Void)? + self.contactListNode = ContactListNode(account: account, presentation: .orderedByPresence(options: [ContactListAdditionalOption(title: presentationData.strings.Contacts_InviteFriends, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/AddMemberIcon"), color: self.presentationData.theme.list.itemAccentColor), action: { + inviteImpl?() + })])) + super.init() self.setViewBlock({ @@ -50,6 +55,10 @@ final class ContactsControllerNode: ASDisplayNode { } } }) + + inviteImpl = { [weak self] in + self?.openInvite?() + } } deinit { diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index 562c61df19..292f6f60b8 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -17,6 +17,7 @@ enum ContactsPeerItemStatus { case none case presence(PeerPresence) case addressName(String) + case custom(String) } enum ContactsPeerItemSelection: Equatable { @@ -222,7 +223,7 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private let titleNode: TextNode private var verificationIconNode: ASImageNode? private let statusNode: TextNode - private var selectionNode: ASImageNode? + private var selectionNode: CheckNode? private var avatarState: (Account, Peer?)? @@ -321,23 +322,23 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { var leftInset: CGFloat = 65.0 + params.leftInset let rightInset: CGFloat = 10.0 + params.rightInset - let updatedSelectionNode: ASImageNode? - var updatedSelectionImage: UIImage? + let updatedSelectionNode: CheckNode? + var isSelected = false switch item.selection { case .none: updatedSelectionNode = nil case let .selectable(selected): leftInset += 28.0 + isSelected = selected - let selectionNode: ASImageNode + let selectionNode: CheckNode if let current = currentSelectionNode { selectionNode = current updatedSelectionNode = selectionNode } else { - selectionNode = ASImageNode() + selectionNode = CheckNode(strokeColor: item.theme.list.itemCheckColors.strokeColor, fillColor: item.theme.list.itemCheckColors.fillColor, foregroundColor: item.theme.list.itemCheckColors.foregroundColor, style: .plain) updatedSelectionNode = selectionNode } - updatedSelectionImage = selected ? selectedImage : selectableImage } var isVerified = false @@ -409,6 +410,8 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } else if !suffix.isEmpty { statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } + case let .custom(text): + statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } } @@ -495,12 +498,9 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.selectionNode = updatedSelectionNode strongSelf.addSubnode(updatedSelectionNode) } - if updatedSelectionImage !== updatedSelectionNode.image { - updatedSelectionNode.image = updatedSelectionImage - } - if let updatedSelectionImage = updatedSelectionImage { - updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 10.0, y: floor((nodeLayout.contentSize.height - updatedSelectionImage.size.height) / 2.0)), size: updatedSelectionImage.size) - } + updatedSelectionNode.setIsChecked(isSelected, animated: animated) + + updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 6.0, y: floor((nodeLayout.contentSize.height - 32.0) / 2.0)), size: CGSize(width: 32.0, height: 32.0)) } else if let selectionNode = strongSelf.selectionNode { selectionNode.removeFromSupernode() strongSelf.selectionNode = nil diff --git a/TelegramUI/CreateContactController.swift b/TelegramUI/CreateContactController.swift new file mode 100644 index 0000000000..868bf056b7 --- /dev/null +++ b/TelegramUI/CreateContactController.swift @@ -0,0 +1,380 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class CreateContactControllerArguments { + let account: Account + let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void + let updatePhone: (Int64, String) -> Void + let openLabelSelection: (Int64, String) -> Void + let addPhone: () -> Void + let deletePhone: (Int64) -> Void + + init(account: Account, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updatePhone: @escaping (Int64, String) -> Void, openLabelSelection: @escaping (Int64, String) -> Void, addPhone: @escaping () -> Void, deletePhone: @escaping (Int64) -> Void) { + self.account = account + self.updateEditingName = updateEditingName + self.updatePhone = updatePhone + self.openLabelSelection = openLabelSelection + self.addPhone = addPhone + self.deletePhone = deletePhone + } +} + +private enum CreateContactSection: ItemListSectionId { + case info + case phones +} + +private enum CreateContactEntryId: Hashable { + case index(Int) + case phone(Int64) + + static func ==(lhs: CreateContactEntryId, rhs: CreateContactEntryId) -> Bool { + switch lhs { + case let .index(value): + if case .index(value) = rhs { + return true + } else { + return false + } + case let .phone(value): + if case .phone(value) = rhs { + return true + } else { + return false + } + } + } + + var hashValue: Int { + switch self { + case let .index(value): + return value.hashValue + case let .phone(value): + return value.hashValue + } + } +} + +private enum CreateContactEntry: ItemListNodeEntry { + case info(PresentationTheme, PresentationStrings, state: ItemListAvatarAndNameInfoItemState) + case phoneNumber(PresentationTheme, PresentationStrings, Int64, Int, String, String, Bool) + case addPhone(PresentationTheme, String) + + var section: ItemListSectionId { + switch self { + case .info: + return CreateContactSection.info.rawValue + case .phoneNumber, .addPhone: + return CreateContactSection.phones.rawValue + } + } + + var stableId: CreateContactEntryId { + switch self { + case .info: + return .index(0) + case let .phoneNumber(_, _, id, _, _, _, _): + return .phone(id) + case .addPhone: + return .index(1000) + } + } + + static func ==(lhs: CreateContactEntry, rhs: CreateContactEntry) -> Bool { + switch lhs { + case let .info(lhsTheme, lhsStrings, lhsState): + if case let .info(rhsTheme, rhsStrings, rhsState) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if lhsState != rhsState { + return false + } + return true + } else { + return false + } + case let .phoneNumber(lhsTheme, lhsStrings, lhsId, lhsIndex, lhsLabel, lhsValue, lhsHasActiveRevealControls): + if case let .phoneNumber(rhsTheme, rhsStrings, rhsId, rhsIndex, rhsLabel, rhsValue, rhsHasActiveRevealControls) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsId == rhsId, lhsIndex == rhsIndex, lhsLabel == rhsLabel, lhsValue == rhsValue, lhsHasActiveRevealControls == rhsHasActiveRevealControls { + return true + } else { + return false + } + case let .addPhone(lhsTheme, lhsTitle): + if case let .addPhone(rhsTheme, rhsTitle) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsTitle != rhsTitle { + return false + } + return true + } else { + return false + } + } + } + + private var sortIndex: Int { + switch self { + case .info: + return 0 + case let .phoneNumber(_, _, _, index, _, _, _): + return 2 + index + case .addPhone: + return 1000 + } + } + + static func <(lhs: CreateContactEntry, rhs: CreateContactEntry) -> Bool { + return lhs.sortIndex < rhs.sortIndex + } + + func item(_ arguments: CreateContactControllerArguments) -> ListViewItem { + switch self { + case let .info(theme, strings, state): + var firstName = "" + var lastName = "" + if let editingName = state.editingName { + switch editingName { + case let .personName(first, last): + firstName = first + lastName = last + default: + break + } + } + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: firstName, lastName: lastName, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []), presence: nil, cachedData: nil, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in + arguments.updateEditingName(editingName) + }, avatarTapped: { + }, context: nil, call: nil) + case let .phoneNumber(theme, strings, id, _, label, value, hasActiveRevealControls): + return UserInfoEditingPhoneItem(theme: theme, strings: strings, id: id, label: label, value: value, editing: UserInfoEditingPhoneItemEditing(editable: true, hasActiveRevealControls: hasActiveRevealControls), sectionId: self.section, setPhoneIdWithRevealedOptions: { _, _ in + }, updated: { value in + arguments.updatePhone(id, value) + }, selectLabel: { + arguments.openLabelSelection(id, label) + }, delete: { + arguments.deletePhone(id) + }) + case let .addPhone(theme, title): + return UserInfoEditingPhoneActionItem(theme: theme, title: title, sectionId: self.section, action: { + arguments.addPhone() + }) + } + } +} + +private struct CreateContactPhoneNumber: Equatable { + let id: Int64 + let label: String + let value: String + + static func ==(lhs: CreateContactPhoneNumber, rhs: CreateContactPhoneNumber) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.label != rhs.label { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + func withUpdatedLabel(_ label: String) -> CreateContactPhoneNumber { + return CreateContactPhoneNumber(id: self.id, label: label, value: self.value) + } + + func withUpdatedValue(_ value: String) -> CreateContactPhoneNumber { + return CreateContactPhoneNumber(id: self.id, label: self.label, value: value) + } +} + +private struct CreateContactState: Equatable { + var editingName: ItemListAvatarAndNameInfoItemName + var phoneNumbers: [CreateContactPhoneNumber] + var revealedPhoneId: Int64? + + init(editingName: ItemListAvatarAndNameInfoItemName = .personName(firstName: "", lastName: ""), phoneNumbers: [CreateContactPhoneNumber] = [], revealedPhoneId: Int64? = nil) { + self.editingName = editingName + self.phoneNumbers = phoneNumbers + self.revealedPhoneId = revealedPhoneId + } + + static func ==(lhs: CreateContactState, rhs: CreateContactState) -> Bool { + if lhs.editingName != rhs.editingName { + return false + } + if lhs.phoneNumbers != rhs.phoneNumbers { + return false + } + return true + } +} + +private func localizedPhoneNumberLabel(label: String, strings: PresentationStrings) -> String { + if label == "_$!!$_" { + return "mobile" + } else if label == "_$!!$_" { + return "home" + } else { + return label + } +} + +private func createContactEntries(account: Account, presentationData: PresentationData, state: CreateContactState) -> [CreateContactEntry] { + var entries: [CreateContactEntry] = [] + + entries.append(.info(presentationData.theme, presentationData.strings, state: ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil))) + + var index = 0 + for phoneNumber in state.phoneNumbers { + entries.append(.phoneNumber(presentationData.theme, presentationData.strings, phoneNumber.id, index, phoneNumber.label, phoneNumber.value, state.revealedPhoneId == phoneNumber.id)) + index += 1 + } + + entries.append(.addPhone(presentationData.theme, presentationData.strings.UserInfo_AddPhone)) + + return entries +} + +public func createContactController(account: Account) -> ViewController { + var initialState = CreateContactState() + initialState.phoneNumbers.append(CreateContactPhoneNumber(id: arc4random64(), label: "mobile", value: "")) + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((CreateContactState) -> CreateContactState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var dismissImpl: (() -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? + + let actionsDisposable = DisposableSet() + + let arguments = CreateContactControllerArguments(account: account, + updateEditingName: { editingName in + updateState { state in + var state = state + state.editingName = editingName + return state + } + }, updatePhone: { id, value in + updateState { state in + var state = state + for i in 0 ..< state.phoneNumbers.count { + if state.phoneNumbers[i].id == id { + state.phoneNumbers[i] = state.phoneNumbers[i].withUpdatedValue(value) + break + } + } + return state + } + }, openLabelSelection: { id, label in + + }, addPhone: { + updateState { state in + var state = state + state.phoneNumbers.append(CreateContactPhoneNumber(id: arc4random64(), label: "mobile", value: "")) + return state + } + }, deletePhone: { id in + updateState { state in + var state = state + for i in 0 ..< state.phoneNumbers.count { + if state.phoneNumbers[i].id == id { + state.phoneNumbers.remove(at: i) + break + } + } + return state + } + }) + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, CreateContactEntry.ItemGenerationArguments)) in + + var canSave = true + switch state.editingName { + case let .personName(first, last): + if first.isEmpty && last.isEmpty { + canSave = false + } + default: + canSave = false + } + + var hasPhoneNumbers = false + for phoneNumber in state.phoneNumbers { + if !phoneNumber.value.isEmpty { + hasPhoneNumbers = true + } + } + + if !hasPhoneNumbers { + canSave = false + } + + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }) + let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: canSave, action: { + var state: CreateContactState? + updateState { + state = $0 + return $0 + } + if let state = state { + var firstName = "" + var lastName = "" + switch state.editingName { + case let .personName(first, last): + firstName = first + lastName = last + default: + break + } + var phoneNumbers: [DeviceContactPhoneNumber] = [] + for number in state.phoneNumbers { + if !number.value.isEmpty { + phoneNumbers.append(DeviceContactPhoneNumber(label: number.label, number: DeviceContactPhoneNumberValue(plain: number.value, normalized: DeviceContactNormalizedPhoneNumber(rawValue: number.value)))) + } + } + let _ = (account.telegramApplicationContext.contactsManager.add(firstName: firstName, lastName: lastName, phoneNumbers: phoneNumbers) + |> deliverOnMainQueue).start(next: { _ in + dismissImpl?() + }) + } + }) + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.NewContact_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) + let listState = ItemListNodeState(entries: createContactEntries(account: account, presentationData: presentationData, state: state), style: .plain) + + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(account: account, state: signal) + dismissImpl = { [weak controller] in + if let navigationController = controller?.navigationController as? NavigationController { + let _ = navigationController.popViewController(animated: true) + } else { + controller?.dismiss() + } + } + presentControllerImpl = { [weak controller] value, presentationArguments in + controller?.present(value, in: .window(.root), with: presentationArguments) + } + + return controller +} + diff --git a/TelegramUI/DebugController.swift b/TelegramUI/DebugController.swift index d5a10db4f1..5edac8bb53 100644 --- a/TelegramUI/DebugController.swift +++ b/TelegramUI/DebugController.swift @@ -22,6 +22,7 @@ private enum DebugControllerSection: Int32 { case logs case payments case logging + case experiments } private enum DebugControllerEntry: ItemListNodeEntry { @@ -30,6 +31,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case clearPaymentData(PresentationTheme) case logToFile(PresentationTheme, Bool) case logToConsole(PresentationTheme, Bool) + case enableRaiseToSpeak(PresentationTheme, Bool) var section: ItemListSectionId { switch self { @@ -40,7 +42,9 @@ private enum DebugControllerEntry: ItemListNodeEntry { case .clearPaymentData: return DebugControllerSection.payments.rawValue case .logToFile, .logToConsole: - return DebugControllerSection.logging.rawValue + return DebugControllerSection.logging.rawValue + case .enableRaiseToSpeak: + return DebugControllerSection.experiments.rawValue } } @@ -56,6 +60,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 3 case .logToConsole: return 4 + case .enableRaiseToSpeak: + return 5 } } @@ -91,6 +97,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { } else { return false } + case let .enableRaiseToSpeak(lhsTheme, lhsValue): + if case let .enableRaiseToSpeak(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { + return true + } else { + return false + } } } @@ -140,11 +152,17 @@ private enum DebugControllerEntry: ItemListNodeEntry { $0.withUpdatedLogToConsole(value) }).start() }) + case let .enableRaiseToSpeak(theme, value): + return ItemListSwitchItem(theme: theme, title: "Enable Raise to Speak", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = updateMediaInputSettingsInteractively(postbox: arguments.account.postbox, { + $0.withUpdatedEnableRaiseToSpeak(value) + }).start() + }) } } } -private func debugControllerEntries(presentationData: PresentationData, loggingSettings: LoggingSettings) -> [DebugControllerEntry] { +private func debugControllerEntries(presentationData: PresentationData, loggingSettings: LoggingSettings, mediaInputSettings: MediaInputSettings) -> [DebugControllerEntry] { var entries: [DebugControllerEntry] = [] entries.append(.sendLogs(presentationData.theme)) @@ -154,6 +172,8 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS entries.append(.logToFile(presentationData.theme, loggingSettings.logToFile)) entries.append(.logToConsole(presentationData.theme, loggingSettings.logToConsole)) + entries.append(.enableRaiseToSpeak(presentationData.theme, mediaInputSettings.enableRaiseToSpeak)) + return entries } @@ -167,7 +187,7 @@ public func debugController(account: Account, accountManager: AccountManager) -> pushControllerImpl?(controller) }) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, account.postbox.preferencesView(keys: [PreferencesKeys.loggingSettings])) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, account.postbox.preferencesView(keys: [PreferencesKeys.loggingSettings, ApplicationSpecificPreferencesKeys.mediaInputSettings])) |> map { presentationData, preferencesView -> (ItemListControllerState, (ItemListNodeState, DebugControllerEntry.ItemGenerationArguments)) in let loggingSettings: LoggingSettings if let value = preferencesView.values[PreferencesKeys.loggingSettings] as? LoggingSettings { @@ -176,8 +196,15 @@ public func debugController(account: Account, accountManager: AccountManager) -> loggingSettings = LoggingSettings.defaultSettings } + let mediaInputSettings: MediaInputSettings + if let value = preferencesView.values[ApplicationSpecificPreferencesKeys.mediaInputSettings] as? MediaInputSettings { + mediaInputSettings = value + } else { + mediaInputSettings = MediaInputSettings.defaultSettings + } + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Debug"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: debugControllerEntries(presentationData: presentationData, loggingSettings: loggingSettings), style: .blocks) + let listState = ItemListNodeState(entries: debugControllerEntries(presentationData: presentationData, loggingSettings: loggingSettings, mediaInputSettings: mediaInputSettings), style: .blocks) return (controllerState, (listState, arguments)) } diff --git a/TelegramUI/DeclareEncodables.swift b/TelegramUI/DeclareEncodables.swift index 62fa22edf9..8afbc8c756 100644 --- a/TelegramUI/DeclareEncodables.swift +++ b/TelegramUI/DeclareEncodables.swift @@ -17,6 +17,7 @@ private var telegramUIDeclaredEncodables: Void = { declareEncodable(ExperimentalSettings.self, f: { ExperimentalSettings(decoder: $0) }) declareEncodable(MusicPlaybackSettings.self, f: { MusicPlaybackSettings(decoder: $0) }) declareEncodable(ICloudFileResource.self, f: { ICloudFileResource(decoder: $0) }) + declareEncodable(MediaInputSettings.self, f: { MediaInputSettings(decoder: $0) }) return }() diff --git a/TelegramUI/DefaultDarkAccentPresentationTheme.swift b/TelegramUI/DefaultDarkAccentPresentationTheme.swift index 2e029a6157..9fe8ab3f3b 100644 --- a/TelegramUI/DefaultDarkAccentPresentationTheme.swift +++ b/TelegramUI/DefaultDarkAccentPresentationTheme.swift @@ -3,6 +3,7 @@ import UIKit private let accentColor: UIColor = UIColor(rgb: 0x2EA6FF) private let destructiveColor: UIColor = UIColor(rgb: 0xFF6767) +private let constructiveColor: UIColor = UIColor(rgb: 0x4cd964) private let secretColor: UIColor = UIColor(rgb: 0x89DF9E) private let rootStatusBar = PresentationThemeRootNavigationStatusBar( @@ -80,7 +81,8 @@ private let list = PresentationThemeList( itemDisclosureActions: PresentationThemeItemDisclosureActions( neutral1: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x415A71), foregroundColor: .white), neutral2: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x374F63), foregroundColor: .white), - destructive: PresentationThemeItemDisclosureAction(fillColor: destructiveColor, foregroundColor: .white) + destructive: PresentationThemeItemDisclosureAction(fillColor: destructiveColor, foregroundColor: .white), + constructive: PresentationThemeItemDisclosureAction(fillColor: constructiveColor, foregroundColor: .white) ), itemCheckColors: PresentationThemeCheck( strokeColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), diff --git a/TelegramUI/DefaultDarkPresentationTheme.swift b/TelegramUI/DefaultDarkPresentationTheme.swift index 2ff3a9dddb..02e90849d6 100644 --- a/TelegramUI/DefaultDarkPresentationTheme.swift +++ b/TelegramUI/DefaultDarkPresentationTheme.swift @@ -3,6 +3,7 @@ import UIKit private let accentColor: UIColor = UIColor(rgb: 0xffffff) private let destructiveColor: UIColor = UIColor(rgb: 0xFF736B) +private let constructiveColor: UIColor = UIColor(rgb: 0x4cd964) private let secretColor: UIColor = UIColor(rgb: 0x00B12C) private let rootStatusBar = PresentationThemeRootNavigationStatusBar( @@ -80,7 +81,8 @@ private let list = PresentationThemeList( itemDisclosureActions: PresentationThemeItemDisclosureActions( neutral1: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x666666), foregroundColor: .white), neutral2: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0x414141), foregroundColor: .white), - destructive: PresentationThemeItemDisclosureAction(fillColor: destructiveColor, foregroundColor: .white) + destructive: PresentationThemeItemDisclosureAction(fillColor: destructiveColor, foregroundColor: .white), + constructive: PresentationThemeItemDisclosureAction(fillColor: constructiveColor, foregroundColor: .white) ), itemCheckColors: PresentationThemeCheck( strokeColor: UIColor(rgb: 0xffffff, alpha: 0.5), diff --git a/TelegramUI/DefaultPresentationTheme.swift b/TelegramUI/DefaultPresentationTheme.swift index 60d46ac5c9..b66651dd05 100644 --- a/TelegramUI/DefaultPresentationTheme.swift +++ b/TelegramUI/DefaultPresentationTheme.swift @@ -3,6 +3,7 @@ import UIKit private let accentColor: UIColor = UIColor(rgb: 0x007ee5) private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) +private let constructiveColor: UIColor = UIColor(rgb: 0x4cd964) private let secretColor: UIColor = UIColor(rgb: 0x00B12C) private let rootStatusBar = PresentationThemeRootNavigationStatusBar( @@ -80,7 +81,8 @@ private let list = PresentationThemeList( itemDisclosureActions: PresentationThemeItemDisclosureActions( neutral1: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0xbcbcc3), foregroundColor: .white), neutral2: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0xaaaab3), foregroundColor: .white), - destructive: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0xff3824), foregroundColor: .white) + destructive: PresentationThemeItemDisclosureAction(fillColor: UIColor(rgb: 0xff3824), foregroundColor: .white), + constructive: PresentationThemeItemDisclosureAction(fillColor: constructiveColor, foregroundColor: .white) ), itemCheckColors: PresentationThemeCheck( strokeColor: UIColor(rgb: 0xC7C7CC), diff --git a/TelegramUI/DeviceContactsManager.swift b/TelegramUI/DeviceContactsManager.swift index 31d1ed66de..ad65135d99 100644 --- a/TelegramUI/DeviceContactsManager.swift +++ b/TelegramUI/DeviceContactsManager.swift @@ -25,6 +25,15 @@ private func authorizedContacts() -> Signal { } } +@available(iOSApplicationExtension 9.0, *) +private func parseContact(_ contact: CNContact) -> DeviceContact { + var phoneNumbers: [DeviceContactPhoneNumber] = [] + for number in contact.phoneNumbers { + phoneNumbers.append(DeviceContactPhoneNumber(label: number.label ?? "", number: DeviceContactPhoneNumberValue(plain: number.value.stringValue, normalized: DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(number.value.stringValue))))) + } + return DeviceContact(id: contact.identifier, firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers) +} + @available(iOSApplicationExtension 9.0, *) private func retrieveContactsWithStore(_ store: CNContactStore) -> [DeviceContact] { let keysToFetch: [CNKeyDescriptor] = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor] @@ -34,11 +43,7 @@ private func retrieveContactsWithStore(_ store: CNContactStore) -> [DeviceContac var result: [DeviceContact] = [] let _ = try? store.enumerateContacts(with: request, usingBlock: { contact, _ in - var phoneNumbers: [DeviceContactPhoneNumber] = [] - for number in contact.phoneNumbers { - phoneNumbers.append(DeviceContactPhoneNumber(label: number.label ?? "", number: number.value.stringValue)) - } - result.append(DeviceContact(id: contact.identifier, firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers)) + result.append(parseContact(contact)) }) return result } @@ -74,17 +79,156 @@ private func modernContacts() -> Signal<[DeviceContact], NoError> { } } +private final class DeviceContactsMappingSubscriberContext { + var subscribers = Bag<([DeviceContact]) -> Void>() + + var isEmpty: Bool { + if !self.subscribers.isEmpty { + return false + } + return true + } +} + +private final class DeviceContactsManagerContext { + private let queue: Queue + private var contacts: [DeviceContact] = [] + private var contactMapping: [DeviceContactNormalizedPhoneNumber: [DeviceContact]] = [:] + + private var disposable: Disposable? + + private var mappingSubscriberContexts: [DeviceContactNormalizedPhoneNumber: DeviceContactsMappingSubscriberContext] = [:] + + init(queue: Queue) { + self.queue = queue + + if #available(iOSApplicationExtension 9.0, *) { + self.disposable = (modernContacts() + |> deliverOn(self.queue)).start(next: { [weak self] value in + if let strongSelf = self { + strongSelf.updateContacts(value) + } + }) + } + } + + private func updateContacts(_ contacts: [DeviceContact]) { + self.contacts = contacts + var contactMapping: [DeviceContactNormalizedPhoneNumber: [DeviceContact]] = [:] + + for contact in contacts { + for phoneNumber in contact.phoneNumbers { + if contactMapping[phoneNumber.number.normalized] == nil { + contactMapping[phoneNumber.number.normalized] = [] + } + contactMapping[phoneNumber.number.normalized]!.append(contact) + } + } + + self.contactMapping = contactMapping + + for (key, context) in self.mappingSubscriberContexts { + for f in context.subscribers.copyItems() { + f(contactMapping[key] ?? []) + } + } + } + + func subscribe(_ number: DeviceContactNormalizedPhoneNumber) -> Signal<[DeviceContact], NoError> { + let queue = self.queue + return Signal { [weak self] subscriber in + if let strongSelf = self { + subscriber.putNext(strongSelf.contactMapping[number] ?? []) + let context: DeviceContactsMappingSubscriberContext + if let current = strongSelf.mappingSubscriberContexts[number] { + context = current + } else { + context = DeviceContactsMappingSubscriberContext() + strongSelf.mappingSubscriberContexts[number] = context + } + let index = context.subscribers.add({ next in + subscriber.putNext(next) + }) + return ActionDisposable { [weak context] in + queue.async { + if let strongSelf = self, let current = strongSelf.mappingSubscriberContexts[number], current === context { + current.subscribers.remove(index) + if current.isEmpty { + strongSelf.mappingSubscriberContexts.removeValue(forKey: number) + } + } + } + } + } else { + subscriber.putNext([]) + subscriber.putCompletion() + return EmptyDisposable + } + } |> runOn(self.queue) + } + + deinit { + self.disposable?.dispose() + } +} + public final class DeviceContactsManager { private let contactsValue = Promise<[DeviceContact]>() public var contacts: Signal<[DeviceContact], NoError> { return self.contactsValue.get() } + private let impl: QueueLocalObject + init() { + let queue = Queue() + self.impl = QueueLocalObject(queue: queue, generate: { + return DeviceContactsManagerContext(queue: queue) + }) + if #available(iOSApplicationExtension 9.0, *) { self.contactsValue.set(modernContacts()) - } else { - + } + } + + public func subscribe(_ number: DeviceContactNormalizedPhoneNumber) -> Signal<[DeviceContact], NoError> { + return Signal, NoError> { subscriber in + self.impl.with { context in + subscriber.putNext(context.subscribe(number)) + subscriber.putCompletion() + } + return EmptyDisposable + } |> switchToLatest + } + + public func add(firstName: String, lastName: String, phoneNumbers: [DeviceContactPhoneNumber]) -> Signal { + return authorizedContacts() + |> mapToSignal { authorized -> Signal in + if !authorized { + return .single(nil) + } + if #available(iOSApplicationExtension 9.0, *) { + let store = CNContactStore() + + let contact = CNMutableContact() + contact.familyName = firstName + contact.givenName = lastName + + contact.phoneNumbers = phoneNumbers.map { value in + return CNLabeledValue(label: value.label, value: CNPhoneNumber(stringValue: value.number.normalized.rawValue)) + } + + let request = CNSaveRequest() + request.add(contact, toContainerWithIdentifier: nil) + + if let _ = try? store.execute(request) { + return .single(parseContact(contact)) + } else { + return .single(nil) + } + } else { + return .single(nil) + } } } } diff --git a/TelegramUI/FetchVideoMediaResource.swift b/TelegramUI/FetchVideoMediaResource.swift index 27e52cf891..692a3480f4 100644 --- a/TelegramUI/FetchVideoMediaResource.swift +++ b/TelegramUI/FetchVideoMediaResource.swift @@ -21,7 +21,7 @@ private final class AVURLAssetCopyItem: MediaResourceDataFetchCopyLocalItem { } } -private final class VideoConversionWatcher: TGMediaVideoFileWatcher { +class VideoConversionWatcher: TGMediaVideoFileWatcher { private let update: (String, Int) -> Void private var path: String? diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 7d1dcf5176..6c368cce71 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -71,7 +71,7 @@ private func mediaForMessage(message: Message) -> Media? { return nil } -func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: PresentationStrings, entry: MessageHistoryEntry, streamVideos: Bool) -> GalleryItem? { +func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: PresentationStrings, entry: MessageHistoryEntry, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, playbackCompleted: @escaping () -> Void = {}) -> GalleryItem? { switch entry { case let .MessageEntry(message, _, location, _): if let media = mediaForMessage(message: message) { @@ -79,7 +79,7 @@ func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: Pr return ChatImageGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } else if let file = media as? TelegramMediaFile { if file.isVideo || file.mimeType.hasPrefix("video/") { - return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(id: .message(message.id, file.fileId), file: file, streamVideo: streamVideos), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: message.text) + return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: NativeVideoContent(id: .message(message.id, file.fileId), file: file, streamVideo: streamVideos, loopVideo: loopVideos), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: message.text, hideControls: hideControls, playbackCompleted: playbackCompleted) } else { if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" && (file.size == nil || file.size! < 5 * 1024 * 1024) { return ChatImageGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) diff --git a/TelegramUI/GalleryControllerNode.swift b/TelegramUI/GalleryControllerNode.swift index a1208a622f..19841f537c 100644 --- a/TelegramUI/GalleryControllerNode.swift +++ b/TelegramUI/GalleryControllerNode.swift @@ -195,6 +195,9 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog } } + func updateDismissTransition(_ value: CGFloat) { + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { let distanceFromEquilibrium = scrollView.contentOffset.y - scrollView.contentSize.height / 3.0 @@ -208,6 +211,8 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog self.footerNode.alpha = transition } + self.updateDismissTransition(transition) + if let toolbarNode = toolbarNode { toolbarNode.layer.position = CGPoint(x: toolbarNode.layer.position.x, y: self.bounds.size.height - toolbarNode.bounds.size.height / 2.0 + (1.0 - transition) * toolbarNode.bounds.size.height) } @@ -269,7 +274,7 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog case .began: break case .changed: - print("changed") + break case .ended: break case .cancelled: diff --git a/TelegramUI/InviteContactsController.swift b/TelegramUI/InviteContactsController.swift new file mode 100644 index 0000000000..a106e55a06 --- /dev/null +++ b/TelegramUI/InviteContactsController.swift @@ -0,0 +1,169 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import SwiftSignalKit +import TelegramCore +import MessageUI + +public class InviteContactsController: ViewController, MFMessageComposeViewControllerDelegate, UINavigationControllerDelegate { + private let account: Account + + private var contactsNode: InviteContactsControllerNode { + return self.displayNode as! InviteContactsControllerNode + } + + private var _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private var composer: MFMessageComposeViewController? + + public init(account: Account) { + self.account = account + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.title = self.presentationData.strings.Contacts_InviteFriends + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: presentationData.strings.Contacts_SelectAll, style: .plain, target: self, action: #selector(self.selectAllPressed)) + + self.scrollToTop = { [weak self] in + if let strongSelf = self { + //strongSelf.contactsNode.listNode.scrollToTop() + } + } + + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() + } + } + }) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + self.title = self.presentationData.strings.Contacts_InviteFriends + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + } + + override public func loadDisplayNode() { + self.displayNode = InviteContactsControllerNode(account: self.account) + self._ready.set(self.contactsNode.ready) + + self.contactsNode.navigationBar = self.navigationBar + + self.contactsNode.requestDeactivateSearch = { [weak self] in + self?.deactivateSearch() + } + + self.contactsNode.requestActivateSearch = { [weak self] in + self?.activateSearch() + } + + self.contactsNode.requestShareTelegram = { [weak self] in + if let strongSelf = self { + let shareController = ShareController(account: strongSelf.account, subject: .url("https://telegram.org/dl"), externalShare: true, immediateExternalShare: true) + strongSelf.present(shareController, in: .window(.root)) + } + } + + self.contactsNode.requestShare = { [weak self] numbers in + if let strongSelf = self, MFMessageComposeViewController.canSendText() { + let composer = MFMessageComposeViewController() + composer.messageComposeDelegate = strongSelf + let recipients: [String] = Array(numbers.map { + return $0.0.phoneNumbers.map { $0.number.plain } + }.joined()) + composer.recipients = Array(Set(recipients)) + let url = strongSelf.presentationData.strings.InviteText_URL + var body = strongSelf.presentationData.strings.InviteText_SingleContact(url).0 + if numbers.count == 1, numbers[0].1 > 0 { + body = strongSelf.presentationData.strings.InviteText_ContactsCount(numbers[0].1) + body = body.replacingOccurrences(of: "(null)", with: url) + } + composer.body = body + strongSelf.composer = composer + if let window = strongSelf.view.window { + window.rootViewController?.present(composer, animated: true) + } + } + } + + self.displayNodeDidLoad() + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + override public func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + private func activateSearch() { + if self.displayNavigationBar { + if let scrollToTop = self.scrollToTop { + scrollToTop() + } + self.contactsNode.activateSearch() + self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + } + } + + private func deactivateSearch() { + if !self.displayNavigationBar { + self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + self.contactsNode.deactivateSearch() + } + } + + @objc func selectAllPressed() { + self.contactsNode.selectAll() + } + + @objc public func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) { + self.composer = nil + + controller.dismiss(animated: true, completion: nil) + + guard case .sent = result else { + return + } + + self.contactsNode.selectionState = self.contactsNode.selectionState.withClearedSelection() + } +} + diff --git a/TelegramUI/InviteContactsControllerNode.swift b/TelegramUI/InviteContactsControllerNode.swift new file mode 100644 index 0000000000..b210ba08da --- /dev/null +++ b/TelegramUI/InviteContactsControllerNode.swift @@ -0,0 +1,615 @@ +import Display +import AsyncDisplayKit +import UIKit +import Postbox +import TelegramCore +import SwiftSignalKit + +private enum InviteContactsEntryId: Hashable { + case search + case option(index: Int) + case contactId(String) + + var hashValue: Int { + switch self { + case .search: + return 0 + case let .option(index): + return (index + 2).hashValue + case let .contactId(contactId): + return contactId.hashValue + } + } + + static func <(lhs: InviteContactsEntryId, rhs: InviteContactsEntryId) -> Bool { + return lhs.hashValue < rhs.hashValue + } + + static func ==(lhs: InviteContactsEntryId, rhs: InviteContactsEntryId) -> Bool { + switch lhs { + case .search: + switch rhs { + case .search: + return true + default: + return false + } + case let .option(index): + if case .option(index) = rhs { + return true + } else { + return false + } + case let .contactId(lhsId): + switch rhs { + case let .contactId(rhsId): + return lhsId == rhsId + default: + return false + } + } + } +} + +private final class InviteContactsInteraction { + let activateSearch: () -> Void + let toggleContact: (String) -> Void + let shareTelegram: () -> Void + + init(activateSearch: @escaping () -> Void, toggleContact: @escaping (String) -> Void, shareTelegram: @escaping () -> Void) { + self.activateSearch = activateSearch + self.toggleContact = toggleContact + self.shareTelegram = shareTelegram + } +} + +private enum InviteContactsEntry: Comparable, Identifiable { + case search(PresentationTheme, PresentationStrings) + case option(Int, ContactListAdditionalOption, PresentationTheme, PresentationStrings) + case peer(Int, DeviceContact, Int32, ContactsPeerItemSelection, PresentationTheme, PresentationStrings) + + var stableId: InviteContactsEntryId { + switch self { + case .search: + return .search + case let .option(index, _, _, _): + return .option(index: index) + case let .peer(_, contact, _, _, _, _): + return .contactId(contact.id) + } + } + + func item(account: Account, interaction: InviteContactsInteraction) -> ListViewItem { + switch self { + case let .search(theme, strings): + return ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { + interaction.activateSearch() + }) + case let .option(_, option, theme, _): + return ContactListActionItem(theme: theme, title: option.title, icon: option.icon, action: option.action) + case let .peer(_, contact, count, selection, theme, strings): + let status: ContactsPeerItemStatus + if count != 0 { + status = .custom(strings.Contacts_ImportersCount(count)) + } else { + status = .none + } + let peer = TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: contact.firstName, lastName: contact.lastName, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in + interaction.toggleContact(contact.id) + }) + } + } + + static func ==(lhs: InviteContactsEntry, rhs: InviteContactsEntry) -> Bool { + switch lhs { + case let .search(lhsTheme, lhsStrings): + if case let .search(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings { + return true + } else { + return false + } + case let .option(lhsIndex, lhsOption, lhsTheme, lhsStrings): + if case let .option(rhsIndex, rhsOption, rhsTheme, rhsStrings) = rhs, lhsIndex == rhsIndex, lhsOption == rhsOption, lhsTheme === rhsTheme, lhsStrings === rhsStrings { + return true + } else { + return false + } + case let .peer(lhsIndex, lhsContact, lhsCount, lhsSelection, lhsTheme, lhsStrings): + switch rhs { + case let .peer(rhsIndex, rhsContact, rhsCount, rhsSelection, rhsTheme, rhsStrings): + if lhsIndex != rhsIndex { + return false + } + if lhsContact.id != rhsContact.id { + return false + } + if lhsCount != rhsCount { + return false + } + if lhsSelection != rhsSelection { + return false + } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + return true + default: + return false + } + } + } + + static func <(lhs: InviteContactsEntry, rhs: InviteContactsEntry) -> Bool { + switch lhs { + case .search: + return true + case let .option(lhsIndex, _, _, _): + switch rhs { + case .search: + return false + case let .option(rhsIndex, _, _, _): + return lhsIndex < rhsIndex + case .peer: + return true + } + case let .peer(lhsIndex, _, _, _, _, _): + switch rhs { + case .search, .option: + return false + case let .peer(rhsIndex, _, _, _, _, _): + return lhsIndex < rhsIndex + } + } + } +} + +struct InviteContactsGroupSelectionState: Equatable { + let selectedContactIndices: [String: Int] + let nextSelectionIndex: Int + + private init(selectedContactIndices: [String: Int], nextSelectionIndex: Int) { + self.selectedContactIndices = selectedContactIndices + self.nextSelectionIndex = nextSelectionIndex + } + + init() { + self.selectedContactIndices = [:] + self.nextSelectionIndex = 0 + } + + func withReplacedSelectedContactIds(_ contactIds: [String]) -> InviteContactsGroupSelectionState { + var selectedContactIndices: [String: Int] = [:] + var nextSelectionIndex: Int = self.nextSelectionIndex + for contactId in contactIds { + selectedContactIndices[contactId] = nextSelectionIndex + nextSelectionIndex += 1 + } + return InviteContactsGroupSelectionState(selectedContactIndices: selectedContactIndices, nextSelectionIndex: nextSelectionIndex) + } + + func withToggledContactId(_ contactId: String) -> InviteContactsGroupSelectionState { + var updatedIndices = self.selectedContactIndices + if let _ = updatedIndices[contactId] { + updatedIndices.removeValue(forKey: contactId) + return InviteContactsGroupSelectionState(selectedContactIndices: updatedIndices, nextSelectionIndex: self.nextSelectionIndex) + } else { + updatedIndices[contactId] = self.nextSelectionIndex + return InviteContactsGroupSelectionState(selectedContactIndices: updatedIndices, nextSelectionIndex: self.nextSelectionIndex + 1) + } + } + + func withClearedSelection() -> InviteContactsGroupSelectionState { + return InviteContactsGroupSelectionState(selectedContactIndices: [:], nextSelectionIndex: self.nextSelectionIndex) + } + + static func ==(lhs: InviteContactsGroupSelectionState, rhs: InviteContactsGroupSelectionState) -> Bool { + return lhs.selectedContactIndices == rhs.selectedContactIndices && lhs.nextSelectionIndex == rhs.nextSelectionIndex + } +} + +private func inviteContactsEntries(accountPeer: Peer?, sortedContacts: [(DeviceContact, Int32)], selectionState: InviteContactsGroupSelectionState, theme: PresentationTheme, strings: PresentationStrings, interaction: InviteContactsInteraction) -> [InviteContactsEntry] { + var entries: [InviteContactsEntry] = [] + + entries.append(.search(theme, strings)) + + entries.append(.option(0, ContactListAdditionalOption(title: strings.Contacts_ShareTelegram, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/InviteActionIcon"), color: theme.list.itemAccentColor), action: { + interaction.shareTelegram() + }), theme, strings)) + + var index = 0 + for (contact, count) in sortedContacts { + entries.append(.peer(index, contact, count, .selectable(selected: selectionState.selectedContactIndices[contact.id] != nil), theme, strings)) + index += 1 + } + + return entries +} + +private func preparedInviteContactsTransition(account: Account, from fromEntries: [InviteContactsEntry], to toEntries: [InviteContactsEntry], sortedContats: [(DeviceContact, Int32)], interaction: InviteContactsInteraction, firstTime: Bool, animated: Bool) -> InviteContactsTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction), directionHint: nil) } + + return InviteContactsTransition(deletions: deletions, insertions: insertions, updates: updates, sortedContats: sortedContats, firstTime: firstTime, animated: animated) +} + +private struct InviteContactsTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let sortedContats: [(DeviceContact, Int32)] + let firstTime: Bool + let animated: Bool +} + +final class InviteContactsControllerNode: ASDisplayNode { + let listNode: ListView + + private let account: Account + private var searchDisplayController: SearchDisplayController? + + private var validLayout: (ContainerViewLayout, CGFloat)? + + var navigationBar: NavigationBar? + + private let countPanelNode: InviteContactsCountPanelNode + + var requestActivateSearch: (() -> Void)? + var requestDeactivateSearch: (() -> Void)? + var requestShareTelegram: (() -> Void)? + var requestShare: (([(DeviceContact, Int32)]) -> Void)? + + let currentSortedContacts = Atomic<[(DeviceContact, Int32)]>(value: []) + + var selectionState = InviteContactsGroupSelectionState() { + didSet { + if self.selectionState != oldValue { + self.selectionStatePromise.set(.single(self.selectionState)) + self.countPanelNode.badge = "\(self.selectionState.selectedContactIndices.count)" + if oldValue.selectedContactIndices.isEmpty != self.selectionState.selectedContactIndices.isEmpty { + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) + } + } + } + } + } + private let selectionStatePromise = Promise(InviteContactsGroupSelectionState()) + + private var queuedTransitions: [InviteContactsTransition] = [] + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + + private let _ready = Promise() + private var readyValue = false { + didSet { + if self.readyValue, self.readyValue != oldValue { + self._ready.set(.single(self.readyValue)) + } + } + } + var ready: Signal { + return self._ready.get() + } + + private var disposable: Disposable? + + private let currentContactIds = Atomic<[String]>(value: []) + + init(account: Account) { + self.account = account + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) + + self.listNode = ListView() + + var shareImpl: (() -> Void)? + self.countPanelNode = InviteContactsCountPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { + shareImpl?() + }) + + super.init() + + self.setViewBlock({ + return UITracingLayerView() + }) + + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor + + self.addSubnode(self.listNode) + self.addSubnode(self.countPanelNode) + + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() + } + } + }) + + let account = self.account + var firstTime: Int32 = 1 + let selectionStateSignal = self.selectionStatePromise.get() + let transition: Signal + let themeAndStringsPromise = self.themeAndStringsPromise + let previousEntries = Atomic<[InviteContactsEntry]?>(value: nil) + + let interaction = InviteContactsInteraction(activateSearch: { [weak self] in + self?.requestActivateSearch?() + }, toggleContact: { [weak self] id in + if let strongSelf = self { + strongSelf.selectionState = strongSelf.selectionState.withToggledContactId(id) + } + }, shareTelegram: { [weak self] in + self?.requestShareTelegram?() + }) + + let existingNumbers: Signal, NoError> = account.postbox.contactPeersView(accountPeerId: nil, includePresences: false) + |> map { view -> Set in + var existingNumbers = Set() + for peer in view.peers { + if let peer = peer as? TelegramUser, let phone = peer.phone { + existingNumbers.insert(formatPhoneNumber(phone)) + } + } + return existingNumbers + } + + let currentSortedContacts = self.currentSortedContacts + let sortedContacts: Signal<[(DeviceContact, Int32)], NoError> = combineLatest(existingNumbers, account.telegramApplicationContext.contactsManager.contacts) + |> mapToSignal { existingNumbers, contacts -> Signal<[(DeviceContact, Int32)], NoError> in + return deviceContactsImportedByCount(postbox: account.postbox, contacts: contacts) + |> map { counts -> [(DeviceContact, Int32)] in + var result: [(DeviceContact, Int32)] = [] + var contactValues: [String: DeviceContact] = [:] + for contact in contacts { + var found = false + for number in contact.phoneNumbers { + if existingNumbers.contains(number.number.normalized.rawValue) { + found = true + } + } + if !found { + contactValues[contact.id] = contact + } + } + var countValues: [(String, Int32)] = [] + for (id, count) in counts { + countValues.append((id, count)) + } + countValues.sort(by: { $0.1 > $1.1 }) + var existing = Set() + for (id, value) in countValues { + existing.insert(id) + if let contact = contactValues[id] { + result.append((contact, value)) + } + } + for contact in contacts { + if !existing.contains(contact.id) { + result.append((contact, 0)) + } + } + + return result + } + } + |> beforeNext { sortedContacts in + let _ = currentSortedContacts.swap(sortedContacts) + } + let processingQueue = Queue() + transition = (combineLatest(sortedContacts, selectionStateSignal, themeAndStringsPromise.get()) + |> mapToQueue { sortedContacts, selectionState, themeAndStrings -> Signal in + let signal = deferred { () -> Signal in + let entries = inviteContactsEntries(accountPeer: nil, sortedContacts: sortedContacts, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, interaction: interaction) + let previous = previousEntries.swap(entries) + let animated: Bool + if let previous = previous { + animated = (entries.count - previous.count) < 20 + } else { + animated = false + } + return .single(preparedInviteContactsTransition(account: account, from: previous ?? [], to: entries, sortedContats: sortedContacts, interaction: interaction, firstTime: previous == nil, animated: animated)) + } + + if OSAtomicCompareAndSwap32(1, 0, &firstTime) { + return signal |> runOn(Queue.mainQueue()) + } else { + return signal |> runOn(processingQueue) + } + }) + |> deliverOnMainQueue + + self.disposable = transition.start(next: { [weak self] transition in + self?.enqueueTransition(transition) + }) + + shareImpl = { [weak self] in + if let strongSelf = self { + var result: [(DeviceContact, Int32)] = [] + for contact in (strongSelf.currentSortedContacts.with { $0 }) { + if strongSelf.selectionState.selectedContactIndices[contact.0.id] != nil { + result.append(contact) + } + } + if !result.isEmpty { + self?.requestShare?(result) + } + } + } + } + + deinit { + self.disposable?.dispose() + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor + + self.searchDisplayController?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let hadValidLayout = self.validLayout != nil + self.validLayout = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.input]) + insets.top += navigationBarHeight + + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + if !searchDisplayController.isDeactivating { + insets.top += 20.0 + } + } + + insets.left += layout.safeInsets.left + insets.right += layout.safeInsets.right + + let countPanelHeight = self.countPanelNode.updateLayout(width: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, transition: transition) + if self.selectionState.selectedContactIndices.isEmpty { + transition.updateFrame(node: self.countPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: countPanelHeight))) + } else { + insets.bottom += countPanelHeight + transition.updateFrame(node: self.countPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - countPanelHeight), size: CGSize(width: layout.size.width, height: countPanelHeight))) + } + + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default + } + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !hadValidLayout { + self.dequeueTransitions() + } + } + + func activateSearch() { + guard let (containerLayout, navigationBarHeight) = self.validLayout, let navigationBar = self.navigationBar else { + return + } + + var maybePlaceholderNode: SearchBarPlaceholderNode? + self.listNode.forEachItemNode { node in + if let node = node as? ChatListSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + if let _ = self.searchDisplayController { + return + } + + if let placeholderNode = maybePlaceholderNode { + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: false, openPeer: { [weak self] peerId in + }), cancel: { [weak self] in + if let requestDeactivateSearch = self?.requestDeactivateSearch { + requestDeactivateSearch() + } + }) + + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { subnode in + self.insertSubnode(subnode, belowSubnode: navigationBar) + }, placeholder: placeholderNode) + } + } + + func deactivateSearch() { + if let searchDisplayController = self.searchDisplayController { + self.searchDisplayController = nil + var maybePlaceholderNode: SearchBarPlaceholderNode? + self.listNode.forEachItemNode { node in + if let node = node as? ChatListSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + searchDisplayController.deactivate(placeholder: maybePlaceholderNode) + } + } + + private func enqueueTransition(_ transition: InviteContactsTransition) { + self.queuedTransitions.append(transition) + + if self.validLayout != nil { + self.dequeueTransitions() + } + } + + private func dequeueTransitions() { + if self.validLayout != nil { + while !self.queuedTransitions.isEmpty { + let transition = self.queuedTransitions.removeFirst() + + var options = ListViewDeleteAndInsertOptions() + if transition.firstTime { + options.insert(.Synchronous) + options.insert(.LowLatency) + } else if transition.animated { + options.insert(.AnimateInsertion) + } + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.readyValue = true + } + }) + } + } + } + + func selectAll() { + let ids = self.currentSortedContacts.with { $0 }.map { $0.0.id } + var allSelected = true + for id in ids { + if self.selectionState.selectedContactIndices[id] == nil { + allSelected = false + break + } + } + self.selectionState = self.selectionState.withReplacedSelectedContactIds(allSelected ? [] : ids) + } +} + diff --git a/TelegramUI/InviteContactsCountPanelNode.swift b/TelegramUI/InviteContactsCountPanelNode.swift new file mode 100644 index 0000000000..0f901ff3b3 --- /dev/null +++ b/TelegramUI/InviteContactsCountPanelNode.swift @@ -0,0 +1,103 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class InviteContactsCountPanelNode: ASDisplayNode { + private let theme: PresentationTheme + private let action: () -> Void + + private let separatorNode: ASDisplayNode + private let labelNode: ImmediateTextNode + private let badgeLabel: ImmediateTextNode + private let badgeBackground: ASImageNode + private let buttonNode: HighlightableButtonNode + + private var validLayout: (CGFloat, CGFloat)? + + var badge: String? { + didSet { + if self.badge != oldValue { + if let badge = self.badge { + self.badgeLabel.attributedText = NSAttributedString(string: badge, font: Font.regular(14.0), textColor: self.theme.rootController.navigationBar.badgeTextColor, paragraphAlignment: .center) + } + + if let (width, bottomInset) = self.validLayout { + let _ = self.updateLayout(width: width, bottomInset: bottomInset, transition: .immediate) + } + } + } + } + + init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) { + self.theme = theme + self.action = action + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor + + self.labelNode = ImmediateTextNode() + self.badgeLabel = ImmediateTextNode() + + self.badgeBackground = ASImageNode() + self.badgeBackground.isLayerBacked = true + self.badgeBackground.displaysAsynchronously = false + self.badgeBackground.displayWithoutProcessing = true + + self.badgeBackground.image = generateStretchableFilledCircleImage(diameter: 22.0, color: theme.rootController.navigationBar.accentTextColor) + + self.buttonNode = HighlightableButtonNode() + + super.init() + + self.backgroundColor = theme.rootController.navigationBar.backgroundColor + + self.addSubnode(self.labelNode) + self.labelNode.attributedText = NSAttributedString(string: strings.Contacts_InviteToTelegram, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + + self.addSubnode(self.badgeBackground) + self.addSubnode(self.badgeLabel) + self.addSubnode(self.separatorNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.labelNode.layer.removeAnimation(forKey: "opacity") + strongSelf.labelNode.alpha = 0.4 + } else { + strongSelf.labelNode.alpha = 1.0 + strongSelf.labelNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + func updateLayout(width: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = (width, bottomInset) + + let panelHeight: CGFloat = bottomInset + 44.0 + + let titleSize = self.labelNode.updateLayout(CGSize(width: width, height: 100.0)) + let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: floor((44.0 - titleSize.height) / 2.0)), size: titleSize) + transition.updateFrame(node: self.labelNode, frame: titleFrame) + + let badgeSize = self.badgeLabel.updateLayout(CGSize(width: 100.0, height: 100.0)) + + let backgroundSize = CGSize(width: max(22.0, badgeSize.width + 10.0 + 1.0), height: 22.0) + let backgroundFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 6.0, y: 11.0), size: backgroundSize) + + self.badgeBackground.frame = backgroundFrame + self.badgeLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: backgroundFrame.minY + 3.0), size: badgeSize) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + + self.buttonNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 44.0)) + + return panelHeight + } + + @objc func buttonPressed() { + self.action() + } +} diff --git a/TelegramUI/ItemListEditableDeleteControlNode.swift b/TelegramUI/ItemListEditableDeleteControlNode.swift index 67f947bf95..0626fae147 100644 --- a/TelegramUI/ItemListEditableDeleteControlNode.swift +++ b/TelegramUI/ItemListEditableDeleteControlNode.swift @@ -30,8 +30,8 @@ final class ItemListEditableControlNode: ASDisplayNode { resultNode = node } else { resultNode = ItemListEditableControlNode() - resultNode.iconNode.image = image } + resultNode.iconNode.image = image return (CGSize(width: 38.0, height: height), { if let image = image { diff --git a/TelegramUI/ItemListTextWithLabelItem.swift b/TelegramUI/ItemListTextWithLabelItem.swift index 594b1c2a1e..8b58413bca 100644 --- a/TelegramUI/ItemListTextWithLabelItem.swift +++ b/TelegramUI/ItemListTextWithLabelItem.swift @@ -3,10 +3,16 @@ import Display import AsyncDisplayKit import SwiftSignalKit +enum ItemListTextWithLabelItemTextColor { + case primary + case accent +} + final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { let theme: PresentationTheme let label: String let text: String + let textColor: ItemListTextWithLabelItemTextColor let enabledEntitiyTypes: EnabledEntityTypes let multiline: Bool let sectionId: ItemListSectionId @@ -16,10 +22,11 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { let tag: Any? - init(theme: PresentationTheme, label: String, text: String, enabledEntitiyTypes: EnabledEntityTypes, multiline: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { + init(theme: PresentationTheme, label: String, text: String, textColor: ItemListTextWithLabelItemTextColor = .primary, enabledEntitiyTypes: EnabledEntityTypes, multiline: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { self.theme = theme self.label = label self.text = text + self.textColor = textColor self.enabledEntitiyTypes = enabledEntitiyTypes self.multiline = multiline self.sectionId = sectionId @@ -159,7 +166,14 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntitiyTypes) - let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: item.theme.list.itemPrimaryTextColor, linkColor: item.theme.list.itemAccentColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, fixedFont: textFixedFont) + let baseColor: UIColor + switch item.textColor { + case .primary: + baseColor = item.theme.list.itemPrimaryTextColor + case .accent: + baseColor = item.theme.list.itemAccentColor + } + let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: baseColor, linkColor: item.theme.list.itemAccentColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, fixedFont: textFixedFont) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize = CGSize(width: params.width, height: textLayout.size.height + 39.0) diff --git a/TelegramUI/LegacyInstantVideoController.swift b/TelegramUI/LegacyInstantVideoController.swift index 6b5a056ef6..b4b77d9f13 100644 --- a/TelegramUI/LegacyInstantVideoController.swift +++ b/TelegramUI/LegacyInstantVideoController.swift @@ -97,7 +97,7 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, return nil }, parentController: baseController, controlsFrame: panelFrame, isAlreadyLocked: { return false - }, liveUploadInterface: nil, pallete: TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor))! + }, liveUploadInterface: LegacyLiveUploadInterface(account: account), pallete: TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor))! controller.finishedWithVideo = { videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments in guard let videoUrl = videoUrl else { return @@ -134,7 +134,13 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, return } - let resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: videoUrl.path, adjustments: resourceAdjustments) + let resource: TelegramMediaResource + if let liveUploadData = liveUploadData as? LegacyLiveUploadInterfaceResult, resourceAdjustments == nil, let data = try? Data(contentsOf: videoUrl) { + resource = LocalFileMediaResource(fileId: liveUploadData.id) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + } else { + 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])]) let attributes: [MessageAttribute] = [] diff --git a/TelegramUI/LegacyLiveUploadInterface.swift b/TelegramUI/LegacyLiveUploadInterface.swift new file mode 100644 index 0000000000..0f6f0c83f5 --- /dev/null +++ b/TelegramUI/LegacyLiveUploadInterface.swift @@ -0,0 +1,67 @@ +import Foundation +import Postbox +import TelegramCore +import LegacyComponents +import SwiftSignalKit + +final class LegacyLiveUploadInterfaceResult: NSObject { + let id: Int64 + + init(id: Int64) { + self.id = id + + super.init() + } +} + +final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUploadInterface { + private let account: Account + private let id: Int64 + private var path: String? + private var size: Int? + + private let data = Promise() + private var dataValue: MediaResourceData? + + init(account: Account) { + self.account = account + self.id = arc4random64() + + var updateImpl: ((String, Int) -> Void)? + super.init(update: { path, size in + updateImpl?(path, size) + }) + + updateImpl = { [weak self] path, size in + if let strongSelf = self { + if strongSelf.path == nil { + strongSelf.path = path + strongSelf.account.messageMediaPreuploadManager.add(network: strongSelf.account.network, postbox: strongSelf.account.postbox, id: strongSelf.id, encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .video), source: strongSelf.data.get()) + } + strongSelf.size = size + + var complete = false + if let dataValue = strongSelf.dataValue, dataValue.complete { + complete = true + } + let dataValue = MediaResourceData(path: path, offset: 0, size: size, complete: complete) + strongSelf.dataValue = dataValue + strongSelf.data.set(.single(dataValue)) + } + } + } + + deinit { + } + + override func fileUpdated(_ completed: Bool) -> Any! { + let _ = super.fileUpdated(completed) + if completed, let dataValue = self.dataValue { + self.dataValue = MediaResourceData(path: dataValue.path, offset: dataValue.offset, size: dataValue.size, complete: true) + self.data.set(.single(dataValue)) + return LegacyLiveUploadInterfaceResult(id: self.id) + } else { + return nil + } + } +} diff --git a/TelegramUI/MediaInputSettings.swift b/TelegramUI/MediaInputSettings.swift new file mode 100644 index 0000000000..5f8ae2324f --- /dev/null +++ b/TelegramUI/MediaInputSettings.swift @@ -0,0 +1,53 @@ +import Foundation +import Postbox +import SwiftSignalKit + +public struct MediaInputSettings: PreferencesEntry, Equatable { + public let enableRaiseToSpeak: Bool + + public static var defaultSettings: MediaInputSettings { + return MediaInputSettings(enableRaiseToSpeak: true) + } + + public init(enableRaiseToSpeak: Bool) { + self.enableRaiseToSpeak = enableRaiseToSpeak + } + + public init(decoder: PostboxDecoder) { + self.enableRaiseToSpeak = decoder.decodeInt32ForKey("enableRaiseToSpeak", orElse: 1) != 0 + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.enableRaiseToSpeak ? 1 : 0, forKey: "enableRaiseToSpeak") + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? MediaInputSettings { + return self == to + } else { + return false + } + } + + public static func ==(lhs: MediaInputSettings, rhs: MediaInputSettings) -> Bool { + return lhs.enableRaiseToSpeak == rhs.enableRaiseToSpeak + } + + func withUpdatedEnableRaiseToSpeak(_ enableRaiseToSpeak: Bool) -> MediaInputSettings { + return MediaInputSettings(enableRaiseToSpeak: enableRaiseToSpeak) + } +} + +func updateMediaInputSettingsInteractively(postbox: Postbox, _ f: @escaping (MediaInputSettings) -> MediaInputSettings) -> Signal { + return postbox.modify { modifier -> Void in + modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.mediaInputSettings, { entry in + let currentSettings: MediaInputSettings + if let entry = entry as? MediaInputSettings { + currentSettings = entry + } else { + currentSettings = MediaInputSettings.defaultSettings + } + return f(currentSettings) + }) + } +} diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index b39d8fc8cf..fb8c26f5dd 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -65,7 +65,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { } } return false - }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in + }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none) let listNode = ChatHistoryListNode(account: account, chatLocation: .peer(updatedPlaylistPeerId), tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false)) diff --git a/TelegramUI/NativeVideoContent.swift b/TelegramUI/NativeVideoContent.swift index ac5a384027..70c1c83518 100644 --- a/TelegramUI/NativeVideoContent.swift +++ b/TelegramUI/NativeVideoContent.swift @@ -42,19 +42,21 @@ final class NativeVideoContent: UniversalVideoContent { let dimensions: CGSize let duration: Int32 let streamVideo: Bool + let loopVideo: Bool let enableSound: Bool - init(id: NativeVideoContentId, file: TelegramMediaFile, streamVideo: Bool = false, enableSound: Bool = true) { + init(id: NativeVideoContentId, file: TelegramMediaFile, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true) { self.id = id self.file = file self.dimensions = file.dimensions ?? CGSize(width: 128.0, height: 128.0) self.duration = file.duration ?? 0 self.streamVideo = streamVideo + self.loopVideo = loopVideo self.enableSound = enableSound } func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, file: self.file, streamVideo: self.streamVideo, enableSound: self.enableSound) + return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, file: self.file, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound) } } @@ -89,7 +91,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private var validLayout: CGSize? - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, file: TelegramMediaFile, streamVideo: Bool, enableSound: Bool) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, file: TelegramMediaFile, streamVideo: Bool, loopVideo: Bool, enableSound: Bool) { self.postbox = postbox self.file = file @@ -97,7 +99,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, resource: file.resource, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound) var actionAtEndImpl: (() -> Void)? - if enableSound { + if enableSound && !loopVideo { self.player.actionAtEnd = .action({ actionAtEndImpl?() }) diff --git a/TelegramUI/OpenChatMessage.swift b/TelegramUI/OpenChatMessage.swift index b67ae9e883..ddf4120b1e 100644 --- a/TelegramUI/OpenChatMessage.swift +++ b/TelegramUI/OpenChatMessage.swift @@ -14,6 +14,7 @@ private enum ChatMessageGalleryControllerData { case stickerPack(StickerPackReference) case audio(TelegramMediaFile) case gallery(GalleryController) + case secretGallery(SecretMediaPreviewController) case other(Media) } @@ -86,10 +87,15 @@ private func chatMessageGalleryControllerData(account: Account, message: Message } else if let file = galleryMedia as? TelegramMediaFile, file.mimeType == "application/vnd.apple.pkpass" || (file.fileName != nil && file.fileName!.lowercased().hasSuffix(".pkpass")) { return .pass(file) } else { - let gallery = GalleryController(account: account, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in - navigationController?.replaceTopController(controller, animated: false, ready: ready) - }, baseNavigationController: navigationController) - return .gallery(gallery) + if message.containsSecretMedia { + let gallery = SecretMediaPreviewController(account: account, messageId: message.id) + return .secretGallery(gallery) + } else { + let gallery = GalleryController(account: account, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in + navigationController?.replaceTopController(controller, animated: false, ready: ready) + }, baseNavigationController: navigationController) + return .gallery(gallery) + } } } if let otherMedia = otherMedia { @@ -200,6 +206,16 @@ func openChatMessage(account: Account, message: Message, standalone: Bool, rever return nil })) return true + case let .secretGallery(gallery): + dismissInput() + present(gallery, GalleryControllerPresentationArguments(transitionArguments: { messageId, media in + let selectedTransitionNode = transitionNode(messageId, media) + if let selectedTransitionNode = selectedTransitionNode { + return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: addToTransitionSurface) + } + return nil + })) + return true case let .other(otherMedia): if let contact = otherMedia as? TelegramMediaContact { let _ = (account.postbox.modify { modifier -> (Peer?, Bool?) in diff --git a/TelegramUI/OpenUrl.swift b/TelegramUI/OpenUrl.swift index 0c935ca753..a412ddfecd 100644 --- a/TelegramUI/OpenUrl.swift +++ b/TelegramUI/OpenUrl.swift @@ -2,6 +2,7 @@ import Foundation import Display import SafariServices import TelegramCore +import Postbox import SwiftSignalKit public func openExternalUrl(account: Account, url: String, presentationData: PresentationData, applicationContext: TelegramApplicationContext, navigationController: NavigationController?) { @@ -92,6 +93,22 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre convertedUrl = result } } + } else if parsedUrl.host == "localpeer" { + if let components = URLComponents(string: "/?" + query) { + var peerId: PeerId? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "id", let intValue = Int64(value) { + peerId = PeerId(intValue) + } + } + } + } + if let peerId = peerId, let navigationController = navigationController { + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) + } + } } else if parsedUrl.host == "join" { if let components = URLComponents(string: "/?" + query) { var invite: String? diff --git a/TelegramUI/OverlayPlayerControllerNode.swift b/TelegramUI/OverlayPlayerControllerNode.swift index 3c83208991..6757961c99 100644 --- a/TelegramUI/OverlayPlayerControllerNode.swift +++ b/TelegramUI/OverlayPlayerControllerNode.swift @@ -54,7 +54,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec } else { return false } - }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in + }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index c659660b5e..310b2fbf33 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -94,7 +94,7 @@ public class PeerMediaCollectionController: TelegramController { }) } return false - }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { [weak self] id, navigation, _ in + }, openPeer: { [weak self] id, navigation, _ in if let strongSelf = self { if let id = id { (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(id), messageId: nil)) diff --git a/TelegramUI/PhoneInputNode.swift b/TelegramUI/PhoneInputNode.swift index 3386c2c26b..b61437da94 100644 --- a/TelegramUI/PhoneInputNode.swift +++ b/TelegramUI/PhoneInputNode.swift @@ -6,7 +6,7 @@ import TelegramCore private func removeDuplicatedPlus(_ text: String?) -> String { var result = "" if let text = text { - for c in text.characters { + for c in text { if c == "+" { if result.isEmpty { result += String(c) @@ -22,7 +22,7 @@ private func removeDuplicatedPlus(_ text: String?) -> String { private func removePlus(_ text: String?) -> String { var result = "" if let text = text { - for c in text.characters { + for c in text { if c != "+" { result += String(c) } @@ -34,7 +34,7 @@ private func removePlus(_ text: String?) -> String { private func cleanPhoneNumber(_ text: String?) -> String { var cleanNumber = "" if let text = text { - for c in text.characters { + for c in text { if c == "+" { if cleanNumber.isEmpty { cleanNumber += String(c) @@ -50,7 +50,7 @@ private func cleanPhoneNumber(_ text: String?) -> String { private func cleanPrefix(_ text: String) -> String { var result = "" var checked = false - for c in text.characters { + for c in text { if c != " " { checked = true } @@ -64,7 +64,7 @@ private func cleanPrefix(_ text: String) -> String { private func cleanSuffix(_ text: String) -> String { var result = "" var checked = false - for c in text.characters.reversed() { + for c in text.reversed() { if c != " " { checked = true } @@ -162,7 +162,7 @@ final class PhoneInputNode: ASDisplayNode, UITextFieldDelegate { if !realRegionPrefix.hasPrefix("+") { realRegionPrefix = "+" + realRegionPrefix } - numberText = cleanPrefix(text.substring(from: realRegionPrefix.endIndex)) + numberText = cleanPrefix(String(text[realRegionPrefix.endIndex...])) } else { realRegionPrefix = text if !realRegionPrefix.hasPrefix("+") { diff --git a/TelegramUI/PreferencesKeys.swift b/TelegramUI/PreferencesKeys.swift index 8f8ec6d5ea..2d1a081369 100644 --- a/TelegramUI/PreferencesKeys.swift +++ b/TelegramUI/PreferencesKeys.swift @@ -13,6 +13,7 @@ private enum ApplicationSpecificPreferencesKeyValues: Int32 { case callListSettings = 7 case experimentalSettings = 8 case musicPlaybackSettings = 9 + case mediaInputSettings = 10 } public struct ApplicationSpecificPreferencesKeys { @@ -26,4 +27,5 @@ public struct ApplicationSpecificPreferencesKeys { public static let callListSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.callListSettings.rawValue) public static let experimentalSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.experimentalSettings.rawValue) public static let musicPlaybackSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.musicPlaybackSettings.rawValue) + public static let mediaInputSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.mediaInputSettings.rawValue) } diff --git a/TelegramUI/PresentationData.swift b/TelegramUI/PresentationData.swift index 04e1403a02..72e006b8c6 100644 --- a/TelegramUI/PresentationData.swift +++ b/TelegramUI/PresentationData.swift @@ -71,8 +71,8 @@ private func currentTimeFormat() -> PresentationTimeFormat { } } -public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(PresentationData, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings), NoError> { - return postbox.modify { modifier -> (PresentationThemeSettings, LocalizationSettings?, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings) in +public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(PresentationData, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings, MediaInputSettings), NoError> { + return postbox.modify { modifier -> (PresentationThemeSettings, LocalizationSettings?, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings, MediaInputSettings) in let themeSettings: PresentationThemeSettings if let current = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings) as? PresentationThemeSettings { themeSettings = current @@ -115,8 +115,15 @@ public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(Pres inAppNotificationSettings = InAppNotificationSettings.defaultSettings } - return (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings) - } |> map { (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings) -> (PresentationData, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings) in + let mediaInputSettings: MediaInputSettings + if let value = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.mediaInputSettings) as? MediaInputSettings { + mediaInputSettings = value + } else { + mediaInputSettings = MediaInputSettings.defaultSettings + } + + return (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings, mediaInputSettings) + } |> map { (themeSettings, localizationSettings, automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings, mediaInputSettings) -> (PresentationData, AutomaticMediaDownloadSettings, LoggingSettings, CallListSettings, InAppNotificationSettings, MediaInputSettings) in let themeValue: PresentationTheme switch themeSettings.theme { case let .builtin(reference): @@ -138,7 +145,7 @@ public func currentPresentationDataAndSettings(postbox: Postbox) -> Signal<(Pres stringsValue = defaultPresentationStrings } let timeFormat: PresentationTimeFormat = currentTimeFormat() - return (PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: themeSettings.chatWallpaper, fontSize: themeSettings.fontSize, timeFormat: timeFormat), automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings) + return (PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: themeSettings.chatWallpaper, fontSize: themeSettings.fontSize, timeFormat: timeFormat), automaticMediaDownloadSettings, loggingSettings, callListSettings, inAppNotificationSettings, mediaInputSettings) } } diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index 3f5936e809..163ed93289 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -17,6 +17,7 @@ enum PresentationResourceKey: Int32 { case navigationCallIcon case navigationShareIcon case navigationSearchIcon + case navigationAddIcon case navigationPlayerCloseButton case navigationLiveLocationIcon @@ -38,6 +39,7 @@ enum PresentationResourceKey: Int32 { case itemListDeleteIndicatorIcon case itemListReorderIndicatorIcon case itemListAddPersonIcon + case itemListAddPhoneIcon case itemListStickerItemUnreadDot case itemListVerifiedPeerIcon diff --git a/TelegramUI/PresentationResourcesItemList.swift b/TelegramUI/PresentationResourcesItemList.swift index 8bc8924ffc..dad68a454a 100644 --- a/TelegramUI/PresentationResourcesItemList.swift +++ b/TelegramUI/PresentationResourcesItemList.swift @@ -102,4 +102,19 @@ struct PresentationResourcesItemList { return generateTintedImage(image: UIImage(bundleImageName: "Contact List/AddMemberIcon"), color: theme.list.itemAccentColor) }) } + + static func addPhoneIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListAddPhoneIcon.rawValue, { theme in + generateImage(CGSize(width: 22.0, height: 26.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor(white: 0.0, alpha: 0.06).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 22.0, height: 22.0))) + context.setFillColor(theme.list.itemDisclosureActions.constructive.fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: CGSize(width: 22.0, height: 22.0))) + context.setFillColor(theme.list.itemDisclosureActions.constructive.foregroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - 11.0) / 2.0), y: 2.0 + floorToScreenPixels((size.width - 1.0) / 2.0)), size: CGSize(width: 11.0, height: 1.0))) + context.fill(CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - 1.0) / 2.0), y: 2.0 + floorToScreenPixels((size.width - 11.0) / 2.0)), size: CGSize(width: 1.0, height: 11.0))) + }) + }) + } } diff --git a/TelegramUI/PresentationResourcesRootController.swift b/TelegramUI/PresentationResourcesRootController.swift index 165c149234..4ceba9edaf 100644 --- a/TelegramUI/PresentationResourcesRootController.swift +++ b/TelegramUI/PresentationResourcesRootController.swift @@ -82,6 +82,12 @@ struct PresentationResourcesRootController { }) } + static func navigationAddIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationAddIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddIcon"), color: theme.rootController.navigationBar.accentTextColor) + }) + } + static func navigationPlayerCloseButton(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationPlayerCloseButton.rawValue, { theme in return generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index a08248b416..fcdcf6b33e 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -232,11 +232,13 @@ public final class PresentationThemeItemDisclosureActions { public let neutral1: PresentationThemeItemDisclosureAction public let neutral2: PresentationThemeItemDisclosureAction public let destructive: PresentationThemeItemDisclosureAction + public let constructive: PresentationThemeItemDisclosureAction - public init(neutral1: PresentationThemeItemDisclosureAction, neutral2: PresentationThemeItemDisclosureAction, destructive: PresentationThemeItemDisclosureAction) { + public init(neutral1: PresentationThemeItemDisclosureAction, neutral2: PresentationThemeItemDisclosureAction, destructive: PresentationThemeItemDisclosureAction, constructive: PresentationThemeItemDisclosureAction) { self.neutral1 = neutral1 self.neutral2 = neutral2 self.destructive = destructive + self.constructive = constructive } } diff --git a/TelegramUI/RadialStatusNode.swift b/TelegramUI/RadialStatusNode.swift index 24f0286a16..fc389b14b4 100644 --- a/TelegramUI/RadialStatusNode.swift +++ b/TelegramUI/RadialStatusNode.swift @@ -9,6 +9,7 @@ enum RadialStatusNodeState: Equatable { case progress(color: UIColor, value: CGFloat?, cancelEnabled: Bool) case check(UIColor) case customIcon(UIImage) + case secretTimeout(color: UIColor, icon: UIImage?, beginTime: Double, timeout: Double) static func ==(lhs: RadialStatusNodeState, rhs: RadialStatusNodeState) -> Bool { switch lhs { @@ -54,6 +55,12 @@ enum RadialStatusNodeState: Equatable { } else { return false } + case let .secretTimeout(lhsColor, lhsIcon, lhsBeginTime, lhsTimeout): + if case let .secretTimeout(rhsColor, rhsIcon, rhsBeginTime, rhsTimeout) = rhs, lhsColor.isEqual(rhsColor), lhsIcon === rhsIcon, lhsBeginTime.isEqual(to: rhsBeginTime), lhsTimeout.isEqual(to: rhsTimeout) { + return true + } else { + return false + } } } @@ -92,6 +99,8 @@ enum RadialStatusNodeState: Equatable { node.progress = value return node } + case let .secretTimeout(color, icon, beginTime, timeout): + return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon) } } } @@ -99,7 +108,7 @@ enum RadialStatusNodeState: Equatable { final class RadialStatusNode: ASControlNode { private var backgroundNodeColor: UIColor - private var state: RadialStatusNodeState = .none + private(set) var state: RadialStatusNodeState = .none private var backgroundNode: RadialStatusBackgroundNode? private var contentNode: RadialStatusContentNode? diff --git a/TelegramUI/RadialStatusSecretTimeoutContentNode.swift b/TelegramUI/RadialStatusSecretTimeoutContentNode.swift new file mode 100644 index 0000000000..fa6d9dae86 --- /dev/null +++ b/TelegramUI/RadialStatusSecretTimeoutContentNode.swift @@ -0,0 +1,221 @@ +import Foundation +import Display +import AsyncDisplayKit +import LegacyComponents + +private struct ContentParticle { + var position: CGPoint + var direction: CGPoint + var velocity: CGFloat + var alpha: CGFloat + var lifetime: Double + var beginTime: Double + + init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) { + self.position = position + self.direction = direction + self.velocity = velocity + self.alpha = alpha + self.lifetime = lifetime + self.beginTime = beginTime + } +} + +private final class RadialStatusSecretTimeoutContentNodeParameters: NSObject { + let color: UIColor + let icon: UIImage? + let progress: CGFloat + let particles: [ContentParticle] + + init(color: UIColor, icon: UIImage?, progress: CGFloat, particles: [ContentParticle]) { + self.color = color + self.icon = icon + self.progress = progress + self.particles = particles + } +} + +final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { + var color: UIColor { + didSet { + self.setNeedsDisplay() + } + } + + private let beginTime: Double + private let timeout: Double + private let icon: UIImage? + + private var progress: CGFloat = 0.0 + private var particles: [ContentParticle] = [] + + private var displayLink: CADisplayLink? + + init(color: UIColor, beginTime: Double, timeout: Double, icon: UIImage?) { + self.color = color + self.beginTime = beginTime + self.timeout = timeout + self.icon = icon + + super.init() + + self.isOpaque = false + self.isLayerBacked = true + + class DisplayLinkProxy: NSObject { + weak var target: RadialStatusSecretTimeoutContentNode? + init(target: RadialStatusSecretTimeoutContentNode) { + self.target = target + } + + @objc func displayLinkEvent() { + self.target?.displayLinkEvent() + } + } + + self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent)) + self.displayLink?.isPaused = true + self.displayLink?.add(to: RunLoop.main, forMode: RunLoopMode.commonModes) + } + + deinit { + self.displayLink?.invalidate() + } + + override func layout() { + super.layout() + } + + override func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + override func animateIn() { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + override func willEnterHierarchy() { + super.willEnterHierarchy() + self.displayLink?.isPaused = false + } + + override func didExitHierarchy() { + super.didExitHierarchy() + self.displayLink?.isPaused = true + } + + private func displayLinkEvent() { + let bounds = self.bounds + if bounds.width.isZero { + return + } + + let absoluteTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + self.progress = min(1.0, CGFloat((absoluteTimestamp - self.beginTime) / self.timeout)) + + let lineWidth: CGFloat = 1.75 + let center = bounds.center + let radius: CGFloat = (bounds.size.width - lineWidth - 2.5 * 2.0) * 0.5 + + let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * self.progress + + let v = CGPoint(x: sin(endAngle), y: -cos(endAngle)) + let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y) + + let timestamp = CACurrentMediaTime() + + let dt: CGFloat = 1.0 / 60.0 + var removeIndices: [Int] = [] + for i in 0 ..< self.particles.count { + let currentTime = timestamp - self.particles[i].beginTime + if currentTime > self.particles[i].lifetime { + removeIndices.append(i) + } else { + let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime) + let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input)) + self.particles[i].alpha = 1.0 - decelerated + + var p = self.particles[i].position + let d = self.particles[i].direction + let v = self.particles[i].velocity + p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt) + self.particles[i].position = p + } + } + + for i in removeIndices.reversed() { + self.particles.remove(at: i) + } + + let newParticleCount = 1 + for _ in 0 ..< newParticleCount { + let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 70.0 + let angle: CGFloat = degrees * CGFloat.pi / 180.0 + + let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle)) + let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.5 + + let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01) + + let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp) + self.particles.append(particle) + } + + self.setNeedsDisplay() + } + + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + return RadialStatusSecretTimeoutContentNodeParameters(color: self.color, icon: self.icon, progress: self.progress, particles: self.particles) + } + + @objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + if let parameters = parameters as? RadialStatusSecretTimeoutContentNodeParameters { + if let icon = parameters.icon, let iconImage = icon.cgImage { + let imageRect = CGRect(origin: CGPoint(x: floor((bounds.size.width - icon.size.width) / 2.0), y: floor((bounds.size.height - icon.size.height) / 2.0)), size: icon.size) + context.saveGState() + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.draw(iconImage, in: imageRect) + context.restoreGState() + } + + let lineWidth: CGFloat = 1.75 + + context.setFillColor(parameters.color.cgColor) + context.setStrokeColor(parameters.color.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + context.setLineJoin(.miter) + context.setMiterLimit(10.0) + + let center = bounds.center + let radius: CGFloat = (bounds.size.width - lineWidth - 2.5 * 2.0) * 0.5 + + let startAngle: CGFloat = -CGFloat.pi / 2.0 + let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * parameters.progress + + let path = CGMutablePath() + path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) + context.addPath(path) + context.strokePath() + + for particle in parameters.particles { + let size: CGFloat = 1.3 + context.setAlpha(particle.alpha) + context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size))) + } + } + } +} + diff --git a/TelegramUI/ReplyAccessoryPanelNode.swift b/TelegramUI/ReplyAccessoryPanelNode.swift index dce901bf5a..163aac3d51 100644 --- a/TelegramUI/ReplyAccessoryPanelNode.swift +++ b/TelegramUI/ReplyAccessoryPanelNode.swift @@ -97,10 +97,13 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { if let imageDimensions = imageDimensions { let boundingSize = CGSize(width: 35.0, height: 35.0) var radius: CGFloat = 2.0 + var imageSize = imageDimensions.aspectFilled(boundingSize) if isRoundImage { radius = floor(boundingSize.width / 2.0) + imageSize.width += 2.0 + imageSize.height += 2.0 } - applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) } var mediaUpdated = false diff --git a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift index 7d486d506d..3e9724cd4f 100644 --- a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift +++ b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift @@ -43,10 +43,17 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState - if let peer = interfaceState.peer as? TelegramSecretChat { + if let renderedPeer = interfaceState.renderedPeer, let peer = renderedPeer.peer as? TelegramSecretChat, let userPeer = renderedPeer.peers[peer.regularPeerId] { switch peer.embeddedState { case .handshake: - self.button.setAttributedTitle(NSAttributedString(string: interfaceState.strings.Conversation_EncryptionProcessing, font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.primaryTextColor), for: []) + let text: String + switch peer.role { + case .creator: + text = interfaceState.strings.DialogList_AwaitingEncryption(userPeer.compactDisplayTitle).0 + case .participant: + text = interfaceState.strings.Conversation_EncryptionProcessing + } + self.button.setAttributedTitle(NSAttributedString(string: text, font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.primaryTextColor), for: []) case .active, .terminated: break } diff --git a/TelegramUI/SecretMediaPreviewController.swift b/TelegramUI/SecretMediaPreviewController.swift index 2bfa696f1c..e0f378261c 100644 --- a/TelegramUI/SecretMediaPreviewController.swift +++ b/TelegramUI/SecretMediaPreviewController.swift @@ -5,6 +5,105 @@ import Postbox import TelegramCore import SwiftSignalKit +private func galleryMediaForMedia(media: Media) -> Media? { + if let media = media as? TelegramMediaImage { + return media + } else if let file = media as? TelegramMediaFile { + if file.mimeType.hasPrefix("audio/") { + return nil + } else if !file.isVideo && file.mimeType.hasPrefix("video/") { + return file + } else { + return file + } + } + return nil +} + +private func mediaForMessage(message: Message) -> Media? { + for media in message.media { + if let result = galleryMediaForMedia(media: media) { + return result + } else if let webpage = media as? TelegramMediaWebpage { + switch webpage.content { + case let .Loaded(content): + if let embedUrl = content.embedUrl, !embedUrl.isEmpty { + return webpage + } else if let image = content.image { + if let result = galleryMediaForMedia(media: image) { + return result + } + } else if let file = content.file { + if let result = galleryMediaForMedia(media: file) { + return result + } + } + case .Pending: + break + } + } + } + return nil +} + +private final class SecretMediaPreviewControllerNode: GalleryControllerNode { + private var timeoutNode: RadialStatusNode? + + private var validLayout: (ContainerViewLayout, CGFloat)? + + var beginTimeAndTimeout: (Double, Double)? { + didSet { + if let (beginTime, timeout) = self.beginTimeAndTimeout { + if self.timeoutNode == nil { + let timeoutNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) + self.timeoutNode = timeoutNode + timeoutNode.transitionToState(.secretTimeout(color: .white, icon: nil, beginTime: beginTime, timeout: timeout), completion: {}) + self.addSubnode(timeoutNode) + + if let (layout, navigationHeight) = self.validLayout { + self.layoutTimeoutNode(layout, navigationBarHeight: navigationHeight, transition: .immediate) + } + } + } else if let timeoutNode = self.timeoutNode { + self.timeoutNode = nil + timeoutNode.removeFromSupernode() + } + } + } + + override func animateIn(animateContent: Bool) { + super.animateIn(animateContent: animateContent) + + self.timeoutNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateOut(animateContent: Bool, completion: @escaping () -> Void) { + super.animateOut(animateContent: animateContent, completion: completion) + + if let timeoutNode = self.timeoutNode { + timeoutNode.layer.animateAlpha(from: timeoutNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + } + + override func updateDismissTransition(_ value: CGFloat) { + self.timeoutNode?.alpha = value + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + self.validLayout = (layout, navigationBarHeight) + self.layoutTimeoutNode(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + + private func layoutTimeoutNode(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + if let timeoutNode = self.timeoutNode { + let diameter: CGFloat = 24.0 + transition.updateFrame(node: timeoutNode, frame: CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - diameter - 8.0, y: navigationBarHeight - 8.0 - diameter), size: CGSize(width: diameter, height: diameter))) + } + } +} + public final class SecretMediaPreviewController: ViewController { private let account: Account @@ -24,15 +123,21 @@ public final class SecretMediaPreviewController: ViewController { private var messageView: MessageView? private var currentNodeMessageId: MessageId? + private let _hiddenMedia = Promise<(MessageId, Media)?>(nil) + private var hiddenMediaManagerIndex: Int? + private let presentationData: PresentationData public init(account: Account, messageId: MessageId) { self.account = account self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - super.init(navigationBarTheme: nil) + super.init(navigationBarTheme: GalleryController.darkNavigationTheme) - self.statusBar.alpha = 0.0 + let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed)) + self.navigationItem.leftBarButtonItem = backItem + + self.statusBar.statusBarStyle = .White self.disposable.set((account.postbox.messageView(messageId) |> deliverOnMainQueue).start(next: { [weak self] view in if let strongSelf = self { @@ -42,6 +147,14 @@ public final class SecretMediaPreviewController: ViewController { } } })) + + self.hiddenMediaManagerIndex = account.telegramApplicationContext.mediaManager.galleryHiddenMediaManager.addSource(self._hiddenMedia.get() |> map { messageIdAndMedia in + if let (messageId, media) = messageIdAndMedia { + return .chat(messageId, media) + } else { + return nil + } + }) } required public init(coder aDecoder: NSCoder) { @@ -51,38 +164,222 @@ public final class SecretMediaPreviewController: ViewController { deinit { self.disposable.dispose() self.markMessageAsConsumedDisposable.dispose() + if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex { + self.account.telegramApplicationContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex) + } + } + + @objc func donePressed() { + self.dismiss(forceAway: false) } public override func loadDisplayNode() { - self.displayNode = SecretMediaPreviewControllerNode() + 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] _, _ in + }) + self.displayNode = SecretMediaPreviewControllerNode(controllerInteraction: controllerInteraction) self.displayNodeDidLoad() + self.controllerNode.statusBar = self.statusBar + self.controllerNode.navigationBar = self.navigationBar + + self.controllerNode.transitionDataForCentralItem = { [weak self] in + if let strongSelf = self { + if let centralItemNode = strongSelf.controllerNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? GalleryControllerPresentationArguments { + if let message = strongSelf.messageView?.message { + if let media = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media) { + return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface) + } + } + } + } + return nil + } self.controllerNode.dismiss = { [weak self] in - self?.dismiss() + self?._hiddenMedia.set(.single(nil)) + self?.presentingViewController?.dismiss(animated: false, completion: nil) } - if let messageView = self.messageView { - applyMessageView() + self.controllerNode.beginCustomDismiss = { [weak self] in + if let strongSelf = self { + strongSelf._hiddenMedia.set(.single(nil)) + + var animatedOutNode = true + var animatedOutInterface = false + + let completion = { + if animatedOutNode && animatedOutInterface { + //self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + } + + strongSelf.controllerNode.animateOut(animateContent: animatedOutNode, completion: { + animatedOutInterface = true + //completion() + }) + } } + + self.controllerNode.completeCustomDismiss = { [weak self] in + self?._hiddenMedia.set(.single(nil)) + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + + self.controllerNode.pager.centralItemIndexUpdated = { [weak self] index in + if let strongSelf = self { + var hiddenItem: (MessageId, Media)? + if let _ = index { + if let message = strongSelf.messageView?.message, let media = mediaForMessage(message: message) { + var beginTimeAndTimeout: (Double, Double)? + for attribute in message.attributes { + if let attribute = attribute as? AutoremoveTimeoutMessageAttribute { + if let countdownBeginTime = attribute.countdownBeginTime { + beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) + } + break + } + } + + if let _ = media as? TelegramMediaFile { + strongSelf.title = strongSelf.presentationData.strings.Message_Video + } else { + strongSelf.title = strongSelf.presentationData.strings.Message_Photo + } + + if let beginTimeAndTimeout = beginTimeAndTimeout { + strongSelf.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout + } + + if !message.flags.contains(.Incoming) { + if let _ = beginTimeAndTimeout { + strongSelf.controllerNode.updatePresentationState({ + $0.withUpdatedFooterContentNode(nil) + }, transition: .immediate) + } else { + let contentNode = SecretMediaPreviewFooterContentNode() + let peerTitle = messageMainPeer(message)?.compactDisplayTitle ?? "" + let text: String + if let _ = media as? TelegramMediaFile { + text = strongSelf.presentationData.strings.SecretVideo_NotViewedYet(peerTitle).0 + } else { + text = strongSelf.presentationData.strings.SecretImage_NotViewedYet(peerTitle).0 + } + contentNode.setText(text) + strongSelf.controllerNode.updatePresentationState({ + $0.withUpdatedFooterContentNode(contentNode) + }, transition: .immediate) + } + } + hiddenItem = (message.id, media) + } + } + if strongSelf.didSetReady { + strongSelf._hiddenMedia.set(.single(hiddenItem)) + } + } + } + + if let _ = self.messageView { + self.applyMessageView() + } + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + var nodeAnimatesItself = false + + if let centralItemNode = self.controllerNode.pager.centralItemNode(), let message = self.messageView?.message { + + if let media = mediaForMessage(message: message) { + if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let transitionArguments = presentationArguments.transitionArguments(message.id, media) { + nodeAnimatesItself = true + centralItemNode.activateAsInitial() + + if presentationArguments.animated { + centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface) + } + + self._hiddenMedia.set(.single((message.id, media))) + } else if self.isPresentedInPreviewingContext() { + centralItemNode.activateAsInitial() + } + } + } + + self.controllerNode.setControlsHidden(false, animated: false) + if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments { + if presentationArguments.animated { + self.controllerNode.animateIn(animateContent: !nodeAnimatesItself) + } + } + } + + 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.controllerNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let message = self.messageView?.message { + if let media = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media), !forceAway { + animatedOutNode = false + centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: { + animatedOutNode = true + completion() + }) + } + } + + self.controllerNode.animateOut(animateContent: animatedOutNode, completion: { + animatedOutInterface = true + completion() + }) } private func applyMessageView() { if let messageView = self.messageView, let message = messageView.message { if self.currentNodeMessageId != message.id { self.currentNodeMessageId = message.id - guard let item = galleryItemForEntry(account: account, theme: self.presentationData.theme, strings: self.presentationData.strings, entry: .MessageEntry(message, false, nil, nil), streamVideos: false) else { + guard let item = galleryItemForEntry(account: account, theme: self.presentationData.theme, strings: self.presentationData.strings, entry: .MessageEntry(message, false, nil, nil), streamVideos: false, hideControls: true, playbackCompleted: { [weak self] in + self?.dismiss(forceAway: false) + }) else { self._ready.set(.single(true)) return } - let itemNode = item.node() - self.controllerNode.setItemNode(itemNode) - - let ready = (itemNode.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void()))) |> afterNext { [weak self] _ in + + self.controllerNode.pager.replaceItems([item], centralItemIndex: 0) + let ready = self.controllerNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in self?.didSetReady = true } self._ready.set(ready |> map { true }) - self.markMessageAsConsumedDisposable.set(markMessageContentAsConsumedInteractively(postbox: self.account.postbox, messageId: message.id).start()) + } else { + var beginTimeAndTimeout: (Double, Double)? + for attribute in message.attributes { + if let attribute = attribute as? AutoremoveTimeoutMessageAttribute { + if let countdownBeginTime = attribute.countdownBeginTime { + beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) + } + break + } + } + + if self.isNodeLoaded { + if let beginTimeAndTimeout = beginTimeAndTimeout { + self.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout + } + } } } else { if !self.didSetReady { @@ -96,7 +393,7 @@ public final class SecretMediaPreviewController: ViewController { super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } override open func dismiss(completion: (() -> Void)? = nil) { diff --git a/TelegramUI/SecretMediaPreviewControllerNode.swift b/TelegramUI/SecretMediaPreviewControllerNode.swift index 2bf366e391..e47fa88bf5 100644 --- a/TelegramUI/SecretMediaPreviewControllerNode.swift +++ b/TelegramUI/SecretMediaPreviewControllerNode.swift @@ -2,73 +2,4 @@ import Foundation import AsyncDisplayKit import Display -class SecretMediaPreviewControllerNode: ASDisplayNode { - var containerLayout: (CGFloat, ContainerViewLayout)? - var backgroundNode: ASDisplayNode - - var dismiss: (() -> Void)? - - private var itemNode: GalleryItemNode? - private var itemNodeActivated = false - - override init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = UIColor.black - - super.init() - - self.setViewBlock({ - return UITracingLayerView() - }) - - self.addSubnode(self.backgroundNode) - } - - override func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (navigationBarHeight, layout) - - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height))) - if let itemNode = self.itemNode { - transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height))) - itemNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition) - if !self.itemNodeActivated { - self.itemNodeActivated = true - itemNode.centralityUpdated(isCentral: true) - } - } - } - - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.dismiss?() - } - } - - func setItemNode(_ itemNode: GalleryItemNode?) { - if let itemNode = self.itemNode { - itemNode.removeFromSupernode() - self.itemNodeActivated = false - } - - self.itemNode = itemNode - - if let itemNode = self.itemNode { - self.addSubnode(itemNode) - - if let (_, layout) = self.containerLayout { - itemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height)) - itemNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: .immediate) - if !self.itemNodeActivated { - self.itemNodeActivated = true - itemNode.centralityUpdated(isCentral: true) - } - } - } - } -} + diff --git a/TelegramUI/SecretMediaPreviewFooterContentNode.swift b/TelegramUI/SecretMediaPreviewFooterContentNode.swift new file mode 100644 index 0000000000..1aa54f3dab --- /dev/null +++ b/TelegramUI/SecretMediaPreviewFooterContentNode.swift @@ -0,0 +1,44 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit + +private let textFont = Font.regular(16.0) + +final class SecretMediaPreviewFooterContentNode: GalleryFooterContentNode { + private var currentText: String? + private let textNode: ImmediateTextNode + + override init() { + self.textNode = ImmediateTextNode() + self.textNode.isLayerBacked = true + self.textNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.textNode) + } + + func setText(_ text: String) { + if self.currentText != text { + self.currentText = text + + self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: .white) + + self.requestLayout?(.immediate) + } + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let panelHeight: CGFloat = 44.0 + bottomInset + + let sideInset: CGFloat = leftInset + 8.0 + let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((width - textSize.width) / 2.0), y: floor((44.0 - textSize.height) / 2.0)), size: textSize)) + + return panelHeight + } +} + diff --git a/TelegramUI/SinglePhoneInputNode.swift b/TelegramUI/SinglePhoneInputNode.swift new file mode 100644 index 0000000000..bd03a9bcc4 --- /dev/null +++ b/TelegramUI/SinglePhoneInputNode.swift @@ -0,0 +1,155 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore + +private func removeDuplicatedPlus(_ text: String?) -> String { + var result = "" + if let text = text { + for c in text { + if c == "+" { + if result.isEmpty { + result += String(c) + } + } else { + result += String(c) + } + } + } + return result +} + +private func removePlus(_ text: String?) -> String { + var result = "" + if let text = text { + for c in text { + if c != "+" { + result += String(c) + } + } + } + return result +} + +private func cleanPhoneNumber(_ text: String?) -> String { + var cleanNumber = "" + if let text = text { + for c in text { + if c == "+" { + if cleanNumber.isEmpty { + cleanNumber += String(c) + } + } else if c >= "0" && c <= "9" { + cleanNumber += String(c) + } + } + } + return cleanNumber +} + +private func cleanPrefix(_ text: String) -> String { + var result = "" + var checked = false + for c in text { + if c != " " { + checked = true + } + if checked { + result += String(c) + } + } + return result +} + +private func cleanSuffix(_ text: String) -> String { + var result = "" + var checked = false + for c in text.reversed() { + if c != " " { + checked = true + } + if checked { + result = String(c) + result + } + } + return result +} + +final class SinglePhoneInputNode: ASDisplayNode, UITextFieldDelegate { + private let fontSize: CGFloat + + var numberField: TextFieldNode? + + var enableEditing: Bool = true + + var number: String { + get { + return cleanPhoneNumber(self.numberField?.textField.text ?? "") + } set(value) { + self.updateNumber(value) + } + } + var numberUpdated: ((String) -> Void)? + + private let phoneFormatter = InteractivePhoneFormatter() + + private var validLayout: CGSize? + + init(fontSize: CGFloat = 20.0) { + self.fontSize = fontSize + + super.init() + } + + override func didLoad() { + super.didLoad() + + let numberField = TextFieldNode() + numberField.textField.font = Font.regular(self.fontSize) + numberField.textField.keyboardType = .numberPad + + self.addSubnode(numberField) + + numberField.textField.addTarget(self, action: #selector(self.numberTextChanged(_:)), for: .editingChanged) + numberField.textField.delegate = self + + self.numberField = numberField + + if let size = self.validLayout { + numberField.frame = CGRect(origin: CGPoint(), size: size) + } + } + + @objc func numberTextChanged(_ textField: UITextField) { + self.updateNumberFromTextFields() + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + return self.enableEditing + } + + private func updateNumberFromTextFields() { + guard let numberField = self.numberField else { + return + } + let inputText = removeDuplicatedPlus(cleanPhoneNumber(cleanPhoneNumber(numberField.textField.text))) + self.updateNumber(inputText) + self.numberUpdated?(inputText) + } + + private func updateNumber(_ inputText: String) { + let (_, numberText) = self.phoneFormatter.updateText(inputText) + guard let numberField = self.numberField else { + return + } + + if numberText != numberField.textField.text { + numberField.textField.text = numberText + } + } + + func updateLayout(size: CGSize) { + self.validLayout = size + self.numberField?.frame = CGRect(origin: CGPoint(), size: size) + } +} diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index 7398494f00..7ed52b90b7 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -66,6 +66,9 @@ public final class TelegramApplicationContext { return self._automaticMediaDownloadSettings.get() } + public let currentMediaInputSettings: Atomic + private var mediaInputSettingsDisposable: Disposable? + private let presentationDataDisposable = MetaDisposable() private let automaticMediaDownloadSettingsDisposable = MetaDisposable() @@ -77,7 +80,7 @@ public final class TelegramApplicationContext { } private var hasOngoingCallDisposable: Disposable? - public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, currentPresentationData: PresentationData, presentationData: Signal, currentMediaDownloadSettings: AutomaticMediaDownloadSettings, automaticMediaDownloadSettings: Signal, currentInAppNotificationSettings: InAppNotificationSettings, postbox: Postbox, network: Network, accountPeerId: PeerId?, viewTracker: AccountViewTracker?, stateManager: AccountStateManager?) { + public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, currentPresentationData: PresentationData, presentationData: Signal, currentMediaDownloadSettings: AutomaticMediaDownloadSettings, automaticMediaDownloadSettings: Signal, currentInAppNotificationSettings: InAppNotificationSettings, currentMediaInputSettings: MediaInputSettings, postbox: Postbox, network: Network, accountPeerId: PeerId?, viewTracker: AccountViewTracker?, stateManager: AccountStateManager?) { self.mediaManager = MediaManager(postbox: postbox, inForeground: applicationBindings.applicationInForeground) if applicationBindings.isMainApp { @@ -95,6 +98,7 @@ public final class TelegramApplicationContext { self.fetchManager = FetchManager(postbox: postbox) self.currentPresentationData = Atomic(value: currentPresentationData) self.currentAutomaticMediaDownloadSettings = Atomic(value: currentMediaDownloadSettings) + self.currentMediaInputSettings = Atomic(value: currentMediaInputSettings) self._presentationData.set(.single(currentPresentationData) |> then(presentationData)) self._automaticMediaDownloadSettings.set(.single(currentMediaDownloadSettings) |> then(automaticMediaDownloadSettings)) self.currentInAppNotificationSettings = Atomic(value: currentInAppNotificationSettings) @@ -111,6 +115,17 @@ public final class TelegramApplicationContext { } }) + let mediaInputSettingsPreferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.mediaInputSettings])) + self.mediaInputSettingsDisposable = (postbox.combinedView(keys: [mediaInputSettingsPreferencesKey]) |> deliverOnMainQueue).start(next: { [weak self] views in + if let strongSelf = self { + if let view = views.views[mediaInputSettingsPreferencesKey] as? PreferencesView { + if let settings = view.values[ApplicationSpecificPreferencesKeys.mediaInputSettings] as? MediaInputSettings { + let _ = strongSelf.currentMediaInputSettings.swap(settings) + } + } + } + }) + self.presentationDataDisposable.set(self._presentationData.get().start(next: { [weak self] next in if let strongSelf = self { var stringsUpdated = false @@ -149,6 +164,7 @@ public final class TelegramApplicationContext { self.presentationDataDisposable.dispose() self.automaticMediaDownloadSettingsDisposable.dispose() self.inAppNotificationSettingsDisposable?.dispose() + self.mediaInputSettingsDisposable?.dispose() } public func attachOverlayMediaController(_ controller: OverlayMediaController) { diff --git a/TelegramUI/ThemeSettingsChatPreviewItem.swift b/TelegramUI/ThemeSettingsChatPreviewItem.swift index 1abda897b8..3b6ef4423d 100644 --- a/TelegramUI/ThemeSettingsChatPreviewItem.swift +++ b/TelegramUI/ThemeSettingsChatPreviewItem.swift @@ -87,7 +87,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) self.controllerInteraction = ChatControllerInteraction(openMessage: { _ in - return false }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false diff --git a/TelegramUI/UniversalVideoCalleryItem.swift b/TelegramUI/UniversalVideoCalleryItem.swift index db9b0f1e3e..de83b4716e 100644 --- a/TelegramUI/UniversalVideoCalleryItem.swift +++ b/TelegramUI/UniversalVideoCalleryItem.swift @@ -19,8 +19,10 @@ class UniversalVideoGalleryItem: GalleryItem { let indexData: GalleryItemIndexData? let contentInfo: UniversalVideoGalleryItemContentInfo? let caption: String + let hideControls: Bool + let playbackCompleted: () -> Void - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: String) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: String, hideControls: Bool = false, playbackCompleted: @escaping () -> Void = {}) { self.account = account self.theme = theme self.strings = strings @@ -29,6 +31,8 @@ class UniversalVideoGalleryItem: GalleryItem { self.indexData = indexData self.contentInfo = contentInfo self.caption = caption + self.hideControls = hideControls + self.playbackCompleted = playbackCompleted } func node() -> GalleryItemNode { @@ -108,6 +112,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let statusDisposable = MetaDisposable() + var playbackCompleted: (() -> Void)? + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { self.account = account self.strings = strings @@ -171,6 +177,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { func setupItem(_ item: UniversalVideoGalleryItem) { if self.item?.content.id != item.content.id { + if item.hideControls { + self.statusButtonNode.isHidden = true + } + if let videoNode = self.videoNode { videoNode.canAttachContent = false videoNode.removeFromSupernode() @@ -242,7 +252,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.isPaused = isPaused - strongSelf.statusButtonNode.isHidden = !initialBuffering && (strongSelf.didPause || !isPaused || value == nil) + if !item.hideControls { + strongSelf.statusButtonNode.isHidden = !initialBuffering && (strongSelf.didPause || !isPaused || value == nil) + } if isPaused { if strongSelf.didPause { strongSelf.footerContentNode.content = .playbackPlay @@ -275,6 +287,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self._rightBarButtonItem.set(.single(rightBarButtonItem)) } + videoNode.playbackCompleted = { + Queue.mainQueue().async { + item.playbackCompleted() + } + } + self._ready.set(videoNode.ready) } diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index 128c8908f6..9ca676f00e 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -66,7 +66,7 @@ private enum UserInfoEntryTag { private enum UserInfoEntry: ItemListNodeEntry { case info(PresentationTheme, PresentationStrings, peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, displayCall: Bool) case about(PresentationTheme, String, String) - case phoneNumber(PresentationTheme, Int, String, String) + case phoneNumber(PresentationTheme, Int, String, String, Bool) case userName(PresentationTheme, String, String) case sendMessage(PresentationTheme, String) case addContact(PresentationTheme, String) @@ -144,8 +144,8 @@ private enum UserInfoEntry: ItemListNodeEntry { } else { return false } - case let .phoneNumber(lhsTheme, lhsIndex, lhsLabel, lhsValue): - if case let .phoneNumber(rhsTheme, rhsIndex, rhsLabel, rhsValue) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsLabel == rhsLabel, lhsValue == rhsValue { + case let .phoneNumber(lhsTheme, lhsIndex, lhsLabel, lhsValue, lhsMain): + if case let .phoneNumber(rhsTheme, rhsIndex, rhsLabel, rhsValue, rhsMain) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsLabel == rhsLabel, lhsValue == rhsValue, lhsMain == rhsMain { return true } else { return false @@ -225,7 +225,7 @@ private enum UserInfoEntry: ItemListNodeEntry { return 0 case .about: return 1 - case let .phoneNumber(_, index, _, _): + case let .phoneNumber(_, index, _, _, _): return 2 + index case .userName: return 1000 @@ -270,8 +270,8 @@ private enum UserInfoEntry: ItemListNodeEntry { return ItemListTextWithLabelItem(theme: theme, label: text, text: value, enabledEntitiyTypes: [], multiline: true, sectionId: self.section, action: { arguments.displayAboutContextMenu(value) }, tag: UserInfoEntryTag.about) - case let .phoneNumber(theme, _, label, value): - return ItemListTextWithLabelItem(theme: theme, label: label, text: value, enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: { + case let .phoneNumber(theme, _, label, value, isMain): + return ItemListTextWithLabelItem(theme: theme, label: label, text: value, textColor: isMain ? .accent : .primary, enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: { arguments.openCallMenu(value) }, longTapAction: { arguments.displayCopyContextMenu(.phoneNumber, value) @@ -394,7 +394,17 @@ private func stringForBlockAction(strings: PresentationStrings, action: Destruct } } -private func userInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, state: UserInfoState, peerChatState: PostboxCoding?, globalNotificationSettings: GlobalNotificationSettings) -> [UserInfoEntry] { +private func localizedPhoneNumberLabel(label: String, strings: PresentationStrings) -> String { + if label == "_$!!$_" { + return "mobile" + } else if label == "_$!!$_" { + return "home" + } else { + return label + } +} + +private func userInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, deviceContacts: [DeviceContact], state: UserInfoState, peerChatState: PostboxCoding?, globalNotificationSettings: GlobalNotificationSettings) -> [UserInfoEntry] { var entries: [UserInfoEntry] = [] guard let peer = view.peers[view.peerId], let user = peerViewMainPeer(view) as? TelegramUser else { @@ -420,7 +430,26 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat } if let phoneNumber = user.phone, !phoneNumber.isEmpty { - entries.append(UserInfoEntry.phoneNumber(presentationData.theme, 0, "home", formatPhoneNumber(phoneNumber))) + let formattedNumber = formatPhoneNumber(phoneNumber) + let normalizedNumber = DeviceContactNormalizedPhoneNumber(rawValue: formattedNumber) + + var index = 0 + var found = false + for contact in deviceContacts { + for number in contact.phoneNumbers { + var isMain = false + if number.number.normalized == normalizedNumber { + found = true + isMain = true + } + entries.append(UserInfoEntry.phoneNumber(presentationData.theme, index, localizedPhoneNumberLabel(label: number.label, strings: presentationData.strings), number.number.normalized.rawValue, isMain)) + index += 1 + } + } + if !found { + entries.append(UserInfoEntry.phoneNumber(presentationData.theme, index, "home", formattedNumber, false)) + index += 1 + } } if !isEditing { @@ -566,7 +595,11 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll } } }, tapAvatarAction: { - let _ = (account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in + let _ = (getUserPeer(postbox: account.postbox, peerId: peerId) |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer else { + return + } + if peer.profileImageRepresentations.isEmpty { return } @@ -737,9 +770,28 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }) }) + let peerView = Promise() + peerView.set(account.viewTracker.peerView(peerId)) + + let deviceContacts: Signal<[DeviceContact], NoError> = peerView.get() + |> map { peerView -> String in + if let peer = peerView.peers[peerId] as? TelegramUser { + return peer.phone ?? "" + } + return "" + } + |> distinctUntilChanged + |> mapToSignal { number -> Signal<[DeviceContact], NoError> in + if number.isEmpty { + return .single([]) + } else { + return account.telegramApplicationContext.contactsManager.subscribe(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(number))) + } + } + let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId), account.postbox.combinedView(keys: [.peerChatState(peerId: peerId), globalNotificationsKey])) - |> map { presentationData, state, view, combinedView -> (ItemListControllerState, (ItemListNodeState, UserInfoEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView.get(), deviceContacts, account.postbox.combinedView(keys: [.peerChatState(peerId: peerId), globalNotificationsKey])) + |> map { presentationData, state, view, deviceContacts, combinedView -> (ItemListControllerState, (ItemListNodeState, UserInfoEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings @@ -818,7 +870,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.UserInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) - let listState = ItemListNodeState(entries: userInfoEntries(account: account, presentationData: presentationData, view: view, state: state, peerChatState: (combinedView.views[.peerChatState(peerId: peerId)] as? PeerChatStateView)?.chatState, globalNotificationSettings: globalNotificationSettings), style: .plain) + let listState = ItemListNodeState(entries: userInfoEntries(account: account, presentationData: presentationData, view: view, deviceContacts: deviceContacts, state: state, peerChatState: (combinedView.views[.peerChatState(peerId: peerId)] as? PeerChatStateView)?.chatState, globalNotificationSettings: globalNotificationSettings), style: .plain) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/UserInfoEditingPhoneActionItem.swift b/TelegramUI/UserInfoEditingPhoneActionItem.swift new file mode 100644 index 0000000000..20dac51a16 --- /dev/null +++ b/TelegramUI/UserInfoEditingPhoneActionItem.swift @@ -0,0 +1,226 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit + +class UserInfoEditingPhoneActionItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let title: String + let sectionId: ItemListSectionId + let action: () -> Void + + init(theme: PresentationTheme, title: String, sectionId: ItemListSectionId, action: @escaping () -> Void, tag: Any? = nil) { + self.theme = theme + self.title = title + self.sectionId = sectionId + self.action = action + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = UserInfoEditingPhoneActionItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply() }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? UserInfoEditingPhoneActionItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } + } + + var selectable: Bool = true + + func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action() + } +} + +private let titleFont = Font.regular(15.0) + +class UserInfoEditingPhoneActionItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + private let iconNode: ASImageNode + private let titleNode: TextNode + + private var item: UserInfoEditingPhoneActionItem? + + var tag: Any? { + return self.item?.tag + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displayWithoutProcessing = true + self.iconNode.displaysAsynchronously = false + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.iconNode) + self.addSubnode(self.titleNode) + } + + func asyncLayout() -> (_ item: UserInfoEditingPhoneActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + + let currentItem = self.item + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + + let textColor = item.theme.list.itemAccentColor + + let iconImage = PresentationResourcesItemList.addPhoneIcon(item.theme) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + itemBackgroundColor = item.theme.list.plainBackgroundColor + itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) + insets = itemListNeighborsPlainInsets(neighbors) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + + strongSelf.iconNode.image = iconImage + + let _ = titleApply() + + let leftInset: CGFloat + + leftInset = 16.0 + params.leftInset + + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + + if let iconImage = iconImage { + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - iconImage.size.height) / 2.0) - 1.0), size: iconImage.size) + } + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + 30.0, y: 12.0), size: titleLayout.size) + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} + diff --git a/TelegramUI/UserInfoEditingPhoneItem.swift b/TelegramUI/UserInfoEditingPhoneItem.swift new file mode 100644 index 0000000000..248e574ddd --- /dev/null +++ b/TelegramUI/UserInfoEditingPhoneItem.swift @@ -0,0 +1,277 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit + +struct UserInfoEditingPhoneItemEditing { + let editable: Bool + let hasActiveRevealControls: Bool +} + +class UserInfoEditingPhoneItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let strings: PresentationStrings + let id: Int64 + let label: String + let value: String + let editing: UserInfoEditingPhoneItemEditing + let sectionId: ItemListSectionId + let setPhoneIdWithRevealedOptions: (Int64?, Int64?) -> Void + let updated: (String) -> Void + let selectLabel: () -> Void + let delete: () -> Void + + init(theme: PresentationTheme, strings: PresentationStrings, id: Int64, label: String, value: String, editing: UserInfoEditingPhoneItemEditing, sectionId: ItemListSectionId, setPhoneIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, updated: @escaping (String) -> Void, selectLabel: @escaping () -> Void, delete: @escaping () -> Void) { + self.theme = theme + self.strings = strings + self.id = id + self.label = label + self.value = value + self.editing = editing + self.sectionId = sectionId + self.setPhoneIdWithRevealedOptions = setPhoneIdWithRevealedOptions + self.updated = updated + self.selectLabel = selectLabel + self.delete = delete + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = UserInfoEditingPhoneItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply() }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? UserInfoEditingPhoneItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } + } + + var selectable: Bool = false +} + +private let titleFont = Font.regular(15.0) + +class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + + private let labelNode: TextNode + private let labelButtonNode: HighlightTrackingButtonNode + private let editableControlNode: ItemListEditableControlNode + private let labelSeparatorNode: ASDisplayNode + private let phoneNode: SinglePhoneInputNode + + private var item: UserInfoEditingPhoneItem? + private var layoutParams: ListViewItemLayoutParams? + + var tag: Any? { + return self.item?.tag + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.editableControlNode = ItemListEditableControlNode() + + self.labelNode = TextNode() + self.labelNode.isLayerBacked = true + self.labelNode.contentMode = .left + self.labelNode.contentsScale = UIScreen.main.scale + + self.labelButtonNode = HighlightTrackingButtonNode() + + self.labelSeparatorNode = ASDisplayNode() + self.labelSeparatorNode.isLayerBacked = true + + self.phoneNode = SinglePhoneInputNode(fontSize: 17.0) + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.editableControlNode) + self.addSubnode(self.labelNode) + self.addSubnode(self.labelButtonNode) + self.addSubnode(self.labelSeparatorNode) + self.addSubnode(self.phoneNode) + + self.labelButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.labelNode.layer.removeAnimation(forKey: "opacity") + strongSelf.labelNode.alpha = 0.4 + } else { + strongSelf.labelNode.alpha = 1.0 + strongSelf.labelNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.labelButtonNode.addTarget(self, action: #selector(self.labelPressed), forControlEvents: .touchUpInside) + + self.editableControlNode.tapped = { [weak self] in + if let strongSelf = self { + strongSelf.setRevealOptionsOpened(true, animated: true) + strongSelf.revealOptionsInteractivelyOpened() + } + } + + self.phoneNode.numberUpdated = { [weak self] number in + self?.item?.updated(number) + } + } + + func asyncLayout() -> (_ item: UserInfoEditingPhoneItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + + let currentItem = self.item + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + + let controlSizeAndApply = editableControlLayout(44.0, item.theme, false) + + let textColor = item.theme.list.itemAccentColor + + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + itemBackgroundColor = item.theme.list.plainBackgroundColor + itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) + insets = itemListNeighborsPlainInsets(neighbors) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.labelSeparatorNode.backgroundColor = itemSeparatorColor + } + + let revealOffset = strongSelf.revealOffset + + let _ = labelApply() + + let leftInset: CGFloat + + leftInset = 16.0 + params.leftInset + + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + + let _ = controlSizeAndApply.1() + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + 4.0 + revealOffset, y: 0.0), size: controlSizeAndApply.0) + strongSelf.editableControlNode.frame = editableControlFrame + + let labelFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset + 30.0, y: 12.0), size: labelLayout.size) + strongSelf.labelNode.frame = labelFrame + strongSelf.labelButtonNode.frame = labelFrame + strongSelf.labelSeparatorNode.frame = CGRect(origin: CGPoint(x: labelFrame.maxX + 8.0, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.contentSize.height)) + + let phoneX = labelFrame.maxX + 16.0 + let phoneFrame = CGRect(origin: CGPoint(x: phoneX, y: 0.0), size: CGSize(width: max(1.0, params.width - params.rightInset - phoneX), height: layout.contentSize.height)) + strongSelf.phoneNode.frame = phoneFrame + strongSelf.phoneNode.updateLayout(size: phoneFrame.size) + strongSelf.phoneNode.number = item.value + strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]) + } + }) + } + } + + override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + guard let params = self.layoutParams else { + return + } + + let revealOffset = offset + let leftInset = 16.0 + params.leftInset + + var controlFrame = self.editableControlNode.frame + controlFrame.origin.x = params.leftInset + 4.0 + revealOffset + transition.updateFrame(node: self.editableControlNode, frame: controlFrame) + + var labelFrame = self.labelNode.frame + labelFrame.origin.x = revealOffset + leftInset + 30.0 + transition.updateFrame(node: self.labelNode, frame: labelFrame) + + var labelSeparatorFrame = self.labelSeparatorNode.frame + labelSeparatorFrame.origin.x = labelFrame.maxX + 8.0 + transition.updateFrame(node: self.labelSeparatorNode, frame: labelSeparatorFrame) + + var phoneFrame = self.phoneNode.frame + phoneFrame.origin.x = labelFrame.maxX + 16.0 + transition.updateFrame(node: self.phoneNode, frame: phoneFrame) + } + + override func revealOptionSelected(_ option: ItemListRevealOption) { + self.item?.delete() + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + @objc func labelPressed() { + self.item?.selectLabel() + } +}