mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00
no message
This commit is contained in:
parent
1b09f8430a
commit
e93d7791a7
@ -76,6 +76,15 @@
|
||||
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 */; };
|
||||
D0383ED4207CFBB900C45548 /* GalleryThumbnailContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383ED3207CFBB900C45548 /* GalleryThumbnailContainerNode.swift */; };
|
||||
D0383EDC207D1A1600C45548 /* emoji_suggestions_data.h in Headers */ = {isa = PBXBuildFile; fileRef = D0383ED6207D1A1500C45548 /* emoji_suggestions_data.h */; };
|
||||
D0383EDD207D1A1600C45548 /* TGEmojiSuggestions.h in Headers */ = {isa = PBXBuildFile; fileRef = D0383ED7207D1A1500C45548 /* TGEmojiSuggestions.h */; };
|
||||
D0383EDE207D1A1600C45548 /* emoji_suggestions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0383ED8207D1A1600C45548 /* emoji_suggestions.cpp */; };
|
||||
D0383EDF207D1A1600C45548 /* emoji_suggestions_data.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0383ED9207D1A1600C45548 /* emoji_suggestions_data.cpp */; };
|
||||
D0383EE0207D1A1600C45548 /* TGEmojiSuggestions.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0383EDA207D1A1600C45548 /* TGEmojiSuggestions.mm */; };
|
||||
D0383EE1207D1A1600C45548 /* emoji_suggestions.h in Headers */ = {isa = PBXBuildFile; fileRef = D0383EDB207D1A1600C45548 /* emoji_suggestions.h */; };
|
||||
D0383EE4207D292800C45548 /* EmojisChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.swift */; };
|
||||
D0383EE6207D299600C45548 /* EmojisChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.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 */; };
|
||||
@ -146,6 +155,8 @@
|
||||
D06BEC771F62F68B0035A545 /* OverlayUniversalVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */; };
|
||||
D06BEC8A1F6597A80035A545 /* OverlayVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BEC891F6597A80035A545 /* OverlayVideoDecoration.swift */; };
|
||||
D06BEC8C1F65E30A0035A545 /* WebEmbedVideoContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */; };
|
||||
D06D37A92077DDF3009219B6 /* AutodownloadMediaCategoryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06D37A82077DDF3009219B6 /* AutodownloadMediaCategoryController.swift */; };
|
||||
D06D37B22077E77F009219B6 /* AutodownloadSizeLimitItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06D37B12077E77F009219B6 /* AutodownloadSizeLimitItem.swift */; };
|
||||
D06E0F8E1F79ABFB003CF3DD /* ChatLoadingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */; };
|
||||
D06F1EA41F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F1EA31F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift */; };
|
||||
D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073D2DA1FB61DA9009E1DA2 /* CallListSettings.swift */; };
|
||||
@ -205,6 +216,7 @@
|
||||
D09E63AA1F0FC681003444CD /* PictureInPictureVideoControlsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E63A91F0FC681003444CD /* PictureInPictureVideoControlsNode.swift */; };
|
||||
D09E63B01F1010FE003444CD /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E63AF1F1010FE003444CD /* Contacts.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
D09E63B21F11289A003444CD /* PassKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E63B11F11289A003444CD /* PassKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
D09F9DCF20768DAF00DB4DE1 /* SecureIdLocalResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09F9DCE20768DAF00DB4DE1 /* SecureIdLocalResource.swift */; };
|
||||
D0A24D281F92C27100584D24 /* DefaultDarkAccentPresentationTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A24D271F92C27100584D24 /* DefaultDarkAccentPresentationTheme.swift */; };
|
||||
D0A723541FC3B40E0094D167 /* RadialCheckContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A723531FC3B40E0094D167 /* RadialCheckContentNode.swift */; };
|
||||
D0A8BBA11F61EE83000F03FD /* UniversalVideoCalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A8BBA01F61EE83000F03FD /* UniversalVideoCalleryItem.swift */; };
|
||||
@ -1098,6 +1110,15 @@
|
||||
D0380DAA204EA72F000414AB /* RadialStatusSecretTimeoutContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialStatusSecretTimeoutContentNode.swift; sourceTree = "<group>"; };
|
||||
D0380DAC204ED434000414AB /* LegacyLiveUploadInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyLiveUploadInterface.swift; sourceTree = "<group>"; };
|
||||
D0380DB7204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInstantVideoMessageDurationNode.swift; sourceTree = "<group>"; };
|
||||
D0383ED3207CFBB900C45548 /* GalleryThumbnailContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryThumbnailContainerNode.swift; sourceTree = "<group>"; };
|
||||
D0383ED6207D1A1500C45548 /* emoji_suggestions_data.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = emoji_suggestions_data.h; sourceTree = "<group>"; };
|
||||
D0383ED7207D1A1500C45548 /* TGEmojiSuggestions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGEmojiSuggestions.h; sourceTree = "<group>"; };
|
||||
D0383ED8207D1A1600C45548 /* emoji_suggestions.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = emoji_suggestions.cpp; sourceTree = "<group>"; };
|
||||
D0383ED9207D1A1600C45548 /* emoji_suggestions_data.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = emoji_suggestions_data.cpp; sourceTree = "<group>"; };
|
||||
D0383EDA207D1A1600C45548 /* TGEmojiSuggestions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TGEmojiSuggestions.mm; sourceTree = "<group>"; };
|
||||
D0383EDB207D1A1600C45548 /* emoji_suggestions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = emoji_suggestions.h; sourceTree = "<group>"; };
|
||||
D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojisChatInputContextPanelNode.swift; sourceTree = "<group>"; };
|
||||
D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojisChatInputPanelItem.swift; sourceTree = "<group>"; };
|
||||
D03922A61DF70E3F000F2CE9 /* MediaPlayerScrubbingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerScrubbingNode.swift; sourceTree = "<group>"; };
|
||||
D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingOverlayButton.swift; sourceTree = "<group>"; };
|
||||
D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputAudioRecordingTimeNode.swift; sourceTree = "<group>"; };
|
||||
@ -1306,6 +1327,8 @@
|
||||
D06BEC761F62F68B0035A545 /* OverlayUniversalVideoNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayUniversalVideoNode.swift; sourceTree = "<group>"; };
|
||||
D06BEC891F6597A80035A545 /* OverlayVideoDecoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayVideoDecoration.swift; sourceTree = "<group>"; };
|
||||
D06BEC8B1F65E30A0035A545 /* WebEmbedVideoContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebEmbedVideoContent.swift; sourceTree = "<group>"; };
|
||||
D06D37A82077DDF3009219B6 /* AutodownloadMediaCategoryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutodownloadMediaCategoryController.swift; sourceTree = "<group>"; };
|
||||
D06D37B12077E77F009219B6 /* AutodownloadSizeLimitItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutodownloadSizeLimitItem.swift; sourceTree = "<group>"; };
|
||||
D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLoadingNode.swift; sourceTree = "<group>"; };
|
||||
D06E4AC31E84806300627D1D /* FetchPhotoLibraryImageResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchPhotoLibraryImageResource.swift; sourceTree = "<group>"; };
|
||||
D06F1EA31F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHistorySearchContainerNode.swift; sourceTree = "<group>"; };
|
||||
@ -1424,6 +1447,7 @@
|
||||
D09E63A91F0FC681003444CD /* PictureInPictureVideoControlsNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PictureInPictureVideoControlsNode.swift; sourceTree = "<group>"; };
|
||||
D09E63AF1F1010FE003444CD /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; };
|
||||
D09E63B11F11289A003444CD /* PassKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PassKit.framework; path = System/Library/Frameworks/PassKit.framework; sourceTree = SDKROOT; };
|
||||
D09F9DCE20768DAF00DB4DE1 /* SecureIdLocalResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdLocalResource.swift; sourceTree = "<group>"; };
|
||||
D0A11BF91E7836C20081CE03 /* ChangePhoneNumberIntroController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberIntroController.swift; sourceTree = "<group>"; };
|
||||
D0A11BFB1E7840750081CE03 /* ChangePhoneNumberController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberController.swift; sourceTree = "<group>"; };
|
||||
D0A11BFD1E7840A50081CE03 /* ChangePhoneNumberControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberControllerNode.swift; sourceTree = "<group>"; };
|
||||
@ -2154,6 +2178,28 @@
|
||||
name = "Plaintext Fields";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0383ED5207D19BC00C45548 /* Emoji */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0383ED9207D1A1600C45548 /* emoji_suggestions_data.cpp */,
|
||||
D0383ED6207D1A1500C45548 /* emoji_suggestions_data.h */,
|
||||
D0383ED8207D1A1600C45548 /* emoji_suggestions.cpp */,
|
||||
D0383EDB207D1A1600C45548 /* emoji_suggestions.h */,
|
||||
D0383ED7207D1A1500C45548 /* TGEmojiSuggestions.h */,
|
||||
D0383EDA207D1A1600C45548 /* TGEmojiSuggestions.mm */,
|
||||
);
|
||||
name = Emoji;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0383EE2207D291100C45548 /* Emojis */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0383EE3207D292800C45548 /* EmojisChatInputContextPanelNode.swift */,
|
||||
D0383EE5207D299600C45548 /* EmojisChatInputPanelItem.swift */,
|
||||
);
|
||||
name = Emojis;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D03ADB461D703250005A521C /* Interface State */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -2974,6 +3020,8 @@
|
||||
D0FA35001EA6127000E56FFA /* StorageUsageController.swift */,
|
||||
D0D4345B1F97CEAA00CC1806 /* ProxySettingsController.swift */,
|
||||
D0DFD5E11FCE2BA50039B3B1 /* CalculatingCacheSizeItem.swift */,
|
||||
D06D37A82077DDF3009219B6 /* AutodownloadMediaCategoryController.swift */,
|
||||
D06D37B12077E77F009219B6 /* AutodownloadSizeLimitItem.swift */,
|
||||
);
|
||||
name = "Data and Storage";
|
||||
sourceTree = "<group>";
|
||||
@ -3146,6 +3194,7 @@
|
||||
D0DF0CA21D82BCBC008AEB01 /* Mentions */,
|
||||
D0DC35481DE366B4000195EB /* Commands */,
|
||||
D0E35A041DE47FFE00BC6096 /* Context Request Results */,
|
||||
D0383EE2207D291100C45548 /* Emojis */,
|
||||
D03AA4DE202DBF6F0056C405 /* ChatContextResultPeekContentNode.swift */,
|
||||
);
|
||||
name = "Input Context Panels";
|
||||
@ -3785,6 +3834,7 @@
|
||||
D0F69E541D6B8BDA0046BCD6 /* GalleryPagerNode.swift */,
|
||||
D042C6801E8D9A6700C863B0 /* GalleryFooterNode.swift */,
|
||||
D042C6851E8DA69D00C863B0 /* GalleryFooterContentNode.swift */,
|
||||
D0383ED3207CFBB900C45548 /* GalleryThumbnailContainerNode.swift */,
|
||||
D0DE66051F9A51E200EF4AE9 /* GalleryHiddenMediaManager.swift */,
|
||||
D00C7CDA1E3776CA0080C3D5 /* Secret Preview */,
|
||||
D0F69E5A1D6B8BDD0046BCD6 /* Items */,
|
||||
@ -3893,6 +3943,7 @@
|
||||
D04614352005093B00EC0EF2 /* Device Location */,
|
||||
D025A4241F79428300563950 /* Fetch Manager */,
|
||||
D046142C2004DB1D00EC0EF2 /* Live Location Manager */,
|
||||
D0383ED5207D19BC00C45548 /* Emoji */,
|
||||
D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */,
|
||||
D08775081E3E59DE00A97350 /* PeerNotificationSoundStrings.swift */,
|
||||
D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */,
|
||||
@ -3956,6 +4007,7 @@
|
||||
D0FB87B11F7C4C19004DE005 /* FetchMediaUtils.swift */,
|
||||
D056CD731FF2996B00880D28 /* ExternalMusicAlbumArtResources.swift */,
|
||||
D007019D2029EFDD006B9E34 /* ICloudResources.swift */,
|
||||
D09F9DCE20768DAF00DB4DE1 /* SecureIdLocalResource.swift */,
|
||||
);
|
||||
name = Resources;
|
||||
sourceTree = "<group>";
|
||||
@ -4065,6 +4117,7 @@
|
||||
D0E9BAE71F0574FF00F079A4 /* STPCustomer.h in Headers */,
|
||||
D0208AD51FA33D14001F0D5F /* RaiseToListenActivator.h in Headers */,
|
||||
D0E9BAE31F0574D800F079A4 /* STPBankAccountParams.h in Headers */,
|
||||
D0383EE1207D1A1600C45548 /* emoji_suggestions.h in Headers */,
|
||||
D0E9BA361F05585000F079A4 /* STPPhoneNumberValidator.h in Headers */,
|
||||
D0E9BA511F0559DA00F079A4 /* STPImageLibrary.h in Headers */,
|
||||
D0E9BA4C1F0559C700F079A4 /* NSString+Stripe_CardBrands.h in Headers */,
|
||||
@ -4073,6 +4126,7 @@
|
||||
D0E9BA2A1F0557A600F079A4 /* STPFormEncoder.h in Headers */,
|
||||
D0E9BA321F05583A00F079A4 /* STPPostalCodeValidator.h in Headers */,
|
||||
D0E9BADC1F0574D800F079A4 /* PKPayment+Stripe.h in Headers */,
|
||||
D0383EDC207D1A1600C45548 /* emoji_suggestions_data.h in Headers */,
|
||||
D0E9BA491F0559B600F079A4 /* STPPaymentMethod.h in Headers */,
|
||||
D08803C51F6064CF00DD7951 /* TelegramUI.h in Headers */,
|
||||
D0E9BA171F05574500F079A4 /* STPPaymentCardTextFieldViewModel.h in Headers */,
|
||||
@ -4098,6 +4152,7 @@
|
||||
D0E9BA151F05574500F079A4 /* STPCardValidator.h in Headers */,
|
||||
D0E9BA401F0558FE00F079A4 /* StripeError.h in Headers */,
|
||||
D0E9BA191F05574500F079A4 /* STPPaymentCardTextField.h in Headers */,
|
||||
D0383EDD207D1A1600C45548 /* TGEmojiSuggestions.h in Headers */,
|
||||
D0E9BA3F1F0558FE00F079A4 /* STPSource.h in Headers */,
|
||||
D0E9BABC1F05735F00F079A4 /* STPPaymentConfiguration.h in Headers */,
|
||||
D0E9BA2E1F0557D400F079A4 /* STPAddress.h in Headers */,
|
||||
@ -4258,6 +4313,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D0684A041F6C3AD50059F570 /* ChatListTypingNode.swift in Sources */,
|
||||
D0383EE6207D299600C45548 /* EmojisChatInputPanelItem.swift in Sources */,
|
||||
D0EC6CAE1EB9F58800EBF1C3 /* animations.c in Sources */,
|
||||
D0FE4DDC1F09AD0400E8A0B3 /* PresentationSurfaceLevels.swift in Sources */,
|
||||
D0EC6CAF1EB9F58800EBF1C3 /* buffer.c in Sources */,
|
||||
@ -4344,6 +4400,7 @@
|
||||
D093D82220699A7C00BC3599 /* FormControllerNode.swift in Sources */,
|
||||
D0EC6CDB1EB9F58800EBF1C3 /* Markdown.swift in Sources */,
|
||||
D0E412D0206A75B200BEE4A2 /* FormControllerDetailActionItem.swift in Sources */,
|
||||
D09F9DCF20768DAF00DB4DE1 /* SecureIdLocalResource.swift in Sources */,
|
||||
D0471B641EFEB5CB0074D609 /* BotPaymentItemNode.swift in Sources */,
|
||||
D0380DB8204EE0A5000414AB /* ChatInstantVideoMessageDurationNode.swift in Sources */,
|
||||
D01C7F001EF9D45B008305F1 /* DeviceContactsManager.swift in Sources */,
|
||||
@ -4660,6 +4717,7 @@
|
||||
D04B4D131EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift in Sources */,
|
||||
D0EC6D951EB9F58900EBF1C3 /* ChatMessageInteractiveFileNode.swift in Sources */,
|
||||
D01A21B11F3A050E00DDA104 /* InstantPageNavigationBar.swift in Sources */,
|
||||
D0383EE4207D292800C45548 /* EmojisChatInputContextPanelNode.swift in Sources */,
|
||||
D0EC6D961EB9F58900EBF1C3 /* ChatMessageInteractiveMediaNode.swift in Sources */,
|
||||
D0B2F7722052D0DD00D3BFB9 /* InviteContactsCountPanelNode.swift in Sources */,
|
||||
D0EC6D971EB9F58900EBF1C3 /* ChatMessageItem.swift in Sources */,
|
||||
@ -4706,6 +4764,7 @@
|
||||
D0EC6DAC1EB9F58900EBF1C3 /* ChatInterfaceStateInputPanels.swift in Sources */,
|
||||
D056CD761FF2A30900880D28 /* ChatSwipeToReplyRecognizer.swift in Sources */,
|
||||
D091C7A41F8EBB1E00D7DE13 /* ChatPresentationData.swift in Sources */,
|
||||
D0383EE0207D1A1600C45548 /* TGEmojiSuggestions.mm in Sources */,
|
||||
D0EB41F31F2FEAB800838FE6 /* LegacyComponentsStickers.swift in Sources */,
|
||||
D0EC6DAD1EB9F58900EBF1C3 /* ChatInterfaceStateNavigationButtons.swift in Sources */,
|
||||
D0EC6DAE1EB9F58900EBF1C3 /* ChatInterfaceStateContextMenus.swift in Sources */,
|
||||
@ -4808,6 +4867,7 @@
|
||||
D0EC6DEA1EB9F58900EBF1C3 /* ChatReportPeerTitlePanelNode.swift in Sources */,
|
||||
D0EC6DEB1EB9F58900EBF1C3 /* ChatRequestInProgressTitlePanelNode.swift in Sources */,
|
||||
D0EC6DEC1EB9F58900EBF1C3 /* ChatToastAlertPanelNode.swift in Sources */,
|
||||
D06D37B22077E77F009219B6 /* AutodownloadSizeLimitItem.swift in Sources */,
|
||||
D0EC6DED1EB9F58900EBF1C3 /* ChatHistoryNavigationButtonNode.swift in Sources */,
|
||||
D0FB87B21F7C4C19004DE005 /* FetchMediaUtils.swift in Sources */,
|
||||
D0E9BA0C1F04580700F079A4 /* BotCheckoutWebInteractionControllerNode.swift in Sources */,
|
||||
@ -4950,6 +5010,7 @@
|
||||
D0EC6E4A1EB9F58900EBF1C3 /* ItemListControllerNode.swift in Sources */,
|
||||
D0147BA9206EA35000E40378 /* SecureIdDocumentGalleryController.swift in Sources */,
|
||||
D0B37C5C1F8D22AE004252DF /* ThemeSettingsController.swift in Sources */,
|
||||
D0383ED4207CFBB900C45548 /* GalleryThumbnailContainerNode.swift in Sources */,
|
||||
D0EC6E4B1EB9F58900EBF1C3 /* ItemListControllerSegmentedTitleView.swift in Sources */,
|
||||
D0EC6E4D1EB9F58900EBF1C3 /* PeerInfoController.swift in Sources */,
|
||||
D0EC6E4E1EB9F58900EBF1C3 /* GroupInfoController.swift in Sources */,
|
||||
@ -4993,12 +5054,14 @@
|
||||
D0EC6E641EB9F58900EBF1C3 /* TwoStepVerificationPasswordEntryController.swift in Sources */,
|
||||
D0EC6E651EB9F58900EBF1C3 /* TwoStepVerificationResetController.swift in Sources */,
|
||||
D0EC6E661EB9F58900EBF1C3 /* PasscodeOptionsController.swift in Sources */,
|
||||
D0383EDE207D1A1600C45548 /* emoji_suggestions.cpp in Sources */,
|
||||
D0EC6E671EB9F58900EBF1C3 /* DataAndStorageSettingsController.swift in Sources */,
|
||||
D0EC6E681EB9F58900EBF1C3 /* VoiceCallDataSavingController.swift in Sources */,
|
||||
D0EC6E691EB9F58900EBF1C3 /* NetworkUsageStatsController.swift in Sources */,
|
||||
D0EC6E6A1EB9F58900EBF1C3 /* StorageUsageController.swift in Sources */,
|
||||
D079FCDF1F05C9280038FADE /* BotReceiptController.swift in Sources */,
|
||||
D0EC6E6B1EB9F58900EBF1C3 /* InstalledStickerPacksController.swift in Sources */,
|
||||
D0383EDF207D1A1600C45548 /* emoji_suggestions_data.cpp in Sources */,
|
||||
D0EC6E6C1EB9F58900EBF1C3 /* FeaturedStickerPacksController.swift in Sources */,
|
||||
D0B85C231FF70BF400E795B4 /* AuthorizationSequenceAwaitingAccountResetController.swift in Sources */,
|
||||
D0EC6E6D1EB9F58900EBF1C3 /* ItemListStickerPackItem.swift in Sources */,
|
||||
@ -5015,6 +5078,7 @@
|
||||
D0EC6E751EB9F58900EBF1C3 /* ThemeGridControllerNode.swift in Sources */,
|
||||
D0EC6E761EB9F58900EBF1C3 /* SettingsController.swift in Sources */,
|
||||
D0EC6E771EB9F58900EBF1C3 /* NotificationsAndSounds.swift in Sources */,
|
||||
D06D37A92077DDF3009219B6 /* AutodownloadMediaCategoryController.swift in Sources */,
|
||||
D0EC6E781EB9F58900EBF1C3 /* NotificationSoundSelection.swift in Sources */,
|
||||
D056CD741FF2996B00880D28 /* ExternalMusicAlbumArtResources.swift in Sources */,
|
||||
D0F0AAE41EC21AAA005EE2A5 /* CallControllerButtonsNode.swift in Sources */,
|
||||
|
576
TelegramUI/AutodownloadMediaCategoryController.swift
Normal file
576
TelegramUI/AutodownloadMediaCategoryController.swift
Normal file
@ -0,0 +1,576 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
enum AutomaticDownloadCategory {
|
||||
case photo
|
||||
case video
|
||||
case file
|
||||
case voiceMessage
|
||||
case videoMessage
|
||||
}
|
||||
|
||||
private enum ConnectionType {
|
||||
case cellular
|
||||
case wifi
|
||||
}
|
||||
|
||||
private enum PeerType {
|
||||
case contact
|
||||
case otherPrivate
|
||||
case group
|
||||
case channel
|
||||
}
|
||||
|
||||
private final class AutodownloadMediaCategoryControllerArguments {
|
||||
let toggle: (ConnectionType, PeerType) -> Void
|
||||
let adjustSize: (Int32) -> Void
|
||||
|
||||
init(toggle: @escaping (ConnectionType, PeerType) -> Void, adjustSize: @escaping (Int32) -> Void) {
|
||||
self.toggle = toggle
|
||||
self.adjustSize = adjustSize
|
||||
}
|
||||
}
|
||||
|
||||
private enum AutodownloadMediaCategorySection: Int32 {
|
||||
case cellular
|
||||
case wifi
|
||||
case size
|
||||
}
|
||||
|
||||
private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry {
|
||||
case cellularHeader(PresentationTheme, String)
|
||||
case cellularContacts(PresentationTheme, String, Bool)
|
||||
case cellularOtherPrivate(PresentationTheme, String, Bool)
|
||||
case cellularGroups(PresentationTheme, String, Bool)
|
||||
case cellularChannels(PresentationTheme, String, Bool)
|
||||
|
||||
case wifiHeader(PresentationTheme, String)
|
||||
case wifiContacts(PresentationTheme, String, Bool)
|
||||
case wifiOtherPrivate(PresentationTheme, String, Bool)
|
||||
case wifiGroups(PresentationTheme, String, Bool)
|
||||
case wifiChannels(PresentationTheme, String, Bool)
|
||||
|
||||
case sizeHeader(PresentationTheme, String)
|
||||
case sizeItem(PresentationTheme, String, Int32)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .cellularHeader, .cellularContacts, .cellularOtherPrivate, .cellularGroups, .cellularChannels:
|
||||
return AutodownloadMediaCategorySection.cellular.rawValue
|
||||
case .wifiHeader, .wifiContacts, .wifiOtherPrivate, .wifiGroups, .wifiChannels:
|
||||
return AutodownloadMediaCategorySection.wifi.rawValue
|
||||
case .sizeHeader, .sizeItem:
|
||||
return AutodownloadMediaCategorySection.size.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .cellularHeader:
|
||||
return 0
|
||||
case .cellularContacts:
|
||||
return 1
|
||||
case .cellularOtherPrivate:
|
||||
return 2
|
||||
case .cellularGroups:
|
||||
return 3
|
||||
case .cellularChannels:
|
||||
return 4
|
||||
case .wifiHeader:
|
||||
return 5
|
||||
case .wifiContacts:
|
||||
return 6
|
||||
case .wifiOtherPrivate:
|
||||
return 7
|
||||
case .wifiGroups:
|
||||
return 8
|
||||
case .wifiChannels:
|
||||
return 9
|
||||
case .sizeHeader:
|
||||
return 10
|
||||
case .sizeItem:
|
||||
return 11
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: AutodownloadMediaCategoryEntry, rhs: AutodownloadMediaCategoryEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .cellularHeader(lhsTheme, lhsText):
|
||||
if case let .cellularHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .cellularContacts(lhsTheme, lhsText, lhsValue):
|
||||
if case let .cellularContacts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .cellularOtherPrivate(lhsTheme, lhsText, lhsValue):
|
||||
if case let .cellularOtherPrivate(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .cellularGroups(lhsTheme, lhsText, lhsValue):
|
||||
if case let .cellularGroups(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .cellularChannels(lhsTheme, lhsText, lhsValue):
|
||||
if case let .cellularChannels(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .wifiHeader(lhsTheme, lhsText):
|
||||
if case let .wifiHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .wifiContacts(lhsTheme, lhsText, lhsValue):
|
||||
if case let .wifiContacts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .wifiOtherPrivate(lhsTheme, lhsText, lhsValue):
|
||||
if case let .wifiOtherPrivate(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .wifiGroups(lhsTheme, lhsText, lhsValue):
|
||||
if case let .wifiGroups(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .wifiChannels(lhsTheme, lhsText, lhsValue):
|
||||
if case let .wifiChannels(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .sizeHeader(lhsTheme, lhsText):
|
||||
if case let .sizeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .sizeItem(lhsTheme, lhsText, lhsValue):
|
||||
if case let .sizeItem(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: AutodownloadMediaCategoryEntry, rhs: AutodownloadMediaCategoryEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(_ arguments: AutodownloadMediaCategoryControllerArguments) -> ListViewItem {
|
||||
switch self {
|
||||
case let .cellularHeader(theme, text):
|
||||
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
|
||||
case let .cellularContacts(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggle(.cellular, .contact)
|
||||
})
|
||||
case let .cellularOtherPrivate(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggle(.cellular, .otherPrivate)
|
||||
})
|
||||
case let .cellularGroups(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggle(.cellular, .group)
|
||||
})
|
||||
case let .cellularChannels(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggle(.cellular, .channel)
|
||||
})
|
||||
case let .wifiHeader(theme, text):
|
||||
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
|
||||
case let .wifiContacts(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggle(.wifi, .contact)
|
||||
})
|
||||
case let .wifiOtherPrivate(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggle(.wifi, .otherPrivate)
|
||||
})
|
||||
case let .wifiGroups(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggle(.wifi, .group)
|
||||
})
|
||||
case let .wifiChannels(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggle(.wifi, .channel)
|
||||
})
|
||||
case let .sizeHeader(theme, text):
|
||||
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
|
||||
case let .sizeItem(theme, text, value):
|
||||
return AutodownloadSizeLimitItem(theme: theme, text: text, value: value, sectionId: self.section, updated: { value in
|
||||
arguments.adjustSize(value)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AutodownloadMediaCategoryControllerState: Equatable {
|
||||
}
|
||||
|
||||
private struct AutomaticDownloadPeers {
|
||||
let contacts: Bool
|
||||
let otherPrivate: Bool
|
||||
let groups: Bool
|
||||
let channels: Bool
|
||||
}
|
||||
|
||||
private func autodownloadMediaCategoryControllerEntries(presentationData: PresentationData, category: AutomaticDownloadCategory, settings: AutomaticMediaDownloadSettings) -> [AutodownloadMediaCategoryEntry] {
|
||||
var entries: [AutodownloadMediaCategoryEntry] = []
|
||||
|
||||
let cellular: AutomaticDownloadPeers
|
||||
let wifi: AutomaticDownloadPeers
|
||||
let size: Int32
|
||||
|
||||
switch category {
|
||||
case .photo:
|
||||
cellular = AutomaticDownloadPeers(
|
||||
contacts: settings.peers.contacts.photo.cellular,
|
||||
otherPrivate: settings.peers.otherPrivate.photo.cellular,
|
||||
groups: settings.peers.groups.photo.cellular,
|
||||
channels: settings.peers.channels.photo.cellular
|
||||
)
|
||||
wifi = AutomaticDownloadPeers(
|
||||
contacts: settings.peers.contacts.photo.wifi,
|
||||
otherPrivate: settings.peers.otherPrivate.photo.wifi,
|
||||
groups: settings.peers.groups.photo.wifi,
|
||||
channels: settings.peers.channels.photo.wifi
|
||||
)
|
||||
size = settings.peers.contacts.photo.sizeLimit
|
||||
case .video:
|
||||
cellular = AutomaticDownloadPeers(
|
||||
contacts: settings.peers.contacts.video.cellular,
|
||||
otherPrivate: settings.peers.otherPrivate.video.cellular,
|
||||
groups: settings.peers.groups.video.cellular,
|
||||
channels: settings.peers.channels.video.cellular
|
||||
)
|
||||
wifi = AutomaticDownloadPeers(
|
||||
contacts: settings.peers.contacts.video.wifi,
|
||||
otherPrivate: settings.peers.otherPrivate.video.wifi,
|
||||
groups: settings.peers.groups.video.wifi,
|
||||
channels: settings.peers.channels.video.wifi
|
||||
)
|
||||
size = settings.peers.contacts.video.sizeLimit
|
||||
case .file:
|
||||
cellular = AutomaticDownloadPeers(
|
||||
contacts: settings.peers.contacts.file.cellular,
|
||||
otherPrivate: settings.peers.otherPrivate.file.cellular,
|
||||
groups: settings.peers.groups.file.cellular,
|
||||
channels: settings.peers.channels.file.cellular
|
||||
)
|
||||
wifi = AutomaticDownloadPeers(
|
||||
contacts: settings.peers.contacts.file.wifi,
|
||||
otherPrivate: settings.peers.otherPrivate.file.wifi,
|
||||
groups: settings.peers.groups.file.wifi,
|
||||
channels: settings.peers.channels.file.wifi
|
||||
)
|
||||
size = settings.peers.contacts.file.sizeLimit
|
||||
case .voiceMessage:
|
||||
cellular = AutomaticDownloadPeers(
|
||||
contacts: settings.peers.contacts.voiceMessage.cellular,
|
||||
otherPrivate: settings.peers.otherPrivate.voiceMessage.cellular,
|
||||
groups: settings.peers.groups.voiceMessage.cellular,
|
||||
channels: settings.peers.channels.voiceMessage.cellular
|
||||
)
|
||||
wifi = AutomaticDownloadPeers(
|
||||
contacts: settings.peers.contacts.voiceMessage.wifi,
|
||||
otherPrivate: settings.peers.otherPrivate.voiceMessage.wifi,
|
||||
groups: settings.peers.groups.voiceMessage.wifi,
|
||||
channels: settings.peers.channels.voiceMessage.wifi
|
||||
)
|
||||
size = settings.peers.contacts.voiceMessage.sizeLimit
|
||||
case .videoMessage:
|
||||
cellular = AutomaticDownloadPeers(
|
||||
contacts: settings.peers.contacts.videoMessage.cellular,
|
||||
otherPrivate: settings.peers.otherPrivate.videoMessage.cellular,
|
||||
groups: settings.peers.groups.videoMessage.cellular,
|
||||
channels: settings.peers.channels.videoMessage.cellular
|
||||
)
|
||||
wifi = AutomaticDownloadPeers(
|
||||
contacts: settings.peers.contacts.videoMessage.wifi,
|
||||
otherPrivate: settings.peers.otherPrivate.videoMessage.wifi,
|
||||
groups: settings.peers.groups.videoMessage.wifi,
|
||||
channels: settings.peers.channels.videoMessage.wifi
|
||||
)
|
||||
size = settings.peers.contacts.videoMessage.sizeLimit
|
||||
}
|
||||
|
||||
entries.append(.cellularHeader(presentationData.theme, "CELLULAR"))
|
||||
entries.append(.cellularContacts(presentationData.theme, "Contacts", cellular.contacts))
|
||||
entries.append(.cellularOtherPrivate(presentationData.theme, "Other Private", cellular.otherPrivate))
|
||||
entries.append(.cellularGroups(presentationData.theme, "Groups", cellular.groups))
|
||||
entries.append(.cellularChannels(presentationData.theme, "Channels", cellular.channels))
|
||||
|
||||
entries.append(.wifiHeader(presentationData.theme, "WI-FI"))
|
||||
entries.append(.wifiContacts(presentationData.theme, "Contacts", wifi.contacts))
|
||||
entries.append(.wifiOtherPrivate(presentationData.theme, "Other Private", wifi.otherPrivate))
|
||||
entries.append(.wifiGroups(presentationData.theme, "Groups", wifi.groups))
|
||||
entries.append(.wifiChannels(presentationData.theme, "Channels", wifi.channels))
|
||||
|
||||
switch category {
|
||||
case .file, .video:
|
||||
entries.append(.sizeHeader(presentationData.theme, "LIMIT BY SIZE"))
|
||||
let text: String
|
||||
if size == Int32.max {
|
||||
text = "unlimited"
|
||||
} else {
|
||||
text = "up to \(dataSizeString(Int(size)))"
|
||||
}
|
||||
entries.append(.sizeItem(presentationData.theme, text, size))
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func autodownloadMediaCategoryController(account: Account, category: AutomaticDownloadCategory) -> ViewController {
|
||||
let arguments = AutodownloadMediaCategoryControllerArguments(toggle: { connection, type in
|
||||
let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { settings in
|
||||
var settings = settings
|
||||
switch category {
|
||||
case .photo:
|
||||
switch type {
|
||||
case .contact:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.contacts.photo.cellular = !settings.peers.contacts.photo.cellular
|
||||
case .wifi:
|
||||
settings.peers.contacts.photo.wifi = !settings.peers.contacts.photo.wifi
|
||||
}
|
||||
case .otherPrivate:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.otherPrivate.photo.cellular = !settings.peers.otherPrivate.photo.cellular
|
||||
case .wifi:
|
||||
settings.peers.otherPrivate.photo.wifi = !settings.peers.otherPrivate.photo.wifi
|
||||
}
|
||||
case .group:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.groups.photo.cellular = !settings.peers.groups.photo.cellular
|
||||
case .wifi:
|
||||
settings.peers.groups.photo.wifi = !settings.peers.groups.photo.wifi
|
||||
}
|
||||
case .channel:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.channels.photo.cellular = !settings.peers.channels.photo.cellular
|
||||
case .wifi:
|
||||
settings.peers.channels.photo.wifi = !settings.peers.channels.photo.wifi
|
||||
}
|
||||
}
|
||||
case .video:
|
||||
switch type {
|
||||
case .contact:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.contacts.video.cellular = !settings.peers.contacts.video.cellular
|
||||
case .wifi:
|
||||
settings.peers.contacts.video.wifi = !settings.peers.contacts.video.wifi
|
||||
}
|
||||
case .otherPrivate:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.otherPrivate.video.cellular = !settings.peers.otherPrivate.video.cellular
|
||||
case .wifi:
|
||||
settings.peers.otherPrivate.video.wifi = !settings.peers.otherPrivate.video.wifi
|
||||
}
|
||||
case .group:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.groups.video.cellular = !settings.peers.groups.video.cellular
|
||||
case .wifi:
|
||||
settings.peers.groups.video.wifi = !settings.peers.groups.video.wifi
|
||||
}
|
||||
case .channel:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.channels.video.cellular = !settings.peers.channels.video.cellular
|
||||
case .wifi:
|
||||
settings.peers.channels.video.wifi = !settings.peers.channels.video.wifi
|
||||
}
|
||||
}
|
||||
case .file:
|
||||
switch type {
|
||||
case .contact:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.contacts.file.cellular = !settings.peers.contacts.file.cellular
|
||||
case .wifi:
|
||||
settings.peers.contacts.file.wifi = !settings.peers.contacts.file.wifi
|
||||
}
|
||||
case .otherPrivate:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.otherPrivate.file.cellular = !settings.peers.otherPrivate.file.cellular
|
||||
case .wifi:
|
||||
settings.peers.otherPrivate.file.wifi = !settings.peers.otherPrivate.file.wifi
|
||||
}
|
||||
case .group:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.groups.file.cellular = !settings.peers.groups.file.cellular
|
||||
case .wifi:
|
||||
settings.peers.groups.file.wifi = !settings.peers.groups.file.wifi
|
||||
}
|
||||
case .channel:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.channels.file.cellular = !settings.peers.channels.file.cellular
|
||||
case .wifi:
|
||||
settings.peers.channels.file.wifi = !settings.peers.channels.file.wifi
|
||||
}
|
||||
}
|
||||
case .voiceMessage:
|
||||
switch type {
|
||||
case .contact:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.contacts.voiceMessage.cellular = !settings.peers.contacts.voiceMessage.cellular
|
||||
case .wifi:
|
||||
settings.peers.contacts.voiceMessage.wifi = !settings.peers.contacts.voiceMessage.wifi
|
||||
}
|
||||
case .otherPrivate:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.otherPrivate.voiceMessage.cellular = !settings.peers.otherPrivate.voiceMessage.cellular
|
||||
case .wifi:
|
||||
settings.peers.otherPrivate.voiceMessage.wifi = !settings.peers.otherPrivate.voiceMessage.wifi
|
||||
}
|
||||
case .group:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.groups.voiceMessage.cellular = !settings.peers.groups.voiceMessage.cellular
|
||||
case .wifi:
|
||||
settings.peers.groups.voiceMessage.wifi = !settings.peers.groups.voiceMessage.wifi
|
||||
}
|
||||
case .channel:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.channels.voiceMessage.cellular = !settings.peers.channels.voiceMessage.cellular
|
||||
case .wifi:
|
||||
settings.peers.channels.voiceMessage.wifi = !settings.peers.channels.file.wifi
|
||||
}
|
||||
}
|
||||
case .videoMessage:
|
||||
switch type {
|
||||
case .contact:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.contacts.videoMessage.cellular = !settings.peers.contacts.videoMessage.cellular
|
||||
case .wifi:
|
||||
settings.peers.contacts.videoMessage.wifi = !settings.peers.contacts.videoMessage.wifi
|
||||
}
|
||||
case .otherPrivate:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.otherPrivate.videoMessage.cellular = !settings.peers.otherPrivate.videoMessage.cellular
|
||||
case .wifi:
|
||||
settings.peers.otherPrivate.videoMessage.wifi = !settings.peers.otherPrivate.videoMessage.wifi
|
||||
}
|
||||
case .group:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.groups.videoMessage.cellular = !settings.peers.groups.videoMessage.cellular
|
||||
case .wifi:
|
||||
settings.peers.groups.videoMessage.wifi = !settings.peers.groups.videoMessage.wifi
|
||||
}
|
||||
case .channel:
|
||||
switch connection {
|
||||
case .cellular:
|
||||
settings.peers.channels.videoMessage.cellular = !settings.peers.channels.videoMessage.cellular
|
||||
case .wifi:
|
||||
settings.peers.channels.videoMessage.wifi = !settings.peers.channels.file.wifi
|
||||
}
|
||||
}
|
||||
}
|
||||
return settings
|
||||
}).start()
|
||||
}, adjustSize: { size in
|
||||
let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { settings in
|
||||
var settings = settings
|
||||
switch category {
|
||||
case .photo:
|
||||
settings.peers.contacts.photo.sizeLimit = size
|
||||
settings.peers.otherPrivate.photo.sizeLimit = size
|
||||
settings.peers.groups.photo.sizeLimit = size
|
||||
settings.peers.channels.photo.sizeLimit = size
|
||||
case .video:
|
||||
settings.peers.contacts.video.sizeLimit = size
|
||||
settings.peers.otherPrivate.video.sizeLimit = size
|
||||
settings.peers.groups.video.sizeLimit = size
|
||||
settings.peers.channels.video.sizeLimit = size
|
||||
case .file:
|
||||
settings.peers.contacts.file.sizeLimit = size
|
||||
settings.peers.otherPrivate.file.sizeLimit = size
|
||||
settings.peers.groups.file.sizeLimit = size
|
||||
settings.peers.channels.file.sizeLimit = size
|
||||
case .videoMessage:
|
||||
settings.peers.contacts.videoMessage.sizeLimit = size
|
||||
settings.peers.otherPrivate.videoMessage.sizeLimit = size
|
||||
settings.peers.groups.videoMessage.sizeLimit = size
|
||||
settings.peers.channels.videoMessage.sizeLimit = size
|
||||
case .voiceMessage:
|
||||
settings.peers.contacts.voiceMessage.sizeLimit = size
|
||||
settings.peers.otherPrivate.voiceMessage.sizeLimit = size
|
||||
settings.peers.groups.voiceMessage.sizeLimit = size
|
||||
settings.peers.channels.voiceMessage.sizeLimit = size
|
||||
}
|
||||
return settings
|
||||
}).start()
|
||||
})
|
||||
|
||||
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings])) |> deliverOnMainQueue
|
||||
|> map { presentationData, prefs -> (ItemListControllerState, (ItemListNodeState<AutodownloadMediaCategoryEntry>, AutodownloadMediaCategoryEntry.ItemGenerationArguments)) in
|
||||
let automaticMediaDownloadSettings: AutomaticMediaDownloadSettings
|
||||
if let value = prefs.values[ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings] as? AutomaticMediaDownloadSettings {
|
||||
automaticMediaDownloadSettings = value
|
||||
} else {
|
||||
automaticMediaDownloadSettings = AutomaticMediaDownloadSettings.defaultSettings
|
||||
}
|
||||
|
||||
let title: String
|
||||
switch category {
|
||||
case .photo:
|
||||
title = "Photos"
|
||||
case .video:
|
||||
title = "Videos"
|
||||
case .file:
|
||||
title = "Files"
|
||||
case .voiceMessage:
|
||||
title = "Voice Messages"
|
||||
case .videoMessage:
|
||||
title = "Video Messages"
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
let listState = ItemListNodeState(entries: autodownloadMediaCategoryControllerEntries(presentationData: presentationData, category: category, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: false)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
let controller = ItemListController(account: account, state: signal)
|
||||
return controller
|
||||
}
|
||||
|
245
TelegramUI/AutodownloadSizeLimitItem.swift
Normal file
245
TelegramUI/AutodownloadSizeLimitItem.swift
Normal file
@ -0,0 +1,245 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
import LegacyComponents
|
||||
|
||||
private let autodownloadSizeValues: [Int32] = [
|
||||
1 * 1024 * 1024,
|
||||
5 * 1024 * 1024,
|
||||
10 * 1024 * 1024,
|
||||
50 * 1024 * 1024,
|
||||
100 * 1024 * 1024,
|
||||
300 * 1024 * 1024,
|
||||
500 * 1024 * 1024,
|
||||
Int32.max
|
||||
]
|
||||
|
||||
private func closestValue(_ v: Int32) -> Int32 {
|
||||
for value in autodownloadSizeValues.reversed() {
|
||||
if v >= value {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return autodownloadSizeValues[0]
|
||||
}
|
||||
|
||||
class AutodownloadSizeLimitItem: ListViewItem, ItemListItem {
|
||||
let theme: PresentationTheme
|
||||
let text: String
|
||||
let value: Int32
|
||||
let sectionId: ItemListSectionId
|
||||
let updated: (Int32) -> Void
|
||||
|
||||
init(theme: PresentationTheme, text: String, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
|
||||
self.theme = theme
|
||||
self.text = text
|
||||
self.value = value
|
||||
self.sectionId = sectionId
|
||||
self.updated = updated
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, () -> Void)) -> Void) {
|
||||
async {
|
||||
let node = AutodownloadSizeLimitItemNode()
|
||||
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? AutodownloadSizeLimitItemNode {
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func generateKnobImage() -> UIImage? {
|
||||
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: -1.0), blur: 3.5, color: UIColor(white: 0.0, alpha: 0.25).cgColor)
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0)))
|
||||
})
|
||||
}
|
||||
|
||||
class AutodownloadSizeLimitItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
|
||||
private let textNode: TextNode
|
||||
private var sliderView: TGPhotoEditorSliderView?
|
||||
|
||||
private var item: AutodownloadSizeLimitItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.textNode = TextNode()
|
||||
self.textNode.isLayerBacked = true
|
||||
self.textNode.displaysAsynchronously = false
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let sliderView = TGPhotoEditorSliderView()
|
||||
sliderView.enablePanHandling = true
|
||||
sliderView.trackCornerRadius = 1.0
|
||||
sliderView.lineSize = 2.0
|
||||
sliderView.dotSize = 5.0
|
||||
sliderView.minimumValue = 0.0
|
||||
sliderView.maximumValue = CGFloat(autodownloadSizeValues.count - 1)
|
||||
sliderView.startValue = 0.0
|
||||
sliderView.positionsCount = autodownloadSizeValues.count
|
||||
sliderView.disablesInteractiveTransitionGestureRecognizer = true
|
||||
if let item = self.item, let params = self.layoutParams {
|
||||
let value: CGFloat
|
||||
let index = autodownloadSizeValues.index(of: closestValue(item.value)) ?? 0
|
||||
value = CGFloat(index)
|
||||
sliderView.value = value
|
||||
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
sliderView.backColor = item.theme.list.itemSecondaryTextColor
|
||||
sliderView.trackColor = item.theme.list.itemAccentColor
|
||||
sliderView.knobImage = generateKnobImage()
|
||||
|
||||
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 14.0, y: 22.0 + 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 14.0 * 2.0, height: 44.0))
|
||||
}
|
||||
self.view.addSubview(sliderView)
|
||||
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
|
||||
self.sliderView = sliderView
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: AutodownloadSizeLimitItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let currentItem = self.item
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
|
||||
return { item, params, neighbors in
|
||||
var themeUpdated = false
|
||||
if currentItem?.theme !== item.theme {
|
||||
themeUpdated = true
|
||||
}
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
contentSize = CGSize(width: params.width, height: 60.0 + 22.0)
|
||||
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.layoutParams = params
|
||||
|
||||
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
strongSelf.topStripeNode.isHidden = false
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = params.leftInset + 16.0
|
||||
bottomStripeOffset = -separatorHeight
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
}
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
|
||||
let _ = textApply()
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.size.width) / 2.0), y: 9.0), size: textLayout.size)
|
||||
|
||||
if let sliderView = strongSelf.sliderView {
|
||||
if themeUpdated {
|
||||
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
sliderView.backColor = item.theme.list.itemSecondaryTextColor
|
||||
sliderView.trackColor = item.theme.list.itemAccentColor
|
||||
sliderView.knobImage = generateKnobImage()
|
||||
}
|
||||
|
||||
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 14.0, y: 22.0 + 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 14.0 * 2.0, height: 44.0))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 sliderValueChanged() {
|
||||
guard let sliderView = self.sliderView else {
|
||||
return
|
||||
}
|
||||
let index = Int(sliderView.value)
|
||||
let value: Int32
|
||||
if index >= 0 && index < autodownloadSizeValues.count {
|
||||
value = autodownloadSizeValues[index]
|
||||
} else {
|
||||
value = autodownloadSizeValues[0]
|
||||
}
|
||||
self.item?.updated(value)
|
||||
}
|
||||
}
|
||||
|
@ -3,160 +3,130 @@ import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
public struct AutomaticMediaDownloadCategoryPeers: PostboxCoding, Equatable {
|
||||
public let privateChats: Bool
|
||||
public let groupsAndChannels: Bool
|
||||
public struct AutomaticMediaDownloadCategory: PostboxCoding, Equatable {
|
||||
public var cellular: Bool
|
||||
public var wifi: Bool
|
||||
public var sizeLimit: Int32
|
||||
|
||||
public init(privateChats: Bool, groupsAndChannels: Bool) {
|
||||
self.privateChats = privateChats
|
||||
self.groupsAndChannels = groupsAndChannels
|
||||
public init(cellular: Bool, wifi: Bool, sizeLimit: Int32) {
|
||||
self.cellular = cellular
|
||||
self.wifi = wifi
|
||||
self.sizeLimit = sizeLimit
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.privateChats = decoder.decodeInt32ForKey("p", orElse: 0) != 0
|
||||
self.groupsAndChannels = decoder.decodeInt32ForKey("g", orElse: 0) != 0
|
||||
self.cellular = decoder.decodeInt32ForKey("cellular", orElse: 0) != 0
|
||||
self.wifi = decoder.decodeInt32ForKey("wifi", orElse: 0) != 0
|
||||
self.sizeLimit = decoder.decodeInt32ForKey("sizeLimit", orElse: 0)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.privateChats ? 1 : 0, forKey: "p")
|
||||
encoder.encodeInt32(self.groupsAndChannels ? 1 : 0, forKey: "g")
|
||||
}
|
||||
|
||||
public func withUpdatedPrivateChats(_ privateChats: Bool) -> AutomaticMediaDownloadCategoryPeers {
|
||||
return AutomaticMediaDownloadCategoryPeers(privateChats: privateChats, groupsAndChannels: self.groupsAndChannels)
|
||||
}
|
||||
|
||||
public func withUpdatedGroupsAndChannels(_ groupsAndChannels: Bool) -> AutomaticMediaDownloadCategoryPeers {
|
||||
return AutomaticMediaDownloadCategoryPeers(privateChats: self.privateChats, groupsAndChannels: groupsAndChannels)
|
||||
}
|
||||
|
||||
public static func ==(lhs: AutomaticMediaDownloadCategoryPeers, rhs: AutomaticMediaDownloadCategoryPeers) -> Bool {
|
||||
if lhs.privateChats != rhs.privateChats {
|
||||
return false
|
||||
}
|
||||
if lhs.groupsAndChannels != rhs.groupsAndChannels {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
encoder.encodeInt32(self.cellular ? 1 : 0, forKey: "cellular")
|
||||
encoder.encodeInt32(self.wifi ? 1 : 0, forKey: "wifi")
|
||||
encoder.encodeInt32(self.sizeLimit, forKey: "sizeLimit")
|
||||
}
|
||||
}
|
||||
|
||||
public struct AutomaticMediaDownloadCategories: PostboxCoding, Equatable {
|
||||
public let photo: AutomaticMediaDownloadCategoryPeers
|
||||
public let voice: AutomaticMediaDownloadCategoryPeers
|
||||
public let instantVideo: AutomaticMediaDownloadCategoryPeers
|
||||
public let gif: AutomaticMediaDownloadCategoryPeers
|
||||
public struct AutomaticMediaDownloadCategories: Equatable, PostboxCoding {
|
||||
public var photo: AutomaticMediaDownloadCategory
|
||||
public var video: AutomaticMediaDownloadCategory
|
||||
public var file: AutomaticMediaDownloadCategory
|
||||
public var voiceMessage: AutomaticMediaDownloadCategory
|
||||
public var videoMessage: AutomaticMediaDownloadCategory
|
||||
|
||||
public func getPhoto(_ peerId: PeerId) -> Bool {
|
||||
if peerId.namespace == Namespaces.Peer.SecretChat || peerId.namespace == Namespaces.Peer.CloudUser {
|
||||
return self.photo.privateChats
|
||||
} else {
|
||||
return self.photo.groupsAndChannels
|
||||
}
|
||||
}
|
||||
|
||||
public func getVoice(_ peerId: PeerId) -> Bool {
|
||||
if peerId.namespace == Namespaces.Peer.SecretChat || peerId.namespace == Namespaces.Peer.CloudUser {
|
||||
return self.voice.privateChats
|
||||
} else {
|
||||
return self.voice.groupsAndChannels
|
||||
}
|
||||
}
|
||||
|
||||
public func getInstantVideo(_ peerId: PeerId) -> Bool {
|
||||
if peerId.namespace == Namespaces.Peer.SecretChat || peerId.namespace == Namespaces.Peer.CloudUser {
|
||||
return self.instantVideo.privateChats
|
||||
} else {
|
||||
return self.instantVideo.groupsAndChannels
|
||||
}
|
||||
}
|
||||
|
||||
public func getGif(_ peerId: PeerId) -> Bool {
|
||||
if peerId.namespace == Namespaces.Peer.SecretChat || peerId.namespace == Namespaces.Peer.CloudUser {
|
||||
return self.gif.privateChats
|
||||
} else {
|
||||
return self.gif.groupsAndChannels
|
||||
}
|
||||
}
|
||||
|
||||
public init(photo: AutomaticMediaDownloadCategoryPeers, voice: AutomaticMediaDownloadCategoryPeers, instantVideo: AutomaticMediaDownloadCategoryPeers, gif: AutomaticMediaDownloadCategoryPeers) {
|
||||
public init(photo: AutomaticMediaDownloadCategory, video: AutomaticMediaDownloadCategory, file: AutomaticMediaDownloadCategory, voiceMessage: AutomaticMediaDownloadCategory, videoMessage: AutomaticMediaDownloadCategory) {
|
||||
self.photo = photo
|
||||
self.voice = voice
|
||||
self.instantVideo = instantVideo
|
||||
self.gif = gif
|
||||
self.video = video
|
||||
self.file = file
|
||||
self.voiceMessage = voiceMessage
|
||||
self.videoMessage = videoMessage
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.photo = decoder.decodeObjectForKey("p", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers
|
||||
self.voice = decoder.decodeObjectForKey("v", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers
|
||||
self.instantVideo = decoder.decodeObjectForKey("iv", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers
|
||||
self.gif = decoder.decodeObjectForKey("g", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers
|
||||
self.photo = decoder.decodeObjectForKey("photo", decoder: AutomaticMediaDownloadCategory.init(decoder:)) as! AutomaticMediaDownloadCategory
|
||||
self.video = decoder.decodeObjectForKey("video", decoder: AutomaticMediaDownloadCategory.init(decoder:)) as! AutomaticMediaDownloadCategory
|
||||
self.file = decoder.decodeObjectForKey("file", decoder: AutomaticMediaDownloadCategory.init(decoder:)) as! AutomaticMediaDownloadCategory
|
||||
self.voiceMessage = decoder.decodeObjectForKey("voiceMessage", decoder: AutomaticMediaDownloadCategory.init(decoder:)) as! AutomaticMediaDownloadCategory
|
||||
self.videoMessage = decoder.decodeObjectForKey("videoMessage", decoder: AutomaticMediaDownloadCategory.init(decoder:)) as! AutomaticMediaDownloadCategory
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObject(self.photo, forKey: "p")
|
||||
encoder.encodeObject(self.voice, forKey: "v")
|
||||
encoder.encodeObject(self.instantVideo, forKey: "iv")
|
||||
encoder.encodeObject(self.gif, forKey: "g")
|
||||
encoder.encodeObject(self.photo, forKey: "photo")
|
||||
encoder.encodeObject(self.video, forKey: "video")
|
||||
encoder.encodeObject(self.file, forKey: "file")
|
||||
encoder.encodeObject(self.voiceMessage, forKey: "voiceMessage")
|
||||
encoder.encodeObject(self.videoMessage, forKey: "videoMessage")
|
||||
}
|
||||
}
|
||||
|
||||
public struct AutomaticMediaDownloadPeers: Equatable, PostboxCoding {
|
||||
public var contacts: AutomaticMediaDownloadCategories
|
||||
public var otherPrivate: AutomaticMediaDownloadCategories
|
||||
public var groups: AutomaticMediaDownloadCategories
|
||||
public var channels: AutomaticMediaDownloadCategories
|
||||
|
||||
public init(contacts: AutomaticMediaDownloadCategories, otherPrivate: AutomaticMediaDownloadCategories, groups: AutomaticMediaDownloadCategories, channels: AutomaticMediaDownloadCategories) {
|
||||
self.contacts = contacts
|
||||
self.otherPrivate = otherPrivate
|
||||
self.groups = groups
|
||||
self.channels = channels
|
||||
}
|
||||
|
||||
public func withUpdatedPhoto(_ photo: AutomaticMediaDownloadCategoryPeers) -> AutomaticMediaDownloadCategories {
|
||||
return AutomaticMediaDownloadCategories(photo: photo, voice: self.voice, instantVideo: self.instantVideo, gif: self.gif)
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.contacts = decoder.decodeObjectForKey("contacts", decoder: AutomaticMediaDownloadCategories.init(decoder:)) as! AutomaticMediaDownloadCategories
|
||||
self.otherPrivate = decoder.decodeObjectForKey("otherPrivate", decoder: AutomaticMediaDownloadCategories.init(decoder:)) as! AutomaticMediaDownloadCategories
|
||||
self.groups = decoder.decodeObjectForKey("groups", decoder: AutomaticMediaDownloadCategories.init(decoder:)) as! AutomaticMediaDownloadCategories
|
||||
self.channels = decoder.decodeObjectForKey("channels", decoder: AutomaticMediaDownloadCategories.init(decoder:)) as! AutomaticMediaDownloadCategories
|
||||
}
|
||||
|
||||
public func withUpdatedVoice(_ voice: AutomaticMediaDownloadCategoryPeers) -> AutomaticMediaDownloadCategories {
|
||||
return AutomaticMediaDownloadCategories(photo: self.photo, voice: voice, instantVideo: self.instantVideo, gif: self.gif)
|
||||
}
|
||||
|
||||
public func withUpdatedInstantVideo(_ instantVideo: AutomaticMediaDownloadCategoryPeers) -> AutomaticMediaDownloadCategories {
|
||||
return AutomaticMediaDownloadCategories(photo: self.photo, voice: self.voice, instantVideo: instantVideo, gif: self.gif)
|
||||
}
|
||||
|
||||
public func withUpdatedGif(_ gif: AutomaticMediaDownloadCategoryPeers) -> AutomaticMediaDownloadCategories {
|
||||
return AutomaticMediaDownloadCategories(photo: self.photo, voice: self.voice, instantVideo: self.instantVideo, gif: gif)
|
||||
}
|
||||
|
||||
public static func ==(lhs: AutomaticMediaDownloadCategories, rhs: AutomaticMediaDownloadCategories) -> Bool {
|
||||
if lhs.photo != rhs.photo {
|
||||
return false
|
||||
}
|
||||
if lhs.voice != rhs.voice {
|
||||
return false
|
||||
}
|
||||
if lhs.instantVideo != rhs.instantVideo {
|
||||
return false
|
||||
}
|
||||
if lhs.gif != rhs.gif {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObject(self.contacts, forKey: "contacts")
|
||||
encoder.encodeObject(self.otherPrivate, forKey: "otherPrivate")
|
||||
encoder.encodeObject(self.groups, forKey: "groups")
|
||||
encoder.encodeObject(self.channels, forKey: "channels")
|
||||
}
|
||||
}
|
||||
|
||||
public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable {
|
||||
public let categories: AutomaticMediaDownloadCategories
|
||||
public let saveIncomingPhotos: Bool
|
||||
public var masterEnabled: Bool
|
||||
public var peers: AutomaticMediaDownloadPeers
|
||||
public var autoplayGifs: Bool
|
||||
public var saveIncomingPhotos: Bool
|
||||
|
||||
public static var defaultSettings: AutomaticMediaDownloadSettings {
|
||||
return AutomaticMediaDownloadSettings(categories: AutomaticMediaDownloadCategories(photo: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupsAndChannels: true), voice: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupsAndChannels: true), instantVideo: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupsAndChannels: true), gif: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupsAndChannels: true)), saveIncomingPhotos: false)
|
||||
let defaultCategory = AutomaticMediaDownloadCategories(
|
||||
photo: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: Int32.max),
|
||||
video: AutomaticMediaDownloadCategory(cellular: false, wifi: false, sizeLimit: 1 * 1024 * 1024),
|
||||
file: AutomaticMediaDownloadCategory(cellular: false, wifi: false, sizeLimit: 1 * 1024 * 1024),
|
||||
voiceMessage: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: Int32.max),
|
||||
videoMessage: AutomaticMediaDownloadCategory(cellular: false, wifi: false, sizeLimit: Int32.max)
|
||||
)
|
||||
return AutomaticMediaDownloadSettings(masterEnabled: true, peers: AutomaticMediaDownloadPeers(
|
||||
contacts: defaultCategory,
|
||||
otherPrivate: defaultCategory,
|
||||
groups: defaultCategory,
|
||||
channels: defaultCategory
|
||||
), autoplayGifs: true, saveIncomingPhotos: false)
|
||||
}
|
||||
|
||||
public static var none: AutomaticMediaDownloadSettings {
|
||||
return AutomaticMediaDownloadSettings(categories: AutomaticMediaDownloadCategories(photo: AutomaticMediaDownloadCategoryPeers(privateChats: false, groupsAndChannels: false), voice: AutomaticMediaDownloadCategoryPeers(privateChats: false, groupsAndChannels: false), instantVideo: AutomaticMediaDownloadCategoryPeers(privateChats: false, groupsAndChannels: false), gif: AutomaticMediaDownloadCategoryPeers(privateChats: false, groupsAndChannels: false)), saveIncomingPhotos: false)
|
||||
}
|
||||
|
||||
init(categories: AutomaticMediaDownloadCategories, saveIncomingPhotos: Bool) {
|
||||
self.categories = categories
|
||||
init(masterEnabled: Bool, peers: AutomaticMediaDownloadPeers, autoplayGifs: Bool, saveIncomingPhotos: Bool) {
|
||||
self.masterEnabled = masterEnabled
|
||||
self.peers = peers
|
||||
self.autoplayGifs = autoplayGifs
|
||||
self.saveIncomingPhotos = saveIncomingPhotos
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.categories = decoder.decodeObjectForKey("c", decoder: { AutomaticMediaDownloadCategories(decoder: $0) }) as! AutomaticMediaDownloadCategories
|
||||
self.masterEnabled = decoder.decodeInt32ForKey("masterEnabled", orElse: 1) != 0
|
||||
self.peers = (decoder.decodeObjectForKey("peers", decoder: AutomaticMediaDownloadPeers.init(decoder:)) as? AutomaticMediaDownloadPeers) ?? AutomaticMediaDownloadSettings.defaultSettings.peers
|
||||
self.autoplayGifs = decoder.decodeInt32ForKey("autoplayGifs", orElse: 1) != 0
|
||||
self.saveIncomingPhotos = decoder.decodeInt32ForKey("siph", orElse: 0) != 0
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObject(self.categories, forKey: "c")
|
||||
encoder.encodeInt32(self.masterEnabled ? 1 : 0, forKey: "autoplayGifs")
|
||||
encoder.encodeObject(self.peers, forKey: "peers")
|
||||
encoder.encodeInt32(self.autoplayGifs ? 1 : 0, forKey: "autoplayGifs")
|
||||
encoder.encodeInt32(self.saveIncomingPhotos ? 1 : 0, forKey: "siph")
|
||||
}
|
||||
|
||||
@ -167,18 +137,6 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: AutomaticMediaDownloadSettings, rhs: AutomaticMediaDownloadSettings) -> Bool {
|
||||
return lhs.categories == rhs.categories && lhs.saveIncomingPhotos == rhs.saveIncomingPhotos
|
||||
}
|
||||
|
||||
func withUpdatedCategories(_ categories: AutomaticMediaDownloadCategories) -> AutomaticMediaDownloadSettings {
|
||||
return AutomaticMediaDownloadSettings(categories: categories, saveIncomingPhotos: self.saveIncomingPhotos)
|
||||
}
|
||||
|
||||
func withUpdatedSaveIncomingPhotos(_ saveIncomingPhotos: Bool) -> AutomaticMediaDownloadSettings {
|
||||
return AutomaticMediaDownloadSettings(categories: self.categories, saveIncomingPhotos: saveIncomingPhotos)
|
||||
}
|
||||
}
|
||||
|
||||
public func currentAutomaticMediaDownloadSettings(postbox: Postbox) -> Signal<AutomaticMediaDownloadSettings, NoError> {
|
||||
@ -208,3 +166,66 @@ func updateMediaDownloadSettingsInteractively(postbox: Postbox, _ f: @escaping (
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func categoriesForPeer(_ peer: Peer, settings: AutomaticMediaDownloadSettings) -> AutomaticMediaDownloadCategories {
|
||||
if let _ = peer as? TelegramUser {
|
||||
return settings.peers.contacts
|
||||
} else if let _ = peer as? TelegramSecretChat {
|
||||
return settings.peers.contacts
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if case .broadcast = channel.info {
|
||||
return settings.peers.channels
|
||||
} else {
|
||||
return settings.peers.groups
|
||||
}
|
||||
} else {
|
||||
return settings.peers.channels
|
||||
}
|
||||
}
|
||||
|
||||
private func categoryForPeerAndMedia(settings: AutomaticMediaDownloadSettings, peer: Peer, media: Media) -> (AutomaticMediaDownloadCategory, Int32?)? {
|
||||
let categories = categoriesForPeer(peer, settings: settings)
|
||||
if let _ = media as? TelegramMediaImage {
|
||||
return (categories.photo, nil)
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
for attribute in file.attributes {
|
||||
switch attribute {
|
||||
case let .Video(_, _, flags):
|
||||
if flags.contains(.instantRoundVideo) {
|
||||
return (categories.videoMessage, file.size.flatMap(Int32.init))
|
||||
} else {
|
||||
return (categories.video, file.size.flatMap(Int32.init))
|
||||
}
|
||||
case let .Audio(isVoice, _, _, _, _):
|
||||
if isVoice {
|
||||
return (categories.voiceMessage, file.size.flatMap(Int32.init))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return (categories.file, file.size.flatMap(Int32.init))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func shouldDownloadMediaAutomatically(settings: AutomaticMediaDownloadSettings, peer: Peer?, media: Media) -> Bool {
|
||||
if !settings.masterEnabled {
|
||||
return false
|
||||
}
|
||||
guard let peer = peer else {
|
||||
return false
|
||||
}
|
||||
if let (category, size) = categoryForPeerAndMedia(settings: settings, peer: peer, media: media) {
|
||||
if let size = size {
|
||||
return category.cellular && size <= category.sizeLimit
|
||||
} else if category.sizeLimit == Int32.max {
|
||||
return category.cellular
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -922,7 +922,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr
|
||||
}
|
||||
aboutLinkActionImpl = { [weak controller] action, itemLink in
|
||||
if let controller = controller {
|
||||
handlePeerInfoAboutTextAction(account: account, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
|
||||
handlePeerInfoAboutTextAction(account: account, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
|
||||
}
|
||||
}
|
||||
return controller
|
||||
|
@ -16,23 +16,7 @@ public enum ChatControllerPeekActions {
|
||||
public enum ChatControllerPresentationMode: Equatable {
|
||||
case standard(previewing: Bool)
|
||||
case overlay
|
||||
|
||||
public static func ==(lhs: ChatControllerPresentationMode, rhs: ChatControllerPresentationMode) -> Bool {
|
||||
switch lhs {
|
||||
case let .standard(previewing):
|
||||
if case .standard(previewing) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .overlay:
|
||||
if case .overlay = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
case inline
|
||||
}
|
||||
|
||||
public final class ChatControllerOverlayPresentationData {
|
||||
@ -206,12 +190,18 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin
|
||||
|
||||
self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, fontSize: self.presentationData.fontSize, accountPeerId: account.peerId, mode: mode, chatLocation: chatLocation)
|
||||
|
||||
|
||||
var enableMediaAccessoryPanel = true
|
||||
if case .overlay = mode {
|
||||
enableMediaAccessoryPanel = false
|
||||
var enableMediaAccessoryPanel = false
|
||||
if case .standard = mode {
|
||||
enableMediaAccessoryPanel = true
|
||||
}
|
||||
super.init(account: account, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), enableMediaAccessoryPanel: enableMediaAccessoryPanel, locationBroadcastPanelSource: locationBroadcastPanelSource)
|
||||
let navigationBarPresentationData: NavigationBarPresentationData?
|
||||
switch mode {
|
||||
case .inline:
|
||||
navigationBarPresentationData = nil
|
||||
default:
|
||||
navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData)
|
||||
}
|
||||
super.init(account: account, navigationBarPresentationData: navigationBarPresentationData, enableMediaAccessoryPanel: enableMediaAccessoryPanel, locationBroadcastPanelSource: locationBroadcastPanelSource)
|
||||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
@ -522,10 +512,36 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin
|
||||
openChatInstantPage(account: strongSelf.account, message: message, navigationController: navigationController)
|
||||
}
|
||||
}, openHashtag: { [weak self] peerName, hashtag in
|
||||
if let strongSelf = self, !hashtag.isEmpty {
|
||||
let searchController = HashtagSearchController(account: strongSelf.account, peerName: peerName, query: hashtag)
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(searchController)
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.resolvePeerByNameDisposable == nil {
|
||||
strongSelf.resolvePeerByNameDisposable = MetaDisposable()
|
||||
}
|
||||
let resolveSignal: Signal<Peer?, NoError>
|
||||
if let peerName = peerName {
|
||||
resolveSignal = resolvePeerByName(account: strongSelf.account, name: peerName)
|
||||
|> mapToSignal { peerId -> Signal<Peer?, NoError> in
|
||||
if let peerId = peerId {
|
||||
return account.postbox.loadedPeerWithId(peerId)
|
||||
|> map(Optional.init)
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
} else if case let .peer(peerId) = strongSelf.chatLocation {
|
||||
resolveSignal = account.postbox.loadedPeerWithId(peerId)
|
||||
|> map(Optional.init)
|
||||
} else {
|
||||
resolveSignal = .single(nil)
|
||||
}
|
||||
strongSelf.resolvePeerByNameDisposable?.set((resolveSignal
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
if let strongSelf = self, !hashtag.isEmpty {
|
||||
let searchController = HashtagSearchController(account: strongSelf.account, peer: peer, query: hashtag)
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(searchController)
|
||||
}
|
||||
}))
|
||||
}, updateInputState: { [weak self] f in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
@ -690,8 +706,20 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
let searchController = HashtagSearchController(account: strongSelf.account, peerName: nil, query: hashtag)
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(searchController)
|
||||
let peerSignal: Signal<Peer?, NoError>
|
||||
if case let .peer(peerId) = strongSelf.chatLocation {
|
||||
peerSignal = strongSelf.account.postbox.loadedPeerWithId(peerId)
|
||||
|> map(Optional.init)
|
||||
} else {
|
||||
peerSignal = .single(nil)
|
||||
}
|
||||
let _ = (peerSignal
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
if let strongSelf = self {
|
||||
let searchController = HashtagSearchController(account: strongSelf.account, peer: peer, query: hashtag)
|
||||
(strongSelf.navigationController as? NavigationController)?.pushViewController(searchController)
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
@ -1125,7 +1153,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = ChatControllerNode(account: self.account, chatLocation: self.chatLocation, messageId: self.messageId, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar!)
|
||||
self.displayNode = ChatControllerNode(account: self.account, chatLocation: self.chatLocation, messageId: self.messageId, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar)
|
||||
|
||||
self.chatDisplayNode.peerView = self.peerView
|
||||
|
||||
@ -1789,7 +1817,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin
|
||||
}
|
||||
}))
|
||||
}
|
||||
}, beginMessageSearch: { [weak self] domain in
|
||||
}, beginMessageSearch: { [weak self] domain, query in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
|
||||
return current.updatedTitlePanelContext {
|
||||
@ -1807,7 +1835,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin
|
||||
} else {
|
||||
return $0
|
||||
}
|
||||
}.updatedSearch(current.search == nil ? ChatSearchData(domain: domain) : current.search?.withUpdatedDomain(domain).withUpdatedQuery(""))
|
||||
}.updatedSearch(current.search == nil ? ChatSearchData(domain: domain).withUpdatedQuery(query) : current.search?.withUpdatedDomain(domain).withUpdatedQuery(query))
|
||||
})
|
||||
}
|
||||
}, dismissMessageSearch: { [weak self] in
|
||||
@ -2730,6 +2758,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin
|
||||
case .overlay:
|
||||
self.statusBar.statusBarStyle = .Hide
|
||||
self.deferScreenEdgeGestures = [.top]
|
||||
case .inline:
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
}
|
||||
}
|
||||
|
||||
@ -2802,7 +2832,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin
|
||||
}
|
||||
}
|
||||
case .search:
|
||||
self.interfaceInteraction?.beginMessageSearch(.everything)
|
||||
self.interfaceInteraction?.beginMessageSearch(.everything, "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -4091,4 +4121,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin
|
||||
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) {
|
||||
self.chatDisplayNode.updateDropInteraction(isActive: false)
|
||||
}
|
||||
|
||||
public func beginMessageSearch(_ query: String) {
|
||||
self.interfaceInteraction?.beginMessageSearch(.everything, query)
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
let chatLocation: ChatLocation
|
||||
let controllerInteraction: ChatControllerInteraction
|
||||
|
||||
let navigationBar: NavigationBar
|
||||
let navigationBar: NavigationBar?
|
||||
|
||||
private var backgroundEffectNode: ASDisplayNode?
|
||||
private var containerBackgroundNode: ASImageNode?
|
||||
@ -141,7 +141,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
init(account: Account, chatLocation: ChatLocation, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings, navigationBar: NavigationBar) {
|
||||
init(account: Account, chatLocation: ChatLocation, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings, navigationBar: NavigationBar?) {
|
||||
self.account = account
|
||||
self.chatLocation = chatLocation
|
||||
self.controllerInteraction = controllerInteraction
|
||||
@ -372,7 +372,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
self.containerNode = containerNode
|
||||
self.scrollContainerNode?.addSubnode(containerNode)
|
||||
self.navigationBar.isHidden = true
|
||||
self.navigationBar?.isHidden = true
|
||||
}
|
||||
if self.overlayNavigationBar == nil {
|
||||
let overlayNavigationBar = ChatOverlayNavigationBar(theme: self.chatPresentationInterfaceState.theme, close: { [weak self] in
|
||||
@ -399,7 +399,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
if let restrictedNode = self.restrictedNode {
|
||||
self.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer)
|
||||
}
|
||||
self.navigationBar.isHidden = false
|
||||
self.navigationBar?.isHidden = false
|
||||
}
|
||||
if let overlayNavigationBar = self.overlayNavigationBar {
|
||||
overlayNavigationBar.removeFromSupernode()
|
||||
@ -449,14 +449,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
activate = true
|
||||
self.searchNavigationNode = ChatSearchNavigationContentNode(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, chatLocation: self.chatPresentationInterfaceState.chatLocation, interaction: interfaceInteraction)
|
||||
}
|
||||
self.navigationBar.setContentNode(self.searchNavigationNode, animated: transitionIsAnimated)
|
||||
self.navigationBar?.setContentNode(self.searchNavigationNode, animated: transitionIsAnimated)
|
||||
self.searchNavigationNode?.update(presentationInterfaceState: self.chatPresentationInterfaceState)
|
||||
if activate {
|
||||
self.searchNavigationNode?.activate()
|
||||
}
|
||||
} else if let _ = self.searchNavigationNode {
|
||||
self.searchNavigationNode = nil
|
||||
self.navigationBar.setContentNode(nil, animated: transitionIsAnimated)
|
||||
self.navigationBar?.setContentNode(nil, animated: transitionIsAnimated)
|
||||
}
|
||||
|
||||
var dismissedTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode?
|
||||
@ -1561,7 +1561,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let _ = self.messageActionSheetController {
|
||||
self.displayMessageActionSheet(stableId: nil, sheetActions: nil, displayContextMenuController: nil)
|
||||
return self.navigationBar.view
|
||||
return self.navigationBar?.view
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -49,6 +49,10 @@ class ChatDocumentGalleryItem: GalleryItem {
|
||||
node.setMessage(self.message)
|
||||
}
|
||||
}
|
||||
|
||||
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private let registeredURLProtocol: Void = {
|
||||
|
@ -49,6 +49,10 @@ class ChatExternalFileGalleryItem: GalleryItem {
|
||||
node.setMessage(self.message)
|
||||
}
|
||||
}
|
||||
|
||||
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
class ChatExternalFileGalleryItemNode: GalleryItemNode {
|
||||
@ -326,4 +330,3 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,69 @@ import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
private enum ChatMediaGalleryThumbnail: Equatable {
|
||||
case image(TelegramMediaImage)
|
||||
case video(TelegramMediaFile)
|
||||
|
||||
static func ==(lhs: ChatMediaGalleryThumbnail, rhs: ChatMediaGalleryThumbnail) -> Bool {
|
||||
switch lhs {
|
||||
case let .image(lhsImage):
|
||||
if case let .image(rhsImage) = rhs, lhsImage.isEqual(rhsImage) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .video(lhsVideo):
|
||||
if case let .video(rhsVideo) = rhs, lhsVideo.isEqual(rhsVideo) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatMediaGalleryThumbnailItem: GalleryThumbnailItem {
|
||||
private let account: Account
|
||||
private let thumbnail: ChatMediaGalleryThumbnail
|
||||
|
||||
init?(account: Account, media: Media) {
|
||||
self.account = account
|
||||
if let media = media as? TelegramMediaImage {
|
||||
self.thumbnail = .image(media)
|
||||
} else if let media = media as? TelegramMediaFile, media.isVideo {
|
||||
self.thumbnail = .video(media)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func isEqual(to: GalleryThumbnailItem) -> Bool {
|
||||
if let to = to as? ChatMediaGalleryThumbnailItem {
|
||||
return self.thumbnail == to.thumbnail
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var image: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) {
|
||||
switch self.thumbnail {
|
||||
case let .image(image):
|
||||
if let representation = largestImageRepresentation(image.representations) {
|
||||
return (mediaGridMessagePhoto(account: self.account, photo: image), representation.dimensions)
|
||||
} else {
|
||||
return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0))
|
||||
}
|
||||
case let .video(file):
|
||||
if let representation = largestImageRepresentation(file.previewRepresentations) {
|
||||
return (mediaGridMessageVideo(postbox: self.account.postbox, video: file), representation.dimensions)
|
||||
} else {
|
||||
return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChatImageGalleryItem: GalleryItem {
|
||||
let account: Account
|
||||
let theme: PresentationTheme
|
||||
@ -57,6 +120,25 @@ class ChatImageGalleryItem: GalleryItem {
|
||||
node.setMessage(self.message)
|
||||
}
|
||||
}
|
||||
|
||||
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
|
||||
if let id = self.message.groupInfo?.stableId {
|
||||
var media: Media?
|
||||
for m in self.message.media {
|
||||
if let m = m as? TelegramMediaImage {
|
||||
media = m
|
||||
} else if let m = m as? TelegramMediaFile, m.isVideo {
|
||||
media = m
|
||||
}
|
||||
}
|
||||
if let media = media {
|
||||
if let item = ChatMediaGalleryThumbnailItem(account: self.account, media: media) {
|
||||
return (Int64(id), item)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
@ -207,7 +207,7 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
case .unmute:
|
||||
self.interfaceInteraction?.togglePeerNotifications()
|
||||
case .search:
|
||||
self.interfaceInteraction?.beginMessageSearch(.everything)
|
||||
self.interfaceInteraction?.beginMessageSearch(.everything, "")
|
||||
case .call:
|
||||
self.interfaceInteraction?.beginCall()
|
||||
case .report:
|
||||
|
@ -13,6 +13,8 @@ private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult
|
||||
return 3
|
||||
case .contextRequestResult:
|
||||
return 4
|
||||
case .emojis:
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +52,16 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa
|
||||
panel.updateResults(results)
|
||||
return panel
|
||||
}
|
||||
case let .emojis(results):
|
||||
if let currentPanel = currentPanel as? EmojisChatInputContextPanelNode {
|
||||
currentPanel.updateResults(results)
|
||||
return currentPanel
|
||||
} else {
|
||||
let panel = EmojisChatInputContextPanelNode(account: account, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
|
||||
panel.interfaceInteraction = interfaceInteraction
|
||||
panel.updateResults(results)
|
||||
return panel
|
||||
}
|
||||
case let .mentions(peers):
|
||||
if !peers.isEmpty {
|
||||
if let currentPanel = currentPanel as? MentionChatInputContextPanelNode, currentPanel.mode == .input {
|
||||
|
@ -19,6 +19,7 @@ struct PossibleContextQueryTypes: OptionSet {
|
||||
static let mention = PossibleContextQueryTypes(rawValue: (1 << 2))
|
||||
static let command = PossibleContextQueryTypes(rawValue: (1 << 3))
|
||||
static let contextRequest = PossibleContextQueryTypes(rawValue: (1 << 4))
|
||||
static let stickerSearch = PossibleContextQueryTypes(rawValue: (1 << 5))
|
||||
}
|
||||
|
||||
private func makeScalar(_ c: Character) -> Character {
|
||||
@ -30,6 +31,7 @@ private let newlineScalar = "\n" as UnicodeScalar
|
||||
private let hashScalar = "#" as UnicodeScalar
|
||||
private let atScalar = "@" as UnicodeScalar
|
||||
private let slashScalar = "/" as UnicodeScalar
|
||||
private let dotsScalar = ":" as UnicodeScalar
|
||||
private let alphanumerics = CharacterSet.alphanumerics
|
||||
|
||||
func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
|
||||
@ -92,7 +94,7 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) ->
|
||||
return [(NSRange(location: 0, length: inputLength), [.emoji], nil)]
|
||||
}
|
||||
|
||||
var possibleTypes = PossibleContextQueryTypes([.command, .mention, .hashtag])
|
||||
var possibleTypes = PossibleContextQueryTypes([.command, .mention, .hashtag, .stickerSearch])
|
||||
var definedType = false
|
||||
|
||||
while true {
|
||||
@ -117,6 +119,12 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) ->
|
||||
index += 1
|
||||
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||
break
|
||||
} else if c == dotsScalar {
|
||||
possibleTypes = possibleTypes.intersection([.stickerSearch])
|
||||
definedType = true
|
||||
index += 1
|
||||
possibleQueryRange = NSRange(location: index, length: maxIndex - index)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,6 +164,8 @@ func inputContextQueriesForChatPresentationIntefaceState(_ chatPresentationInter
|
||||
} else if possibleTypes == [.contextRequest], let additionalStringRange = additionalStringRange {
|
||||
let additionalString = inputString.substring(with: additionalStringRange)
|
||||
result.append(.contextRequest(addressName: query, query: additionalString))
|
||||
} else if possibleTypes == [.stickerSearch], !query.isEmpty {
|
||||
result.append(.stickerSearch(query))
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
@ -3,6 +3,9 @@ import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
|
||||
import TelegramUIPrivateModule
|
||||
import LegacyComponents
|
||||
|
||||
enum ChatContextQueryUpdate {
|
||||
case remove
|
||||
case update(ChatPresentationInputQuery, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>)
|
||||
@ -230,6 +233,24 @@ private func updatedContextQueryResultStateForQuery(account: Account, peer: Peer
|
||||
}
|
||||
|
||||
return signal |> then(contextBot)
|
||||
case let .stickerSearch(query):
|
||||
let foundEmojis: Signal<[(String, String)], NoError> = Signal { subscriber in
|
||||
var result: [(String, String)] = []
|
||||
for entry in TGEmojiSuggestions.suggestions(forQuery: query.lowercased()) {
|
||||
if let entry = entry as? TGAlphacodeEntry {
|
||||
result.append((entry.emoji, entry.code))
|
||||
}
|
||||
}
|
||||
subscriber.putNext(result)
|
||||
subscriber.putCompletion()
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let emojis: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = foundEmojis |> map { result -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
return { _ in return .emojis(result) }
|
||||
}
|
||||
|
||||
return emojis
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,6 +186,10 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
|
||||
}
|
||||
}
|
||||
|
||||
if case .inline = chatPresentationInterfaceState.mode {
|
||||
displayInputTextPanel = false
|
||||
}
|
||||
|
||||
if displayInputTextPanel {
|
||||
if let currentPanel = currentPanel as? ChatTextInputPanelNode {
|
||||
currentPanel.interfaceInteraction = interfaceInteraction
|
||||
|
@ -119,10 +119,15 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
|
||||
self.actionButton.setImage(actionImage, for: [.normal])
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.isLayerBacked = true
|
||||
self.authorNameNode = ASTextNode()
|
||||
self.authorNameNode.maximumNumberOfLines = 1
|
||||
self.authorNameNode.isLayerBacked = true
|
||||
self.authorNameNode.displaysAsynchronously = false
|
||||
self.dateNode = ASTextNode()
|
||||
self.dateNode.maximumNumberOfLines = 1
|
||||
self.dateNode.isLayerBacked = true
|
||||
self.dateNode.displaysAsynchronously = false
|
||||
|
||||
self.playbackControlButton = HighlightableButtonNode()
|
||||
self.playbackControlButton.isHidden = true
|
||||
@ -248,36 +253,59 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
var panelHeight: CGFloat = 44.0 + bottomInset
|
||||
panelHeight += contentInset
|
||||
if !self.textNode.isHidden {
|
||||
let sideInset: CGFloat = 8.0 + leftInset
|
||||
let topInset: CGFloat = 8.0
|
||||
let textBottomInset: CGFloat = 8.0
|
||||
let textBottomInset: CGFloat = 8.0 + contentInset
|
||||
let textSize = self.textNode.measure(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
panelHeight += textSize.height + topInset + textBottomInset
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: sideInset, y: topInset), size: textSize))
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: textSize)
|
||||
}
|
||||
|
||||
transition.updateFrame(view: self.actionButton, frame: CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)))
|
||||
transition.updateFrame(view: self.deleteButton, frame: CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)))
|
||||
self.actionButton.frame = CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
|
||||
self.deleteButton.frame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
|
||||
|
||||
transition.updateFrame(node: self.playbackControlButton, frame: CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)))
|
||||
self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
|
||||
|
||||
let authorNameSize = self.authorNameNode.measure(CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude))
|
||||
let dateSize = self.dateNode.measure(CGSize(width: width - 44.0 * 2.0 - 8.0 * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
|
||||
if authorNameSize.height.isZero {
|
||||
transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height) / 2.0)), size: dateSize))
|
||||
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height) / 2.0)), size: dateSize)
|
||||
} else {
|
||||
let labelsSpacing: CGFloat = 0.0
|
||||
transition.updateFrame(node: self.authorNameNode, frame: CGRect(origin: CGPoint(x: floor((width - authorNameSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0)), size: authorNameSize))
|
||||
transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize))
|
||||
self.authorNameNode.frame = CGRect(origin: CGPoint(x: floor((width - authorNameSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0)), size: authorNameSize)
|
||||
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((width - dateSize.width) / 2.0), y: panelHeight - bottomInset - 44.0 + floor((44.0 - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize)
|
||||
}
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
|
||||
override func animateIn(fromHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: 0.0, y: self.bounds.size.height - fromHeight))
|
||||
self.textNode.alpha = 1.0
|
||||
self.dateNode.alpha = 1.0
|
||||
self.authorNameNode.alpha = 1.0
|
||||
self.deleteButton.alpha = 1.0
|
||||
self.actionButton.alpha = 1.0
|
||||
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
|
||||
override func animateOut(toHeight: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
transition.updateFrame(node: self.textNode, frame: self.textNode.frame.offsetBy(dx: 0.0, dy: self.bounds.height - toHeight))
|
||||
self.textNode.alpha = 0.0
|
||||
self.dateNode.alpha = 0.0
|
||||
self.authorNameNode.alpha = 0.0
|
||||
self.deleteButton.alpha = 0.0
|
||||
self.actionButton.alpha = 0.0
|
||||
self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
@objc func deleteButtonPressed() {
|
||||
if let currentMessage = self.currentMessage {
|
||||
let _ = (self.account.postbox.modify { modifier -> [Message] in
|
||||
|
@ -369,19 +369,13 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
if let file = media as? TelegramMediaFile {
|
||||
if file.isVideo {
|
||||
var automaticDownload = false
|
||||
if file.isAnimated {
|
||||
automaticDownload = automaticDownloadSettings.categories.getGif(message.id.peerId)
|
||||
} else if file.isInstantVideo {
|
||||
automaticDownload = automaticDownloadSettings.categories.getInstantVideo(message.id.peerId)
|
||||
}
|
||||
automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peer: message.peers[message.id.peerId], media: file)
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(account, presentationData.theme, presentationData.strings, message, file, automaticDownload, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants)
|
||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
||||
refineContentImageLayout = refineLayout
|
||||
} else {
|
||||
var automaticDownload = false
|
||||
if file.isVoice {
|
||||
automaticDownload = automaticDownloadSettings.categories.getVoice(message.id.peerId)
|
||||
}
|
||||
automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peer: message.peers[message.id.peerId], media: file)
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType
|
||||
if message.effectivelyIncoming(account.peerId) {
|
||||
@ -401,7 +395,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
} else if let image = media as? TelegramMediaImage {
|
||||
if !flags.contains(.preferMediaInline) {
|
||||
let automaticDownload = automaticDownloadSettings.categories.getPhoto(message.id.peerId)
|
||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peer: message.peers[message.id.peerId], media: image)
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(account, presentationData.theme, presentationData.strings, message, image, automaticDownload, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants)
|
||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
||||
refineContentImageLayout = refineLayout
|
||||
@ -413,7 +407,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
} else if let image = media as? TelegramMediaWebFile {
|
||||
let automaticDownload = automaticDownloadSettings.categories.getPhoto(message.id.peerId)
|
||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peer: message.peers[message.id.peerId], media: image)
|
||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(account, presentationData.theme, presentationData.strings, message, image, automaticDownload, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants)
|
||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
||||
refineContentImageLayout = refineLayout
|
||||
|
@ -59,9 +59,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
var automaticDownload = false
|
||||
if selectedFile!.isVoice {
|
||||
automaticDownload = item.controllerInteraction.automaticMediaDownloadSettings.categories.getVoice(item.message.id.peerId)
|
||||
}
|
||||
automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peer: item.message.peers[item.message.id.peerId], media: selectedFile!)
|
||||
|
||||
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))
|
||||
|
||||
|
@ -53,12 +53,10 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
for media in item.message.media {
|
||||
if let telegramImage = media as? TelegramMediaImage {
|
||||
selectedMedia = telegramImage
|
||||
automaticDownload = item.controllerInteraction.automaticMediaDownloadSettings.categories.getPhoto(item.message.id.peerId)
|
||||
automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peer: item.message.peers[item.message.id.peerId], media: telegramImage)
|
||||
} else if let telegramFile = media as? TelegramMediaFile {
|
||||
selectedMedia = telegramFile
|
||||
if telegramFile.isAnimated {
|
||||
automaticDownload = item.controllerInteraction.automaticMediaDownloadSettings.categories.getGif(item.message.id.peerId)
|
||||
}
|
||||
automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peer: item.message.peers[item.message.id.peerId], media: telegramFile)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ final class ChatPanelInterfaceInteraction {
|
||||
let updateTextInputState: (@escaping (ChatTextInputState) -> ChatTextInputState) -> Void
|
||||
let updateInputModeAndDismissedButtonKeyboardMessageId: ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void
|
||||
let editMessage: () -> Void
|
||||
let beginMessageSearch: (ChatSearchDomain) -> Void
|
||||
let beginMessageSearch: (ChatSearchDomain, String) -> Void
|
||||
let dismissMessageSearch: () -> Void
|
||||
let updateMessageSearch: (String) -> Void
|
||||
let navigateMessageSearch: (ChatPanelSearchNavigationAction) -> Void
|
||||
@ -80,7 +80,7 @@ final class ChatPanelInterfaceInteraction {
|
||||
let toggleSilentPost: () -> Void
|
||||
let statuses: ChatPanelInterfaceInteractionStatuses?
|
||||
|
||||
init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, deleteMessages: @escaping ([Message]) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) {
|
||||
init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, deleteMessages: @escaping ([Message]) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) {
|
||||
self.setupReplyMessage = setupReplyMessage
|
||||
self.setupEditMessage = setupEditMessage
|
||||
self.beginMessageSelection = beginMessageSelection
|
||||
|
@ -8,6 +8,7 @@ enum ChatPresentationInputQueryKind: Int32 {
|
||||
case mention
|
||||
case command
|
||||
case contextRequest
|
||||
case stickerSearch
|
||||
}
|
||||
|
||||
struct ChatInputQueryMentionTypes: OptionSet {
|
||||
@ -27,6 +28,7 @@ enum ChatPresentationInputQuery: Hashable, Equatable {
|
||||
case hashtag(String)
|
||||
case mention(query: String, types: ChatInputQueryMentionTypes)
|
||||
case command(String)
|
||||
case stickerSearch(String)
|
||||
case contextRequest(addressName: String, query: String)
|
||||
|
||||
var kind: ChatPresentationInputQueryKind {
|
||||
@ -41,6 +43,8 @@ enum ChatPresentationInputQuery: Hashable, Equatable {
|
||||
return .command
|
||||
case .contextRequest:
|
||||
return .contextRequest
|
||||
case .stickerSearch:
|
||||
return .stickerSearch
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +60,8 @@ enum ChatPresentationInputQuery: Hashable, Equatable {
|
||||
return 4 &+ value.hashValue
|
||||
case let .contextRequest(addressName, query):
|
||||
return 5 &+ addressName.hashValue &* 31 + query.hashValue
|
||||
case let .stickerSearch(value):
|
||||
return 6 &+ value.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,6 +91,12 @@ enum ChatPresentationInputQuery: Hashable, Equatable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .stickerSearch(value):
|
||||
if case .stickerSearch(value) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .contextRequest(addressName, query):
|
||||
if case .contextRequest(addressName, query) = rhs {
|
||||
return true
|
||||
@ -100,6 +112,7 @@ enum ChatPresentationInputQueryResult: Equatable {
|
||||
case hashtags([String])
|
||||
case mentions([Peer])
|
||||
case commands([PeerCommand])
|
||||
case emojis([(String, String)])
|
||||
case contextRequestResult(Peer?, ChatContextResultCollection?)
|
||||
|
||||
static func ==(lhs: ChatPresentationInputQueryResult, rhs: ChatPresentationInputQueryResult) -> Bool {
|
||||
@ -140,6 +153,20 @@ enum ChatPresentationInputQueryResult: Equatable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .emojis(lhsValue):
|
||||
if case let .emojis(rhsValue) = rhs {
|
||||
if lhsValue.count != rhsValue.count {
|
||||
return false
|
||||
}
|
||||
for i in 0 ..< lhsValue.count {
|
||||
if lhsValue[i] != rhsValue[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .contextRequestResult(lhsPeer, lhsCollection):
|
||||
if case let .contextRequestResult(rhsPeer, rhsCollection) = rhs {
|
||||
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
|
||||
|
@ -104,7 +104,7 @@ final class ChatRecentActionsController: ViewController {
|
||||
}, updateTextInputState: { _ in
|
||||
}, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in
|
||||
}, editMessage: {
|
||||
}, beginMessageSearch: { _ in
|
||||
}, beginMessageSearch: { _, _ in
|
||||
}, dismissMessageSearch: {
|
||||
}, updateMessageSearch: { _ in
|
||||
}, navigateMessageSearch: { _ in
|
||||
|
@ -67,6 +67,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
private var enqueuedTransitions: [(ChatRecentActionsHistoryTransition, Bool)] = []
|
||||
|
||||
private var historyDisposable: Disposable?
|
||||
private let resolvePeerByNameDisposable = MetaDisposable()
|
||||
|
||||
init(account: Account, peer: Peer, presentationData: PresentationData, interaction: ChatRecentActionsInteraction, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?) {
|
||||
self.account = account
|
||||
@ -181,10 +182,31 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
openChatInstantPage(account: strongSelf.account, message: message, navigationController: navigationController)
|
||||
}
|
||||
}, openHashtag: { [weak self] peerName, hashtag in
|
||||
if let strongSelf = self, !hashtag.isEmpty {
|
||||
let searchController = HashtagSearchController(account: strongSelf.account, peerName: peerName, query: hashtag)
|
||||
strongSelf.pushController(searchController)
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let resolveSignal: Signal<Peer?, NoError>
|
||||
if let peerName = peerName {
|
||||
resolveSignal = resolvePeerByName(account: strongSelf.account, name: peerName)
|
||||
|> mapToSignal { peerId -> Signal<Peer?, NoError> in
|
||||
if let peerId = peerId {
|
||||
return account.postbox.loadedPeerWithId(peerId)
|
||||
|> map(Optional.init)
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resolveSignal = account.postbox.loadedPeerWithId(strongSelf.peer.id)
|
||||
|> map(Optional.init)
|
||||
}
|
||||
strongSelf.resolvePeerByNameDisposable.set((resolveSignal
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
if let strongSelf = self, !hashtag.isEmpty {
|
||||
let searchController = HashtagSearchController(account: strongSelf.account, peer: peer, query: hashtag)
|
||||
strongSelf.pushController(searchController)
|
||||
}
|
||||
}))
|
||||
}, updateInputState: { _ in }, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action in
|
||||
if let strongSelf = self {
|
||||
@ -296,7 +318,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
let searchController = HashtagSearchController(account: strongSelf.account, peerName: nil, query: hashtag)
|
||||
let searchController = HashtagSearchController(account: strongSelf.account, peer: strongSelf.peer, query: hashtag)
|
||||
strongSelf.pushController(searchController)
|
||||
}
|
||||
}),
|
||||
@ -392,6 +414,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
self.historyDisposable?.dispose()
|
||||
self.navigationActionDisposable.dispose()
|
||||
self.galleryHiddenMesageAndMediaDisposable.dispose()
|
||||
self.resolvePeerByNameDisposable.dispose()
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
|
@ -63,3 +63,9 @@ public extension NavigationControllerTheme {
|
||||
self.init(navigationBar: NavigationBarTheme(rootControllerTheme: presentationTheme), emptyAreaColor: presentationTheme.chatList.backgroundColor, emptyDetailIcon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/EmptyMasterDetailIcon"), color: presentationTheme.chatList.messageTextColor.withAlphaComponent(0.2)))
|
||||
}
|
||||
}
|
||||
|
||||
public extension StatusBarVolumeColors {
|
||||
convenience init(presentationTheme: PresentationTheme) {
|
||||
self.init(background: presentationTheme.rootController.navigationBar.secondaryTextColor, foreground: presentationTheme.rootController.navigationBar.primaryTextColor)
|
||||
}
|
||||
}
|
||||
|
@ -5,16 +5,16 @@ import AsyncDisplayKit
|
||||
struct CounterContollerTitle: Equatable {
|
||||
let title: String
|
||||
let counter: String
|
||||
|
||||
static func ==(lhs: CounterContollerTitle, rhs: CounterContollerTitle) -> Bool {
|
||||
return lhs.title == rhs.title && lhs.counter == rhs.counter
|
||||
}
|
||||
}
|
||||
|
||||
final class CounterContollerTitleView: UIView {
|
||||
private var theme: PresentationTheme
|
||||
private let titleNode: ASTextNode
|
||||
|
||||
func f() {
|
||||
|
||||
}
|
||||
|
||||
var title: CounterContollerTitle = CounterContollerTitle(title: "", counter: "") {
|
||||
didSet {
|
||||
if self.title != oldValue {
|
||||
|
@ -5,33 +5,25 @@ import Postbox
|
||||
import TelegramCore
|
||||
import LegacyComponents
|
||||
|
||||
private enum AutomaticDownloadCategory {
|
||||
case photo
|
||||
case voice
|
||||
case instantVideo
|
||||
case gif
|
||||
}
|
||||
|
||||
private enum AutomaticDownloadPeers {
|
||||
case privateChats
|
||||
case groupsAndChannels
|
||||
}
|
||||
|
||||
private final class DataAndStorageControllerArguments {
|
||||
let openStorageUsage: () -> Void
|
||||
let openNetworkUsage: () -> Void
|
||||
let openProxy: () -> Void
|
||||
let toggleAutomaticDownload: (AutomaticDownloadCategory, AutomaticDownloadPeers, Bool) -> Void
|
||||
let toggleAutomaticDownloadMaster: (Bool) -> Void
|
||||
let openAutomaticDownloadCategory: (AutomaticDownloadCategory) -> Void
|
||||
let resetAutomaticDownload: () -> Void
|
||||
let openVoiceUseLessData: () -> Void
|
||||
let toggleSaveIncomingPhotos: (Bool) -> Void
|
||||
let toggleSaveEditedPhotos: (Bool) -> Void
|
||||
let toggleAutoplayGifs: (Bool) -> Void
|
||||
|
||||
init(openStorageUsage: @escaping () -> Void, openNetworkUsage: @escaping () -> Void, openProxy: @escaping () -> Void, toggleAutomaticDownload: @escaping (AutomaticDownloadCategory, AutomaticDownloadPeers, Bool) -> Void, openVoiceUseLessData: @escaping () -> Void, toggleSaveIncomingPhotos: @escaping (Bool) -> Void, toggleSaveEditedPhotos: @escaping (Bool) -> Void, toggleAutoplayGifs: @escaping (Bool) -> Void) {
|
||||
init(openStorageUsage: @escaping () -> Void, openNetworkUsage: @escaping () -> Void, openProxy: @escaping () -> Void, toggleAutomaticDownloadMaster: @escaping (Bool) -> Void, openAutomaticDownloadCategory: @escaping (AutomaticDownloadCategory) -> Void, resetAutomaticDownload: @escaping () -> Void, openVoiceUseLessData: @escaping () -> Void, toggleSaveIncomingPhotos: @escaping (Bool) -> Void, toggleSaveEditedPhotos: @escaping (Bool) -> Void, toggleAutoplayGifs: @escaping (Bool) -> Void) {
|
||||
self.openStorageUsage = openStorageUsage
|
||||
self.openNetworkUsage = openNetworkUsage
|
||||
self.openProxy = openProxy
|
||||
self.toggleAutomaticDownload = toggleAutomaticDownload
|
||||
self.toggleAutomaticDownloadMaster = toggleAutomaticDownloadMaster
|
||||
self.openAutomaticDownloadCategory = openAutomaticDownloadCategory
|
||||
self.resetAutomaticDownload = resetAutomaticDownload
|
||||
self.openVoiceUseLessData = openVoiceUseLessData
|
||||
self.toggleSaveIncomingPhotos = toggleSaveIncomingPhotos
|
||||
self.toggleSaveEditedPhotos = toggleSaveEditedPhotos
|
||||
@ -41,9 +33,7 @@ private final class DataAndStorageControllerArguments {
|
||||
|
||||
private enum DataAndStorageSection: Int32 {
|
||||
case usage
|
||||
case automaticPhotoDownload
|
||||
case automaticVoiceDownload
|
||||
case automaticInstantVideoDownload
|
||||
case automaticMediaDownload
|
||||
case voiceCalls
|
||||
case other
|
||||
case connection
|
||||
@ -52,15 +42,14 @@ private enum DataAndStorageSection: Int32 {
|
||||
private enum DataAndStorageEntry: ItemListNodeEntry {
|
||||
case storageUsage(PresentationTheme, String)
|
||||
case networkUsage(PresentationTheme, String)
|
||||
case automaticPhotoDownloadHeader(PresentationTheme, String)
|
||||
case automaticPhotoDownloadPrivateChats(PresentationTheme, String, Bool)
|
||||
case automaticPhotoDownloadGroupsAndChannels(PresentationTheme, String, Bool)
|
||||
case automaticVoiceDownloadHeader(PresentationTheme, String)
|
||||
case automaticVoiceDownloadPrivateChats(PresentationTheme, String, Bool)
|
||||
case automaticVoiceDownloadGroupsAndChannels(PresentationTheme, String, Bool)
|
||||
case automaticInstantVideoDownloadHeader(PresentationTheme, String)
|
||||
case automaticInstantVideoDownloadPrivateChats(PresentationTheme, String, Bool)
|
||||
case automaticInstantVideoDownloadGroupsAndChannels(PresentationTheme, String, Bool)
|
||||
case automaticMediaDownloadHeader(PresentationTheme, String)
|
||||
case automaticDownloadMaster(PresentationTheme, String, Bool)
|
||||
case automaticDownloadPhoto(PresentationTheme, String)
|
||||
case automaticDownloadVideo(PresentationTheme, String)
|
||||
case automaticDownloadFile(PresentationTheme, String)
|
||||
case automaticDownloadVoiceMessage(PresentationTheme, String)
|
||||
case automaticDownloadVideoMessage(PresentationTheme, String)
|
||||
case automaticDownloadReset(PresentationTheme, String, Bool)
|
||||
case voiceCallsHeader(PresentationTheme, String)
|
||||
case useLessVoiceData(PresentationTheme, String, String)
|
||||
case otherHeader(PresentationTheme, String)
|
||||
@ -74,12 +63,8 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
||||
switch self {
|
||||
case .storageUsage, .networkUsage:
|
||||
return DataAndStorageSection.usage.rawValue
|
||||
case .automaticPhotoDownloadHeader, .automaticPhotoDownloadPrivateChats, .automaticPhotoDownloadGroupsAndChannels:
|
||||
return DataAndStorageSection.automaticPhotoDownload.rawValue
|
||||
case .automaticVoiceDownloadHeader, .automaticVoiceDownloadPrivateChats, .automaticVoiceDownloadGroupsAndChannels:
|
||||
return DataAndStorageSection.automaticVoiceDownload.rawValue
|
||||
case .automaticInstantVideoDownloadHeader, .automaticInstantVideoDownloadPrivateChats, .automaticInstantVideoDownloadGroupsAndChannels:
|
||||
return DataAndStorageSection.automaticInstantVideoDownload.rawValue
|
||||
case .automaticMediaDownloadHeader, .automaticDownloadMaster, .automaticDownloadPhoto, .automaticDownloadVideo, .automaticDownloadFile, .automaticDownloadVideoMessage, .automaticDownloadVoiceMessage, .automaticDownloadReset:
|
||||
return DataAndStorageSection.automaticMediaDownload.rawValue
|
||||
case .voiceCallsHeader, .useLessVoiceData:
|
||||
return DataAndStorageSection.voiceCalls.rawValue
|
||||
case .otherHeader, .saveIncomingPhotos, .saveEditedPhotos, .autoplayGifs:
|
||||
@ -95,24 +80,22 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
||||
return 0
|
||||
case .networkUsage:
|
||||
return 1
|
||||
case .automaticPhotoDownloadHeader:
|
||||
case .automaticMediaDownloadHeader:
|
||||
return 2
|
||||
case .automaticPhotoDownloadPrivateChats:
|
||||
case .automaticDownloadMaster:
|
||||
return 3
|
||||
case .automaticPhotoDownloadGroupsAndChannels:
|
||||
case .automaticDownloadPhoto:
|
||||
return 4
|
||||
case .automaticVoiceDownloadHeader:
|
||||
case .automaticDownloadVideo:
|
||||
return 5
|
||||
case .automaticVoiceDownloadPrivateChats:
|
||||
case .automaticDownloadFile:
|
||||
return 6
|
||||
case .automaticVoiceDownloadGroupsAndChannels:
|
||||
case .automaticDownloadVoiceMessage:
|
||||
return 7
|
||||
case .automaticInstantVideoDownloadHeader:
|
||||
case .automaticDownloadVideoMessage:
|
||||
return 8
|
||||
case .automaticInstantVideoDownloadPrivateChats:
|
||||
case .automaticDownloadReset:
|
||||
return 9
|
||||
case .automaticInstantVideoDownloadGroupsAndChannels:
|
||||
return 10
|
||||
case .voiceCallsHeader:
|
||||
return 11
|
||||
case .useLessVoiceData:
|
||||
@ -146,56 +129,50 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .automaticPhotoDownloadHeader(lhsTheme, lhsText):
|
||||
if case let .automaticPhotoDownloadHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
case let .automaticMediaDownloadHeader(lhsTheme, lhsText):
|
||||
if case let .automaticMediaDownloadHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .automaticPhotoDownloadPrivateChats(lhsTheme, lhsText, lhsValue):
|
||||
if case let .automaticPhotoDownloadPrivateChats(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
case let .automaticDownloadMaster(lhsTheme, lhsText, lhsValue):
|
||||
if case let .automaticDownloadMaster(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .automaticPhotoDownloadGroupsAndChannels(lhsTheme, lhsText, lhsValue):
|
||||
if case let .automaticPhotoDownloadGroupsAndChannels(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
case let .automaticDownloadPhoto(lhsTheme, lhsText):
|
||||
if case let .automaticDownloadPhoto(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .automaticVoiceDownloadHeader(lhsTheme, lhsText):
|
||||
if case let .automaticVoiceDownloadHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
case let .automaticDownloadVideo(lhsTheme, lhsText):
|
||||
if case let .automaticDownloadVideo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .automaticVoiceDownloadPrivateChats(lhsTheme, lhsText, lhsValue):
|
||||
if case let .automaticVoiceDownloadPrivateChats(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
case let .automaticDownloadFile(lhsTheme, lhsText):
|
||||
if case let .automaticDownloadFile(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .automaticVoiceDownloadGroupsAndChannels(lhsTheme, lhsText, lhsValue):
|
||||
if case let .automaticVoiceDownloadGroupsAndChannels(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
case let .automaticDownloadVoiceMessage(lhsTheme, lhsText):
|
||||
if case let .automaticDownloadVoiceMessage(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .automaticInstantVideoDownloadHeader(lhsTheme, lhsText):
|
||||
if case let .automaticInstantVideoDownloadHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
case let .automaticDownloadVideoMessage(lhsTheme, lhsText):
|
||||
if case let .automaticDownloadVideoMessage(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .automaticInstantVideoDownloadPrivateChats(lhsTheme, lhsText, lhsValue):
|
||||
if case let .automaticInstantVideoDownloadPrivateChats(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .automaticInstantVideoDownloadGroupsAndChannels(lhsTheme, lhsText, lhsValue):
|
||||
if case let .automaticInstantVideoDownloadGroupsAndChannels(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
case let .automaticDownloadReset(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .automaticDownloadReset(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -265,35 +242,35 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
||||
return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openNetworkUsage()
|
||||
})
|
||||
case let .automaticPhotoDownloadHeader(theme, text):
|
||||
case let .automaticMediaDownloadHeader(theme, text):
|
||||
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
|
||||
case let .automaticPhotoDownloadPrivateChats(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleAutomaticDownload(.photo, .privateChats, value)
|
||||
case let .automaticDownloadMaster(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleAutomaticDownloadMaster(value)
|
||||
})
|
||||
case let .automaticPhotoDownloadGroupsAndChannels(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleAutomaticDownload(.photo, .groupsAndChannels, value)
|
||||
case let .automaticDownloadPhoto(theme, text):
|
||||
return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
|
||||
arguments.openAutomaticDownloadCategory(.photo)
|
||||
})
|
||||
case let .automaticVoiceDownloadHeader(theme, text):
|
||||
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
|
||||
case let .automaticVoiceDownloadPrivateChats(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleAutomaticDownload(.voice, .privateChats, value)
|
||||
case let .automaticDownloadVideo(theme, text):
|
||||
return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
|
||||
arguments.openAutomaticDownloadCategory(.video)
|
||||
})
|
||||
case let .automaticVoiceDownloadGroupsAndChannels(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleAutomaticDownload(.voice, .groupsAndChannels, value)
|
||||
case let .automaticDownloadFile(theme, text):
|
||||
return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
|
||||
arguments.openAutomaticDownloadCategory(.file)
|
||||
})
|
||||
case let .automaticInstantVideoDownloadHeader(theme, text):
|
||||
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
|
||||
case let .automaticInstantVideoDownloadPrivateChats(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleAutomaticDownload(.instantVideo, .privateChats, value)
|
||||
case let .automaticDownloadVoiceMessage(theme, text):
|
||||
return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
|
||||
arguments.openAutomaticDownloadCategory(.voiceMessage)
|
||||
})
|
||||
case let .automaticInstantVideoDownloadGroupsAndChannels(theme, text, value):
|
||||
return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleAutomaticDownload(.instantVideo, .groupsAndChannels, value)
|
||||
case let .automaticDownloadVideoMessage(theme, text):
|
||||
return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
|
||||
arguments.openAutomaticDownloadCategory(.videoMessage)
|
||||
})
|
||||
case let .automaticDownloadReset(theme, text, enabled):
|
||||
return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.resetAutomaticDownload()
|
||||
})
|
||||
case let .voiceCallsHeader(theme, text):
|
||||
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
|
||||
@ -366,21 +343,14 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat
|
||||
entries.append(.storageUsage(presentationData.theme, presentationData.strings.Cache_Title))
|
||||
entries.append(.networkUsage(presentationData.theme, presentationData.strings.NetworkUsageSettings_Title))
|
||||
|
||||
entries.append(.automaticPhotoDownloadHeader(presentationData.theme, presentationData.strings.ChatSettings_AutomaticPhotoDownload))
|
||||
entries.append(.automaticPhotoDownloadPrivateChats(presentationData.theme, presentationData.strings.ChatSettings_PrivateChats, data.automaticMediaDownloadSettings.categories.photo.privateChats))
|
||||
entries.append(.automaticPhotoDownloadGroupsAndChannels(presentationData.theme, presentationData.strings.ChatSettings_Groups, data.automaticMediaDownloadSettings.categories.photo.groupsAndChannels))
|
||||
|
||||
entries.append(.automaticVoiceDownloadHeader(presentationData.theme, presentationData.strings.ChatSettings_AutomaticAudioDownload))
|
||||
entries.append(.automaticVoiceDownloadPrivateChats(presentationData.theme, presentationData.strings.ChatSettings_PrivateChats, data.automaticMediaDownloadSettings.categories.voice.privateChats))
|
||||
entries.append(.automaticVoiceDownloadGroupsAndChannels(presentationData.theme, presentationData.strings.ChatSettings_Groups, data.automaticMediaDownloadSettings.categories.voice.groupsAndChannels))
|
||||
|
||||
entries.append(.automaticInstantVideoDownloadHeader(presentationData.theme, presentationData.strings.ChatSettings_AutomaticVideoMessageDownload))
|
||||
entries.append(.automaticInstantVideoDownloadPrivateChats(presentationData.theme, presentationData.strings.ChatSettings_PrivateChats, data.automaticMediaDownloadSettings.categories.instantVideo.privateChats))
|
||||
entries.append(.automaticInstantVideoDownloadGroupsAndChannels(presentationData.theme, presentationData.strings.ChatSettings_Groups, data.automaticMediaDownloadSettings.categories.instantVideo.groupsAndChannels))
|
||||
|
||||
/*entries.append(.automaticGifDownloadHeader("AUTOMATIC GIF DOWNLOAD"))
|
||||
entries.append(.automaticGifDownloadPrivateChats("Private Chats", data.automaticMediaDownloadSettings.categories.gif.privateChats))
|
||||
entries.append(.automaticGifDownloadGroupsAndChannels("Groups and Channels", data.automaticMediaDownloadSettings.categories.gif.groupsAndChannels))*/
|
||||
entries.append(.automaticMediaDownloadHeader(presentationData.theme, presentationData.strings.ChatSettings_AutomaticMediaDownload))
|
||||
entries.append(.automaticDownloadMaster(presentationData.theme, presentationData.strings.ChatSettings_AutomaticMediaDownloadMaster, data.automaticMediaDownloadSettings.masterEnabled))
|
||||
entries.append(.automaticDownloadPhoto(presentationData.theme, presentationData.strings.ChatSettings_AutomaticDownloadPhoto))
|
||||
entries.append(.automaticDownloadVideo(presentationData.theme, presentationData.strings.ChatSettings_AutomaticDownloadVideo))
|
||||
entries.append(.automaticDownloadFile(presentationData.theme, presentationData.strings.ChatSettings_AutomaticDownloadFile))
|
||||
entries.append(.automaticDownloadVoiceMessage(presentationData.theme, presentationData.strings.ChatSettings_AutomaticDownloadVoiceMessage))
|
||||
entries.append(.automaticDownloadVideoMessage(presentationData.theme, presentationData.strings.ChatSettings_AutomaticDownloadVideoMessage))
|
||||
entries.append(.automaticDownloadReset(presentationData.theme, presentationData.strings.ChatSettings_AutomaticDownloadReset, data.automaticMediaDownloadSettings.peers != AutomaticMediaDownloadSettings.defaultSettings.peers || !data.automaticMediaDownloadSettings.masterEnabled))
|
||||
|
||||
entries.append(.voiceCallsHeader(presentationData.theme, presentationData.strings.Settings_CallSettings.uppercased()))
|
||||
entries.append(.useLessVoiceData(presentationData.theme, presentationData.strings.CallSettings_UseLessData, stringForUseLessDataSetting(strings: presentationData.strings, settings: data.voiceCallSettings)))
|
||||
@ -388,7 +358,7 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat
|
||||
entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other))
|
||||
//entries.append(.saveIncomingPhotos(presentationData.theme, presentationData.strings.Settings_SaveIncomingPhotos, data.automaticMediaDownloadSettings.saveIncomingPhotos))
|
||||
entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos))
|
||||
entries.append(.autoplayGifs(presentationData.theme, presentationData.strings.ChatSettings_AutoPlayAnimations, data.automaticMediaDownloadSettings.categories.gif.privateChats))
|
||||
/*entries.append(.autoplayGifs(presentationData.theme, presentationData.strings.ChatSettings_AutoPlayAnimations, data.automaticMediaDownloadSettings.categories.gif.privateChats))*/
|
||||
|
||||
let proxyValue: String
|
||||
if let _ = data.proxySettings {
|
||||
@ -453,54 +423,39 @@ func dataAndStorageController(account: Account) -> ViewController {
|
||||
} |> deliverOnMainQueue).start(next: { settings in
|
||||
pushControllerImpl?(proxySettingsController(account: account, currentSettings: settings))
|
||||
})
|
||||
}, toggleAutomaticDownload: { category, peers, value in
|
||||
let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { current in
|
||||
switch category {
|
||||
case .photo:
|
||||
switch peers {
|
||||
case .privateChats:
|
||||
return current.withUpdatedCategories(current.categories.withUpdatedPhoto(current.categories.photo.withUpdatedPrivateChats(value)))
|
||||
case .groupsAndChannels:
|
||||
return current.withUpdatedCategories(current.categories.withUpdatedPhoto(current.categories.photo.withUpdatedGroupsAndChannels(value)))
|
||||
}
|
||||
case .voice:
|
||||
switch peers {
|
||||
case .privateChats:
|
||||
return current.withUpdatedCategories(current.categories.withUpdatedVoice(current.categories.voice.withUpdatedPrivateChats(value)))
|
||||
case .groupsAndChannels:
|
||||
return current.withUpdatedCategories(current.categories.withUpdatedVoice(current.categories.voice.withUpdatedGroupsAndChannels(value)))
|
||||
}
|
||||
case .instantVideo:
|
||||
switch peers {
|
||||
case .privateChats:
|
||||
return current.withUpdatedCategories(current.categories.withUpdatedInstantVideo(current.categories.instantVideo.withUpdatedPrivateChats(value)))
|
||||
case .groupsAndChannels:
|
||||
return current.withUpdatedCategories(current.categories.withUpdatedInstantVideo(current.categories.instantVideo.withUpdatedGroupsAndChannels(value)))
|
||||
}
|
||||
case .gif:
|
||||
switch peers {
|
||||
case .privateChats:
|
||||
return current.withUpdatedCategories(current.categories.withUpdatedGif(current.categories.gif.withUpdatedPrivateChats(value)))
|
||||
case .groupsAndChannels:
|
||||
return current.withUpdatedCategories(current.categories.withUpdatedGif(current.categories.gif.withUpdatedGroupsAndChannels(value)))
|
||||
}
|
||||
}
|
||||
}, toggleAutomaticDownloadMaster: { value in
|
||||
let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { settings in
|
||||
var settings = settings
|
||||
settings.masterEnabled = value
|
||||
return settings
|
||||
}).start()
|
||||
}, openAutomaticDownloadCategory: { category in
|
||||
pushControllerImpl?(autodownloadMediaCategoryController(account: account, category: category))
|
||||
}, resetAutomaticDownload: {
|
||||
let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { settings in
|
||||
var settings = settings
|
||||
let defaultSettings = AutomaticMediaDownloadSettings.defaultSettings
|
||||
settings.masterEnabled = defaultSettings.masterEnabled
|
||||
settings.peers = defaultSettings.peers
|
||||
return settings
|
||||
}).start()
|
||||
}, openVoiceUseLessData: {
|
||||
pushControllerImpl?(voiceCallDataSavingController(account: account))
|
||||
}, toggleSaveIncomingPhotos: { value in
|
||||
let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { current in
|
||||
return current.withUpdatedSaveIncomingPhotos(value)
|
||||
let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { settings in
|
||||
var settings = settings
|
||||
settings.saveIncomingPhotos = value
|
||||
return settings
|
||||
}).start()
|
||||
}, toggleSaveEditedPhotos: { value in
|
||||
let _ = updateGeneratedMediaStoreSettingsInteractively(postbox: account.postbox, { current in
|
||||
return current.withUpdatedStoreEditedPhotos(value)
|
||||
}).start()
|
||||
}, toggleAutoplayGifs: { value in
|
||||
let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { current in
|
||||
var updated = current.withUpdatedCategories(current.categories.withUpdatedGif(current.categories.gif.withUpdatedPrivateChats(value)))
|
||||
updated = updated.withUpdatedCategories(updated.categories.withUpdatedGif(updated.categories.gif.withUpdatedGroupsAndChannels(value)))
|
||||
return updated
|
||||
let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { settings in
|
||||
var settings = settings
|
||||
settings.autoplayGifs = value
|
||||
return settings
|
||||
}).start()
|
||||
})
|
||||
|
||||
|
247
TelegramUI/EmojisChatInputContextPanelNode.swift
Normal file
247
TelegramUI/EmojisChatInputContextPanelNode.swift
Normal file
@ -0,0 +1,247 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import Display
|
||||
|
||||
private struct EmojisChatInputContextPanelEntryStableId: Hashable, Equatable {
|
||||
let symbol: String
|
||||
let text: String
|
||||
}
|
||||
|
||||
private struct EmojisChatInputContextPanelEntry: Comparable, Identifiable {
|
||||
let index: Int
|
||||
let theme: PresentationTheme
|
||||
let symbol: String
|
||||
let text: String
|
||||
|
||||
var stableId: EmojisChatInputContextPanelEntryStableId {
|
||||
return EmojisChatInputContextPanelEntryStableId(symbol: self.symbol, text: self.text)
|
||||
}
|
||||
|
||||
static func ==(lhs: EmojisChatInputContextPanelEntry, rhs: EmojisChatInputContextPanelEntry) -> Bool {
|
||||
return lhs.index == rhs.index && lhs.symbol == rhs.symbol && lhs.text == rhs.text && lhs.theme === rhs.theme
|
||||
}
|
||||
|
||||
static func <(lhs: EmojisChatInputContextPanelEntry, rhs: EmojisChatInputContextPanelEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(account: Account, hashtagSelected: @escaping (String) -> Void) -> ListViewItem {
|
||||
return EmojisChatInputPanelItem(theme: self.theme, symbol: self.symbol, text: self.text, hashtagSelected: hashtagSelected)
|
||||
}
|
||||
}
|
||||
|
||||
private struct EmojisChatInputContextPanelTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
}
|
||||
|
||||
private func preparedTransition(from fromEntries: [EmojisChatInputContextPanelEntry], to toEntries: [EmojisChatInputContextPanelEntry], account: Account, hashtagSelected: @escaping (String) -> Void) -> EmojisChatInputContextPanelTransition {
|
||||
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, hashtagSelected: hashtagSelected), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, hashtagSelected: hashtagSelected), directionHint: nil) }
|
||||
|
||||
return EmojisChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates)
|
||||
}
|
||||
|
||||
final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
private var theme: PresentationTheme
|
||||
|
||||
private let listView: ListView
|
||||
private var currentEntries: [EmojisChatInputContextPanelEntry]?
|
||||
|
||||
private var enqueuedTransitions: [(EmojisChatInputContextPanelTransition, Bool)] = []
|
||||
private var validLayout: (CGSize, CGFloat, CGFloat)?
|
||||
|
||||
override init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
|
||||
self.listView = ListView()
|
||||
self.listView.isOpaque = false
|
||||
self.listView.stackFromBottom = true
|
||||
self.listView.keepBottomItemOverscrollBackground = theme.list.plainBackgroundColor
|
||||
self.listView.limitHitTestToNodes = true
|
||||
self.listView.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
|
||||
super.init(account: account, theme: theme, strings: strings)
|
||||
|
||||
self.isOpaque = false
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.listView)
|
||||
}
|
||||
|
||||
func updateResults(_ results: [(String, String)]) {
|
||||
var entries: [EmojisChatInputContextPanelEntry] = []
|
||||
var index = 0
|
||||
var stableIds = Set<EmojisChatInputContextPanelEntryStableId>()
|
||||
for (symbol, text) in results {
|
||||
let entry = EmojisChatInputContextPanelEntry(index: index, theme: self.theme, symbol: symbol, text: text)
|
||||
if stableIds.contains(entry.stableId) {
|
||||
continue
|
||||
}
|
||||
stableIds.insert(entry.stableId)
|
||||
entries.append(entry)
|
||||
index += 1
|
||||
}
|
||||
|
||||
let firstTime = self.currentEntries == nil
|
||||
let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.account, hashtagSelected: { [weak self] text in
|
||||
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
||||
interfaceInteraction.updateTextInputState { textInputState in
|
||||
var hashtagQueryRange: NSRange?
|
||||
inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) {
|
||||
if type == [.stickerSearch] {
|
||||
var range = range
|
||||
range.location -= 1
|
||||
range.length += 1
|
||||
hashtagQueryRange = range
|
||||
break inner
|
||||
}
|
||||
}
|
||||
|
||||
if let range = hashtagQueryRange {
|
||||
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
|
||||
|
||||
let replacementText = text
|
||||
|
||||
inputText.replaceCharacters(in: range, with: replacementText)
|
||||
|
||||
let selectionPosition = range.lowerBound + (replacementText as NSString).length
|
||||
|
||||
return ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||
}
|
||||
return textInputState
|
||||
}
|
||||
}
|
||||
})
|
||||
self.currentEntries = entries
|
||||
self.enqueueTransition(transition, firstTime: firstTime)
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: EmojisChatInputContextPanelTransition, firstTime: Bool) {
|
||||
enqueuedTransitions.append((transition, firstTime))
|
||||
|
||||
if self.validLayout != nil {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
if let validLayout = self.validLayout, let (transition, firstTime) = self.enqueuedTransitions.first {
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
if firstTime {
|
||||
//options.insert(.Synchronous)
|
||||
//options.insert(.LowLatency)
|
||||
} else {
|
||||
options.insert(.AnimateTopItemPosition)
|
||||
options.insert(.AnimateCrossfade)
|
||||
}
|
||||
|
||||
var insets = UIEdgeInsets()
|
||||
insets.top = topInsetForLayout(size: validLayout.0)
|
||||
insets.left = validLayout.1
|
||||
insets.right = validLayout.2
|
||||
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: insets, duration: 0.0, curve: .Default)
|
||||
|
||||
self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
if let strongSelf = self, firstTime {
|
||||
var topItemOffset: CGFloat?
|
||||
strongSelf.listView.forEachItemNode { itemNode in
|
||||
if topItemOffset == nil {
|
||||
topItemOffset = itemNode.frame.minY
|
||||
}
|
||||
}
|
||||
|
||||
if let topItemOffset = topItemOffset {
|
||||
let position = strongSelf.listView.layer.position
|
||||
strongSelf.listView.layer.animatePosition(from: CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset)), to: position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func topInsetForLayout(size: CGSize) -> CGFloat {
|
||||
var minimumItemHeights: CGFloat = floor(MentionChatInputPanelItemNode.itemHeight * 3.5)
|
||||
|
||||
return max(size.height - minimumItemHeights, 0.0)
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) {
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
self.validLayout = (size, leftInset, rightInset)
|
||||
|
||||
var insets = UIEdgeInsets()
|
||||
insets.top = self.topInsetForLayout(size: size)
|
||||
insets.left = leftInset
|
||||
insets.right = rightInset
|
||||
|
||||
transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
|
||||
|
||||
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: size, insets: insets, duration: duration, curve: listViewCurve)
|
||||
|
||||
self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
if !hadValidLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func animateOut(completion: @escaping () -> Void) {
|
||||
var topItemOffset: CGFloat?
|
||||
self.listView.forEachItemNode { itemNode in
|
||||
if topItemOffset == nil {
|
||||
topItemOffset = itemNode.frame.minY
|
||||
}
|
||||
}
|
||||
|
||||
if let topItemOffset = topItemOffset {
|
||||
let position = self.listView.layer.position
|
||||
self.listView.layer.animatePosition(from: position, to: CGPoint(x: position.x, y: position.y + (self.listView.bounds.size.height - topItemOffset)), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let listViewFrame = self.listView.frame
|
||||
return self.listView.hitTest(CGPoint(x: point.x - listViewFrame.minX, y: point.y - listViewFrame.minY), with: event)
|
||||
}
|
||||
}
|
||||
|
177
TelegramUI/EmojisChatInputPanelItem.swift
Normal file
177
TelegramUI/EmojisChatInputPanelItem.swift
Normal file
@ -0,0 +1,177 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
|
||||
final class EmojisChatInputPanelItem: ListViewItem {
|
||||
fileprivate let theme: PresentationTheme
|
||||
fileprivate let symbol: String
|
||||
fileprivate let text: String
|
||||
private let hashtagSelected: (String) -> Void
|
||||
|
||||
let selectable: Bool = true
|
||||
|
||||
public init(theme: PresentationTheme, symbol: String, text: String, hashtagSelected: @escaping (String) -> Void) {
|
||||
self.theme = theme
|
||||
self.symbol = symbol
|
||||
self.text = text
|
||||
self.hashtagSelected = hashtagSelected
|
||||
}
|
||||
|
||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, () -> Void)) -> Void) {
|
||||
let configure = { () -> Void in
|
||||
let node = EmojisChatInputPanelItemNode()
|
||||
|
||||
let nodeLayout = node.asyncLayout()
|
||||
let (top, bottom) = (previousItem != nil, nextItem != nil)
|
||||
let (layout, apply) = nodeLayout(self, params, top, bottom)
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
completion(node, {
|
||||
return (nil, { apply(.None) })
|
||||
})
|
||||
}
|
||||
if Thread.isMainThread {
|
||||
async {
|
||||
configure()
|
||||
}
|
||||
} else {
|
||||
configure()
|
||||
}
|
||||
}
|
||||
|
||||
public 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? EmojisChatInputPanelItemNode {
|
||||
Queue.mainQueue().async {
|
||||
let nodeLayout = node.asyncLayout()
|
||||
|
||||
async {
|
||||
let (top, bottom) = (previousItem != nil, nextItem != nil)
|
||||
|
||||
let (layout, apply) = nodeLayout(self, params, top, bottom)
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, {
|
||||
apply(animation)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
self.hashtagSelected(self.symbol)
|
||||
}
|
||||
}
|
||||
|
||||
private let textFont = Font.regular(14.0)
|
||||
|
||||
final class EmojisChatInputPanelItemNode: ListViewItemNode {
|
||||
static let itemHeight: CGFloat = 42.0
|
||||
private let symbolNode: TextNode
|
||||
private let textNode: TextNode
|
||||
private let topSeparatorNode: ASDisplayNode
|
||||
private let separatorNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
|
||||
init() {
|
||||
self.symbolNode = TextNode()
|
||||
self.textNode = TextNode()
|
||||
|
||||
self.topSeparatorNode = ASDisplayNode()
|
||||
self.topSeparatorNode.isLayerBacked = true
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.topSeparatorNode)
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.symbolNode)
|
||||
self.addSubnode(self.textNode)
|
||||
}
|
||||
|
||||
override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
||||
if let item = item as? EmojisChatInputPanelItem {
|
||||
let doLayout = self.asyncLayout()
|
||||
let merged = (top: previousItem != nil, bottom: nextItem != nil)
|
||||
let (layout, apply) = doLayout(item, params, merged.top, merged.bottom)
|
||||
self.contentSize = layout.contentSize
|
||||
self.insets = layout.insets
|
||||
apply(.None)
|
||||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: EmojisChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
||||
let makeSymbolLayout = TextNode.asyncLayout(self.symbolNode)
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
return { [weak self] item, params, mergedTop, mergedBottom in
|
||||
let leftInset: CGFloat = 15.0 + params.leftInset + 24.0
|
||||
let rightInset: CGFloat = 10.0 + params.rightInset
|
||||
|
||||
let (symbolLayout, symbolApply) = makeSymbolLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(item.symbol)", font: textFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(item.text)", font: textFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: EmojisChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets())
|
||||
|
||||
return (nodeLayout, { _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.backgroundColor = item.theme.list.plainBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
|
||||
let _ = symbolApply()
|
||||
strongSelf.symbolNode.frame = CGRect(origin: CGPoint(x: floor((leftInset - symbolLayout.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - symbolLayout.size.height) / 2.0)), size: symbolLayout.size)
|
||||
|
||||
let _ = textApply()
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size)
|
||||
|
||||
strongSelf.topSeparatorNode.isHidden = mergedTop
|
||||
strongSelf.separatorNode.isHidden = !mergedBottom
|
||||
|
||||
strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel))
|
||||
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset, height: UIScreenPixel))
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + 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 {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog
|
||||
var statusBar: StatusBar?
|
||||
var navigationBar: NavigationBar?
|
||||
let footerNode: GalleryFooterNode
|
||||
var currentThumbnailContainerNode: GalleryThumbnailContainerNode?
|
||||
var toolbarNode: ASDisplayNode?
|
||||
var transitionDataForCentralItem: (() -> ((ASDisplayNode, () -> UIView?)?, (UIView) -> Void)?)?
|
||||
var dismiss: (() -> Void)?
|
||||
@ -85,6 +86,54 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog
|
||||
|
||||
self.scrollView.addSubview(self.pager.view)
|
||||
self.addSubnode(self.footerNode)
|
||||
|
||||
self.pager.centralItemIndexOffsetUpdated = { [weak self] indexAndProgress in
|
||||
if let strongSelf = self {
|
||||
var node: GalleryThumbnailContainerNode?
|
||||
if let (index, progress) = indexAndProgress {
|
||||
if let (centralId, centralItem) = strongSelf.pager.items[index].thumbnailItem() {
|
||||
var items: [GalleryThumbnailItem] = [centralItem]
|
||||
for i in (0 ..< index).reversed() {
|
||||
if let (id, item) = strongSelf.pager.items[i].thumbnailItem(), id == centralId {
|
||||
items.insert(item, at: 0)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
for i in (index + 1) ..< strongSelf.pager.items.count {
|
||||
if let (id, item) = strongSelf.pager.items[i].thumbnailItem(), id == centralId {
|
||||
items.append(item)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
let convertedIndex = (items.index(where: { $0.isEqual(to: centralItem) })!, progress)
|
||||
if strongSelf.currentThumbnailContainerNode?.groupId != centralId {
|
||||
node = GalleryThumbnailContainerNode(groupId: centralId)
|
||||
node?.updateItems(items, centralIndex: convertedIndex.0, progress: convertedIndex.1)
|
||||
} else {
|
||||
node = strongSelf.currentThumbnailContainerNode
|
||||
node?.updateItems(items, centralIndex: convertedIndex.0, progress: convertedIndex.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
if node !== strongSelf.currentThumbnailContainerNode {
|
||||
if let current = strongSelf.currentThumbnailContainerNode {
|
||||
current.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak current] _ in
|
||||
current?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
strongSelf.currentThumbnailContainerNode = node
|
||||
if let node = node {
|
||||
strongSelf.insertSubnode(node, aboveSubnode: strongSelf.footerNode)
|
||||
if let (navigationHeight, layout) = strongSelf.containerLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
|
||||
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
@ -101,7 +150,16 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isBackgroundExtendedOverNavigationBar ? 0.0 : navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - (self.isBackgroundExtendedOverNavigationBar ? 0.0 : navigationBarHeight))))
|
||||
|
||||
transition.updateFrame(node: self.footerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
self.footerNode.updateLayout(layout, footerContentNode: self.presentationState.footerContentNode, transition: transition)
|
||||
|
||||
var thumbnailPanelHeight: CGFloat = 0.0
|
||||
if let currentThumbnailContainerNode = self.currentThumbnailContainerNode {
|
||||
thumbnailPanelHeight = 40.0
|
||||
let thumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - 40.0 - thumbnailPanelHeight + 4.0), size: CGSize(width: layout.size.width, height: thumbnailPanelHeight - 4.0))
|
||||
transition.updateFrame(node: currentThumbnailContainerNode, frame: thumbnailsFrame)
|
||||
currentThumbnailContainerNode.updateLayout(size: thumbnailsFrame.size, transition: transition)
|
||||
}
|
||||
|
||||
self.footerNode.updateLayout(layout, footerContentNode: self.presentationState.footerContentNode, thumbnailPanelHeight: thumbnailPanelHeight, transition: transition)
|
||||
|
||||
let previousContentHeight = self.scrollView.contentSize.height
|
||||
let previousVerticalOffset = self.scrollView.contentOffset.y
|
||||
@ -127,12 +185,14 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog
|
||||
self.navigationBar?.alpha = alpha
|
||||
self.statusBar?.alpha = alpha
|
||||
self.footerNode.alpha = alpha
|
||||
self.currentThumbnailContainerNode?.alpha = alpha
|
||||
})
|
||||
} else {
|
||||
let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0
|
||||
self.navigationBar?.alpha = alpha
|
||||
self.statusBar?.alpha = alpha
|
||||
self.footerNode.alpha = alpha
|
||||
self.currentThumbnailContainerNode?.alpha = alpha
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,11 +201,13 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog
|
||||
self.statusBar?.alpha = 0.0
|
||||
self.navigationBar?.alpha = 0.0
|
||||
self.footerNode.alpha = 0.0
|
||||
self.currentThumbnailContainerNode?.alpha = 0.0
|
||||
UIView.animate(withDuration: 0.2, animations: {
|
||||
self.backgroundNode.backgroundColor = self.backgroundNode.backgroundColor?.withAlphaComponent(1.0)
|
||||
self.statusBar?.alpha = 1.0
|
||||
self.navigationBar?.alpha = 1.0
|
||||
self.footerNode.alpha = 1.0
|
||||
self.currentThumbnailContainerNode?.alpha = 1.0
|
||||
})
|
||||
|
||||
if let toolbarNode = self.toolbarNode {
|
||||
@ -177,6 +239,7 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog
|
||||
self.statusBar?.alpha = 0.0
|
||||
self.navigationBar?.alpha = 0.0
|
||||
self.footerNode.alpha = 0.0
|
||||
self.currentThumbnailContainerNode?.alpha = 0.0
|
||||
}, completion: { _ in
|
||||
interfaceAnimationCompleted = true
|
||||
intermediateCompletion()
|
||||
@ -209,6 +272,7 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog
|
||||
self.statusBar?.alpha = transition
|
||||
self.navigationBar?.alpha = transition
|
||||
self.footerNode.alpha = transition
|
||||
self.currentThumbnailContainerNode?.alpha = transition
|
||||
}
|
||||
|
||||
self.updateDismissTransition(transition)
|
||||
|
@ -19,7 +19,14 @@ open class GalleryFooterContentNode: ASDisplayNode {
|
||||
var requestLayout: ((ContainedViewLayoutTransition) -> Void)?
|
||||
var controllerInteraction: GalleryControllerInteraction?
|
||||
|
||||
func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
func animateIn(fromHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
}
|
||||
|
||||
func animateOut(toHeight: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ final class GalleryFooterNode: ASDisplayNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
|
||||
private var currentFooterContentNode: GalleryFooterContentNode?
|
||||
private var currentLayout: ContainerViewLayout?
|
||||
private var currentLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private let controllerInteraction: GalleryControllerInteraction
|
||||
|
||||
@ -21,8 +21,8 @@ final class GalleryFooterNode: ASDisplayNode {
|
||||
self.addSubnode(self.backgroundNode)
|
||||
}
|
||||
|
||||
func updateLayout(_ layout: ContainerViewLayout, footerContentNode: GalleryFooterContentNode?, transition: ContainedViewLayoutTransition) {
|
||||
self.currentLayout = layout
|
||||
func updateLayout(_ layout: ContainerViewLayout, footerContentNode: GalleryFooterContentNode?, thumbnailPanelHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.currentLayout = (layout, thumbnailPanelHeight)
|
||||
let cleanInsets = layout.insets(options: [])
|
||||
|
||||
var removeCurrentFooterContentNode: GalleryFooterContentNode?
|
||||
@ -35,25 +35,35 @@ final class GalleryFooterNode: ASDisplayNode {
|
||||
if let footerContentNode = footerContentNode {
|
||||
footerContentNode.controllerInteraction = self.controllerInteraction
|
||||
footerContentNode.requestLayout = { [weak self] transition in
|
||||
if let strongSelf = self, let currentLayout = strongSelf.currentLayout {
|
||||
strongSelf.updateLayout(currentLayout, footerContentNode: strongSelf.currentFooterContentNode, transition: transition)
|
||||
if let strongSelf = self, let (currentLayout, currentThumbnailPanelHeight) = strongSelf.currentLayout {
|
||||
strongSelf.updateLayout(currentLayout, footerContentNode: strongSelf.currentFooterContentNode, thumbnailPanelHeight: currentThumbnailPanelHeight, transition: transition)
|
||||
}
|
||||
}
|
||||
self.addSubnode(footerContentNode)
|
||||
}
|
||||
}
|
||||
|
||||
if let removeCurrentFooterContentNode = removeCurrentFooterContentNode {
|
||||
removeCurrentFooterContentNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
var backgroundHeight: CGFloat = 0.0
|
||||
if let footerContentNode = self.currentFooterContentNode {
|
||||
backgroundHeight = footerContentNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, transition: transition)
|
||||
backgroundHeight = footerContentNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, contentInset: thumbnailPanelHeight, transition: transition)
|
||||
transition.updateFrame(node: footerContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight)))
|
||||
if let removeCurrentFooterContentNode = removeCurrentFooterContentNode {
|
||||
let contentTransition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
footerContentNode.animateIn(fromHeight: removeCurrentFooterContentNode.bounds.height, transition: contentTransition)
|
||||
removeCurrentFooterContentNode.animateOut(toHeight: backgroundHeight, transition: contentTransition, completion: { [weak removeCurrentFooterContentNode] in
|
||||
removeCurrentFooterContentNode?.removeFromSupernode()
|
||||
})
|
||||
contentTransition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight)))
|
||||
} else {
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight)))
|
||||
}
|
||||
} else {
|
||||
if let removeCurrentFooterContentNode = removeCurrentFooterContentNode {
|
||||
removeCurrentFooterContentNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight)))
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight), size: CGSize(width: layout.size.width, height: backgroundHeight)))
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
|
@ -21,4 +21,5 @@ struct GalleryItemIndexData: Equatable {
|
||||
protocol GalleryItem {
|
||||
func node() -> GalleryItemNode
|
||||
func updateNode(node: GalleryItemNode)
|
||||
func thumbnailItem() -> (Int64, GalleryThumbnailItem)?
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
private let scrollView: UIScrollView
|
||||
|
||||
private var items: [GalleryItem] = []
|
||||
private(set) var items: [GalleryItem] = []
|
||||
private var itemNodes: [GalleryItemNode] = []
|
||||
private var ignoreDidScroll = false
|
||||
private var ignoreCentralItemIndexUpdate = false
|
||||
@ -55,6 +55,7 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
var centralItemIndexUpdated: (Int?) -> Void = { _ in }
|
||||
var centralItemIndexOffsetUpdated: ((Int, CGFloat)?) -> Void = { _ in }
|
||||
var toggleControlsVisibility: () -> Void = { }
|
||||
var beginCustomDismiss: () -> Void = { }
|
||||
var completeCustomDismiss: () -> Void = { }
|
||||
@ -361,6 +362,8 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
if notifyCentralItemUpdated {
|
||||
self.centralItemIndexUpdated(self.centralItemIndex)
|
||||
}
|
||||
|
||||
self.updateCentralIndexOffset(transition: .immediate)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
@ -389,5 +392,16 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCentralIndexOffset(transition: ContainedViewLayoutTransition) {
|
||||
if let centralIndex = self.centralItemIndex, let itemNode = self.visibleItemNode(at: centralIndex) {
|
||||
let offset: CGFloat = self.scrollView.contentOffset.x + self.pageGap - itemNode.frame.minX
|
||||
var progress = offset / self.scrollView.bounds.size.height
|
||||
progress = min(1.0, progress)
|
||||
progress = max(-1.0, progress)
|
||||
self.centralItemIndexOffsetUpdated((centralIndex, progress))
|
||||
} else {
|
||||
self.centralItemIndexOffsetUpdated(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
147
TelegramUI/GalleryThumbnailContainerNode.swift
Normal file
147
TelegramUI/GalleryThumbnailContainerNode.swift
Normal file
@ -0,0 +1,147 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
|
||||
protocol GalleryThumbnailItem {
|
||||
func isEqual(to: GalleryThumbnailItem) -> Bool
|
||||
var image: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) { get }
|
||||
}
|
||||
|
||||
private final class GalleryThumbnailItemNode: ASDisplayNode {
|
||||
private let imageNode: TransformImageNode
|
||||
private let imageContainerNode: ASDisplayNode
|
||||
|
||||
private let imageSize: CGSize
|
||||
|
||||
init(item: GalleryThumbnailItem) {
|
||||
self.imageNode = TransformImageNode()
|
||||
self.imageContainerNode = ASDisplayNode()
|
||||
self.imageContainerNode.clipsToBounds = true
|
||||
self.imageContainerNode.cornerRadius = 4.0
|
||||
let (signal, imageSize) = item.image
|
||||
self.imageSize = imageSize
|
||||
|
||||
super.init()
|
||||
|
||||
self.imageContainerNode.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.imageContainerNode)
|
||||
self.imageNode.setSignal(signal)
|
||||
}
|
||||
|
||||
func updateLayout(height: CGFloat, progress: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
let baseWidth: CGFloat = 20.0
|
||||
let boundingSize = self.imageSize.aspectFilled(CGSize(width: 1.0, height: height))
|
||||
let width = baseWidth * (1.0 - progress) + boundingSize.width * progress
|
||||
let arguments = TransformImageArguments(corners: ImageCorners(radius: 0), imageSize: boundingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())
|
||||
let makeLayout = self.imageNode.asyncLayout()
|
||||
let apply = makeLayout(arguments)
|
||||
apply()
|
||||
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: (width - boundingSize.width) / 2.0, y: 0.0), size: boundingSize))
|
||||
transition.updateFrame(node: self.imageContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
|
||||
|
||||
return width
|
||||
}
|
||||
}
|
||||
|
||||
final class GalleryThumbnailContainerNode: ASDisplayNode {
|
||||
let groupId: Int64
|
||||
private let contentNode: ASDisplayNode
|
||||
|
||||
private var items: [GalleryThumbnailItem] = []
|
||||
private var itemNodes: [GalleryThumbnailItemNode] = []
|
||||
private var centralIndexAndProgress: (Int, CGFloat)?
|
||||
private var currentLayout: CGSize?
|
||||
|
||||
init(groupId: Int64) {
|
||||
self.groupId = groupId
|
||||
self.contentNode = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.contentNode)
|
||||
}
|
||||
|
||||
func updateItems(_ items: [GalleryThumbnailItem], centralIndex: Int, progress: CGFloat) {
|
||||
var updated = false
|
||||
if self.items.count == items.count {
|
||||
for i in 0 ..< self.items.count {
|
||||
if !self.items[i].isEqual(to: items[i]) {
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updated = true
|
||||
}
|
||||
if updated {
|
||||
var itemNodes: [GalleryThumbnailItemNode] = []
|
||||
for item in items {
|
||||
if let index = self.items.index(where: { $0.isEqual(to: item) }) {
|
||||
itemNodes.append(self.itemNodes[index])
|
||||
} else {
|
||||
itemNodes.append(GalleryThumbnailItemNode(item: item))
|
||||
}
|
||||
}
|
||||
|
||||
for itemNode in itemNodes {
|
||||
if itemNode.supernode == nil {
|
||||
self.contentNode.addSubnode(itemNode)
|
||||
}
|
||||
}
|
||||
for itemNode in self.itemNodes {
|
||||
if !itemNodes.contains(where: { $0 === itemNode }) {
|
||||
itemNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
self.items = items
|
||||
self.itemNodes = itemNodes
|
||||
}
|
||||
self.centralIndexAndProgress = (centralIndex, progress)
|
||||
if let size = self.currentLayout {
|
||||
self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
self.currentLayout = size
|
||||
if let (centralIndex, progress) = self.centralIndexAndProgress {
|
||||
self.updateLayout(size: size, centralIndex: centralIndex, progress: progress, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, centralIndex: Int, progress: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.currentLayout = size
|
||||
self.contentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
let spacing: CGFloat = 2.0
|
||||
let centralSpacing: CGFloat = 6.0
|
||||
let itemHeight: CGFloat = 30.0
|
||||
let centralProgress: CGFloat = 1.0 - abs(progress * 2.0)
|
||||
let leftProgress: CGFloat = max(0.0, -progress * 2.0)
|
||||
let rightProgress: CGFloat = max(0.0, progress * 2.0)
|
||||
|
||||
let centralWidth = self.itemNodes[centralIndex].updateLayout(height: itemHeight, progress: centralProgress, transition: transition)
|
||||
var centralFrame = CGRect(origin: CGPoint(x: ((size.width - centralWidth) / 2.0), y: 0.0), size: CGSize(width: centralWidth, height: itemHeight))
|
||||
centralFrame.origin.x += -progress * 2.0 * centralFrame.width
|
||||
let currentCentralSpacing: CGFloat = centralProgress * centralSpacing + (1.0 - centralProgress) * spacing
|
||||
var leftOffset = centralFrame.minX - currentCentralSpacing
|
||||
var rightOffset = centralFrame.maxX + currentCentralSpacing
|
||||
transition.updateFrame(node: self.itemNodes[centralIndex], frame: centralFrame)
|
||||
|
||||
for i in (0 ..< centralIndex).reversed() {
|
||||
let progress: CGFloat = i == centralIndex - 1 ? leftProgress : 0.0
|
||||
let itemSpacing: CGFloat = progress * centralSpacing + (1.0 - progress) * spacing
|
||||
let itemWidth = self.itemNodes[i].updateLayout(height: itemHeight, progress: progress, transition: transition)
|
||||
transition.updateFrame(node: self.itemNodes[i], frame: CGRect(origin: CGPoint(x: leftOffset - itemWidth, y: 0.0), size: CGSize(width: itemWidth, height: itemHeight)))
|
||||
leftOffset -= itemSpacing + itemWidth
|
||||
}
|
||||
|
||||
for i in (centralIndex + 1) ..< self.itemNodes.count {
|
||||
let progress = i == centralIndex + 1 ? rightProgress : 0.0
|
||||
let itemSpacing: CGFloat = progress * centralSpacing + (1.0 - progress) * spacing
|
||||
let itemWidth = self.itemNodes[i].updateLayout(height: itemHeight, progress: progress, transition: transition)
|
||||
transition.updateFrame(node: self.itemNodes[i], frame: CGRect(origin: CGPoint(x: rightOffset, y: 0.0), size: CGSize(width: itemWidth, height: itemHeight)))
|
||||
rightOffset += itemSpacing + itemWidth
|
||||
}
|
||||
}
|
||||
}
|
@ -668,6 +668,8 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa
|
||||
let peerNotificationSettings: TelegramPeerNotificationSettings = (view.notificationSettings as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings
|
||||
let notificationsText: String
|
||||
switch peerNotificationSettings.muteState {
|
||||
case .default:
|
||||
notificationsText = "Default"
|
||||
case .muted:
|
||||
notificationsText = presentationData.strings.UserInfo_NotificationsDisabled
|
||||
case .unmuted:
|
||||
@ -1111,20 +1113,28 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl
|
||||
let dismissAction: () -> Void = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
let notificationAction: (Int32) -> Void = { muteUntil in
|
||||
let notificationAction: (Int32?) -> Void = { muteUntil in
|
||||
let muteInterval: Int32?
|
||||
if muteUntil <= 0 {
|
||||
muteInterval = nil
|
||||
} else if muteUntil == Int32.max {
|
||||
muteInterval = Int32.max
|
||||
if let muteUntil = muteUntil {
|
||||
if muteUntil <= 0 {
|
||||
muteInterval = 0
|
||||
} else if muteUntil == Int32.max {
|
||||
muteInterval = Int32.max
|
||||
} else {
|
||||
muteInterval = muteUntil
|
||||
}
|
||||
} else {
|
||||
muteInterval = muteUntil
|
||||
muteInterval = nil
|
||||
}
|
||||
|
||||
changeMuteSettingsDisposable.set(updatePeerMuteSetting(account: account, peerId: peerId, muteInterval: muteInterval).start())
|
||||
}
|
||||
controller.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: "Default", action: {
|
||||
dismissAction()
|
||||
notificationAction(nil)
|
||||
}),
|
||||
ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsEnable, action: {
|
||||
dismissAction()
|
||||
notificationAction(0)
|
||||
@ -1525,7 +1535,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl
|
||||
|
||||
aboutLinkActionImpl = { [weak controller] action, itemLink in
|
||||
if let controller = controller {
|
||||
handlePeerInfoAboutTextAction(account: account, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
|
||||
handlePeerInfoAboutTextAction(account: account, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1556,7 +1566,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl
|
||||
return controller
|
||||
}
|
||||
|
||||
func handlePeerInfoAboutTextAction(account: Account, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem) {
|
||||
func handlePeerInfoAboutTextAction(account: Account, peerId: PeerId, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem) {
|
||||
let openPeerImpl: (PeerId) -> Void = { [weak controller] peerId in
|
||||
let peerSignal: Signal<Peer?, NoError>
|
||||
peerSignal = account.postbox.loadedPeerWithId(peerId) |> map { Optional($0) }
|
||||
@ -1612,9 +1622,13 @@ func handlePeerInfoAboutTextAction(account: Account, navigateDisposable: MetaDis
|
||||
openLinkImpl(url)
|
||||
case let .mention(mention):
|
||||
openPeerMentionImpl(mention)
|
||||
case let .hashtag(peerName, hashtag):
|
||||
let searchController = HashtagSearchController(account: account, peerName: peerName, query: hashtag)
|
||||
(controller.navigationController as? NavigationController)?.pushViewController(searchController)
|
||||
case let .hashtag(_, hashtag):
|
||||
let peerSignal = account.postbox.loadedPeerWithId(peerId)
|
||||
let _ = (peerSignal
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
let searchController = HashtagSearchController(account: account, peer: peer, query: hashtag)
|
||||
(controller.navigationController as? NavigationController)?.pushViewController(searchController)
|
||||
})
|
||||
}
|
||||
case .longTap:
|
||||
switch itemLink {
|
||||
@ -1666,7 +1680,7 @@ func handlePeerInfoAboutTextAction(account: Account, navigateDisposable: MetaDis
|
||||
ActionSheetTextItem(title: hashtag),
|
||||
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
let searchController = HashtagSearchController(account: account, peerName: peerName, query: hashtag)
|
||||
let searchController = HashtagSearchController(account: account, peer: nil, query: hashtag)
|
||||
(controller.navigationController as? NavigationController)?.pushViewController(searchController)
|
||||
}),
|
||||
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||||
|
@ -8,6 +8,8 @@ final class HashtagSearchController: TelegramController {
|
||||
private let queue = Queue()
|
||||
|
||||
private let account: Account
|
||||
private let peer: Peer?
|
||||
private let query: String
|
||||
private var transitionDisposable: Disposable?
|
||||
private let openMessageFromSearchDisposable = MetaDisposable()
|
||||
|
||||
@ -17,8 +19,10 @@ final class HashtagSearchController: TelegramController {
|
||||
return self.displayNode as! HashtagSearchControllerNode
|
||||
}
|
||||
|
||||
init(account: Account, peerName: String?, query: String) {
|
||||
init(account: Account, peer: Peer?, query: String) {
|
||||
self.account = account
|
||||
self.peer = peer
|
||||
self.query = query
|
||||
|
||||
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
|
||||
@ -26,35 +30,15 @@ final class HashtagSearchController: TelegramController {
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
|
||||
|
||||
if let peerName = peerName {
|
||||
self.title = query + "@" + peerName
|
||||
} else {
|
||||
self.title = query
|
||||
}
|
||||
self.title = query
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
let peerId: Signal<PeerId?, NoError>
|
||||
if let peerName = peerName {
|
||||
peerId = resolvePeerByName(account: account, name: peerName)
|
||||
|> take(1)
|
||||
} else {
|
||||
peerId = .single(nil)
|
||||
}
|
||||
|
||||
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, strings: self.presentationData.strings, timeFormat: self.presentationData.timeFormat)
|
||||
let foundMessages: Signal<[ChatListSearchEntry], NoError> = peerId
|
||||
|> mapToSignal { peerId -> Signal<[ChatListSearchEntry], NoError> in
|
||||
let location: SearchMessagesLocation
|
||||
if let peerId = peerId {
|
||||
location = .peer(peerId: peerId, fromId: nil, tags: nil)
|
||||
} else {
|
||||
location = .general
|
||||
}
|
||||
let search = searchMessages(account: account, location: location, query: query)
|
||||
return search
|
||||
|> map { return $0.map({ .message($0, chatListPresentationData) }) }
|
||||
}
|
||||
|
||||
let location: SearchMessagesLocation = .general
|
||||
let search = searchMessages(account: account, location: location, query: query)
|
||||
let foundMessages: Signal<[ChatListSearchEntry], NoError> = search
|
||||
|> map { return $0.map({ .message($0, chatListPresentationData) }) }
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {
|
||||
}, peerSelected: { peer in
|
||||
|
||||
@ -99,7 +83,7 @@ final class HashtagSearchController: TelegramController {
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
self.displayNode = HashtagSearchControllerNode(account: self.account, theme: self.presentationData.theme)
|
||||
self.displayNode = HashtagSearchControllerNode(account: self.account, peer: self.peer, query: self.query, theme: self.presentationData.theme, strings: self.presentationData.strings)
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
@ -5,9 +5,15 @@ import Postbox
|
||||
import TelegramCore
|
||||
|
||||
final class HashtagSearchControllerNode: ASDisplayNode {
|
||||
private let toolbarBackgroundNode: ASDisplayNode
|
||||
private let toolbarSeparatorNode: ASDisplayNode
|
||||
private let segmentedControl: UISegmentedControl
|
||||
let listNode: ListView
|
||||
|
||||
private var chatController: ChatController?
|
||||
|
||||
private let account: Account
|
||||
private let query: String
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
private var enqueuedTransitions: [(ChatListSearchContainerTransition, Bool)] = []
|
||||
@ -15,10 +21,27 @@ final class HashtagSearchControllerNode: ASDisplayNode {
|
||||
|
||||
var navigationBar: NavigationBar?
|
||||
|
||||
init(account: Account, theme: PresentationTheme) {
|
||||
init(account: Account, peer: Peer?, query: String, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.account = account
|
||||
self.query = query
|
||||
self.listNode = ListView()
|
||||
|
||||
self.toolbarBackgroundNode = ASDisplayNode()
|
||||
self.toolbarBackgroundNode.backgroundColor = theme.rootController.navigationBar.backgroundColor
|
||||
|
||||
self.toolbarSeparatorNode = ASDisplayNode()
|
||||
self.toolbarSeparatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor
|
||||
|
||||
self.segmentedControl = UISegmentedControl(items: [peer?.displayTitle ?? "", strings.HashtagSearch_AllChats])
|
||||
self.segmentedControl.tintColor = theme.rootController.navigationBar.accentTextColor
|
||||
self.segmentedControl.selectedSegmentIndex = 1
|
||||
|
||||
if let peer = peer {
|
||||
self.chatController = ChatController(account: account, chatLocation: .peer(peer.id), messageId: nil, botStart: nil, mode: .inline)
|
||||
} else {
|
||||
self.chatController = nil
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
@ -29,6 +52,8 @@ final class HashtagSearchControllerNode: ASDisplayNode {
|
||||
|
||||
self.listNode.isHidden = true
|
||||
self.addSubnode(self.listNode)
|
||||
|
||||
self.segmentedControl.addTarget(self, action: #selector(self.indexChanged), for: .valueChanged)
|
||||
}
|
||||
|
||||
func enqueueTransition(_ transition: ChatListSearchContainerTransition, firstTime: Bool) {
|
||||
@ -42,7 +67,7 @@ final class HashtagSearchControllerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
if let (transition, firstTime) = self.enqueuedTransitions.first {
|
||||
if let (transition, _) = self.enqueuedTransitions.first {
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
let options = ListViewDeleteAndInsertOptions()
|
||||
@ -64,9 +89,43 @@ final class HashtagSearchControllerNode: ASDisplayNode {
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.containerLayout = (layout, navigationBarHeight)
|
||||
|
||||
if self.chatController != nil && self.toolbarBackgroundNode.supernode == nil {
|
||||
self.addSubnode(self.toolbarBackgroundNode)
|
||||
self.addSubnode(self.toolbarSeparatorNode)
|
||||
|
||||
self.view.addSubview(self.segmentedControl)
|
||||
}
|
||||
|
||||
var insets = layout.insets(options: [.input])
|
||||
insets.top += navigationBarHeight
|
||||
|
||||
let toolbarHeight: CGFloat = 40.0
|
||||
let panelY: CGFloat = insets.top - UIScreenPixel - 4.0
|
||||
|
||||
transition.updateFrame(node: self.toolbarBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: layout.size.width, height: toolbarHeight)))
|
||||
transition.updateFrame(node: self.toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY + toolbarHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
|
||||
|
||||
var controlSize = self.segmentedControl.sizeThatFits(layout.size)
|
||||
controlSize.width = layout.size.width - 14.0 * 2.0
|
||||
|
||||
transition.updateFrame(view: self.segmentedControl, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - controlSize.width) / 2.0), y: panelY + floor((toolbarHeight - controlSize.height) / 2.0)), size: controlSize))
|
||||
|
||||
if let chatController = self.chatController {
|
||||
insets.top += toolbarHeight - 4.0
|
||||
let chatSize = CGSize(width: layout.size.width, height: layout.size.height)
|
||||
transition.updateFrame(node: chatController.displayNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: chatSize))
|
||||
chatController.containerLayoutUpdated(ContainerViewLayout(size: chatSize, metrics: layout.metrics, intrinsicInsets: UIEdgeInsets(top: insets.top, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate)
|
||||
|
||||
if chatController.displayNode.supernode == nil {
|
||||
chatController.viewWillAppear(false)
|
||||
self.insertSubnode(chatController.displayNode, at: 0)
|
||||
chatController.viewDidAppear(false)
|
||||
chatController.displayNode.isHidden = true
|
||||
|
||||
chatController.beginMessageSearch(self.query)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@ -103,4 +162,14 @@ final class HashtagSearchControllerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func indexChanged() {
|
||||
if self.segmentedControl.selectedSegmentIndex == 0 {
|
||||
self.chatController?.displayNode.isHidden = false
|
||||
self.listNode.isHidden = true
|
||||
} else {
|
||||
self.chatController?.displayNode.isHidden = true
|
||||
self.listNode.isHidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,10 @@ class InstantImageGalleryItem: GalleryItem {
|
||||
node.setCaption(self.caption)
|
||||
}
|
||||
}
|
||||
|
||||
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
@ -55,7 +55,7 @@ final class InstantPageGalleryFooterContentNode: GalleryFooterContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
var panelHeight: CGFloat = 44.0 + bottomInset
|
||||
if !self.textNode.isHidden {
|
||||
let sideInset: CGFloat = leftInset + 8.0
|
||||
|
@ -66,7 +66,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode {
|
||||
}
|
||||
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 }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none)
|
||||
}, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings)
|
||||
|
||||
let listNode = ChatHistoryListNode(account: account, chatLocation: .peer(updatedPlaylistPeerId), tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false))
|
||||
listNode.preloadPages = true
|
||||
|
@ -137,7 +137,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
return textInputState
|
||||
}
|
||||
case .search:
|
||||
interfaceInteraction.beginMessageSearch(.member(peer))
|
||||
interfaceInteraction.beginMessageSearch(.member(peer), "")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -225,7 +225,7 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre
|
||||
convertedUrl = result
|
||||
}
|
||||
}
|
||||
} else if parsedUrl.host == "secureid" {
|
||||
} else if parsedUrl.host == "passport" {
|
||||
if let components = URLComponents(string: "/?" + query) {
|
||||
var botId: Int32?
|
||||
var scope: String?
|
||||
|
@ -59,7 +59,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec
|
||||
}, canSetupReply: { _ in
|
||||
return false
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, automaticMediaDownloadSettings: .none)
|
||||
}, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings)
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
@ -37,6 +37,10 @@ class PeerAvatarImageGalleryItem: GalleryItem {
|
||||
node.setEntry(self.entry)
|
||||
}
|
||||
}
|
||||
|
||||
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
@ -215,7 +215,7 @@ public class PeerMediaCollectionController: TelegramController {
|
||||
}, canSetupReply: { _ in
|
||||
return false
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, automaticMediaDownloadSettings: .none)
|
||||
}, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings)
|
||||
|
||||
self.controllerInteraction = controllerInteraction
|
||||
|
||||
@ -337,7 +337,7 @@ public class PeerMediaCollectionController: TelegramController {
|
||||
}, updateTextInputState: { _ in
|
||||
}, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in
|
||||
}, editMessage: {
|
||||
}, beginMessageSearch: { _ in
|
||||
}, beginMessageSearch: { _, _ in
|
||||
}, dismissMessageSearch: {
|
||||
}, updateMessageSearch: { _ in
|
||||
}, navigateMessageSearch: { _ in
|
||||
|
@ -303,6 +303,7 @@ public final class PresentationStrings {
|
||||
}
|
||||
public let Month_ShortDecember: String
|
||||
public let Channel_SignMessages: String
|
||||
public let ChatSettings_AutomaticDownloadVoiceMessage: String
|
||||
public let Conversation_Moderate_Delete: String
|
||||
public let Conversation_CloudStorage_ChatStatus: String
|
||||
public let Login_InfoTitle: String
|
||||
@ -778,6 +779,7 @@ public final class PresentationStrings {
|
||||
public let ChannelInfo_DeleteChannelConfirmation: String
|
||||
public let Weekday_ShortSaturday: String
|
||||
public let Map_SendThisLocation: String
|
||||
public let ChatSettings_AutomaticMediaDownloadMaster: String
|
||||
private let _Notification_PinnedDocumentMessage: String
|
||||
private let _Notification_PinnedDocumentMessage_r: [(Int, NSRange)]
|
||||
public func Notification_PinnedDocumentMessage(_ _0: String) -> (String, [(Int, NSRange)]) {
|
||||
@ -816,6 +818,7 @@ public final class PresentationStrings {
|
||||
public let MaskStickerSettings_Title: String
|
||||
public let TwoStepAuth_SetPassword: String
|
||||
public let GroupInfo_InviteLink_ShareLink: String
|
||||
public let ChatSettings_AutomaticDownloadFile: String
|
||||
public let Common_Cancel: String
|
||||
public let UserInfo_About_Placeholder: String
|
||||
public let ChangePhoneNumberCode_RequestingACall: String
|
||||
@ -1415,6 +1418,7 @@ public final class PresentationStrings {
|
||||
public let Profile_ShareContactButton: String
|
||||
public let Group_ErrorSendRestrictedStickers: String
|
||||
public let Bot_GroupStatusDoesNotReadHistory: String
|
||||
public let ChatSettings_AutomaticDownloadVideo: String
|
||||
public let Notification_Mute1h: String
|
||||
public let Settings_TabTitle: String
|
||||
public let NetworkUsageSettings_MediaAudioDataSection: String
|
||||
@ -2235,6 +2239,7 @@ public final class PresentationStrings {
|
||||
public let Channel_Status: String
|
||||
public let Map_ChooseLocationTitle: String
|
||||
public let Map_OpenInYandexNavigator: String
|
||||
public let ChatSettings_AutomaticDownloadPhoto: String
|
||||
public let State_WaitingForNetwork: String
|
||||
public let TwoStepAuth_EmailHelp: String
|
||||
public let Conversation_StopLiveLocation: String
|
||||
@ -2307,7 +2312,7 @@ public final class PresentationStrings {
|
||||
private let _Checkout_LiabilityAlert: String
|
||||
private let _Checkout_LiabilityAlert_r: [(Int, NSRange)]
|
||||
public func Checkout_LiabilityAlert(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) {
|
||||
return formatWithArgumentRanges(_Checkout_LiabilityAlert, self._Checkout_LiabilityAlert_r, [_1, _2])
|
||||
return formatWithArgumentRanges(_Checkout_LiabilityAlert, self._Checkout_LiabilityAlert_r, [_1, _1, _1, _2])
|
||||
}
|
||||
public let Channel_Info_BlackList: String
|
||||
public let Profile_BotInfo: String
|
||||
@ -2560,6 +2565,7 @@ public final class PresentationStrings {
|
||||
public func Login_EmailCodeBody(_ _0: String) -> (String, [(Int, NSRange)]) {
|
||||
return formatWithArgumentRanges(_Login_EmailCodeBody, self._Login_EmailCodeBody_r, [_0])
|
||||
}
|
||||
public let ChatSettings_AutomaticDownloadVideoMessage: String
|
||||
public let Profile_About: String
|
||||
private let _EncryptionKey_Description: String
|
||||
private let _EncryptionKey_Description_r: [(Int, NSRange)]
|
||||
@ -2906,6 +2912,7 @@ public final class PresentationStrings {
|
||||
public let Preview_CopyAddress: String
|
||||
public let Settings_BlockedUsers: String
|
||||
public let Month_ShortAugust: String
|
||||
public let ChatSettings_AutomaticMediaDownload: String
|
||||
public let Channel_AdminLogFilter_AdminsTitle: String
|
||||
public let Channel_EditAdmin_PermissionChangeInfo: String
|
||||
public let Notifications_ResetAllNotificationsHelp: String
|
||||
@ -2979,6 +2986,7 @@ public final class PresentationStrings {
|
||||
public let Group_ErrorAddBlocked: String
|
||||
public let TwoStepAuth_AdditionalPassword: String
|
||||
public let MediaPicker_Videos: String
|
||||
public let ChatSettings_AutomaticDownloadReset: String
|
||||
public let BlockedUsers_AddNew: String
|
||||
public let StickerPacksSettings_StickerPacksSection: String
|
||||
public let Channel_NotificationLoading: String
|
||||
@ -4942,6 +4950,7 @@ public final class PresentationStrings {
|
||||
self._Channel_AdminLog_MessageToggleSignaturesOff_r = extractArgumentRanges(self._Channel_AdminLog_MessageToggleSignaturesOff)
|
||||
self.Month_ShortDecember = getValue(dict, "Month.ShortDecember")
|
||||
self.Channel_SignMessages = getValue(dict, "Channel.SignMessages")
|
||||
self.ChatSettings_AutomaticDownloadVoiceMessage = getValue(dict, "ChatSettings.AutomaticDownloadVoiceMessage")
|
||||
self.Conversation_Moderate_Delete = getValue(dict, "Conversation.Moderate.Delete")
|
||||
self.Conversation_CloudStorage_ChatStatus = getValue(dict, "Conversation.CloudStorage.ChatStatus")
|
||||
self.Login_InfoTitle = getValue(dict, "Login.InfoTitle")
|
||||
@ -5288,6 +5297,7 @@ public final class PresentationStrings {
|
||||
self.ChannelInfo_DeleteChannelConfirmation = getValue(dict, "ChannelInfo.DeleteChannelConfirmation")
|
||||
self.Weekday_ShortSaturday = getValue(dict, "Weekday.ShortSaturday")
|
||||
self.Map_SendThisLocation = getValue(dict, "Map.SendThisLocation")
|
||||
self.ChatSettings_AutomaticMediaDownloadMaster = getValue(dict, "ChatSettings.AutomaticMediaDownloadMaster")
|
||||
self._Notification_PinnedDocumentMessage = getValue(dict, "Notification.PinnedDocumentMessage")
|
||||
self._Notification_PinnedDocumentMessage_r = extractArgumentRanges(self._Notification_PinnedDocumentMessage)
|
||||
self.Conversation_ContextMenuReply = getValue(dict, "Conversation.ContextMenuReply")
|
||||
@ -5317,6 +5327,7 @@ public final class PresentationStrings {
|
||||
self.MaskStickerSettings_Title = getValue(dict, "MaskStickerSettings.Title")
|
||||
self.TwoStepAuth_SetPassword = getValue(dict, "TwoStepAuth.SetPassword")
|
||||
self.GroupInfo_InviteLink_ShareLink = getValue(dict, "GroupInfo.InviteLink.ShareLink")
|
||||
self.ChatSettings_AutomaticDownloadFile = getValue(dict, "ChatSettings.AutomaticDownloadFile")
|
||||
self.Common_Cancel = getValue(dict, "Common.Cancel")
|
||||
self.UserInfo_About_Placeholder = getValue(dict, "UserInfo.About.Placeholder")
|
||||
self.ChangePhoneNumberCode_RequestingACall = getValue(dict, "ChangePhoneNumberCode.RequestingACall")
|
||||
@ -5739,6 +5750,7 @@ public final class PresentationStrings {
|
||||
self.Profile_ShareContactButton = getValue(dict, "Profile.ShareContactButton")
|
||||
self.Group_ErrorSendRestrictedStickers = getValue(dict, "Group.ErrorSendRestrictedStickers")
|
||||
self.Bot_GroupStatusDoesNotReadHistory = getValue(dict, "Bot.GroupStatusDoesNotReadHistory")
|
||||
self.ChatSettings_AutomaticDownloadVideo = getValue(dict, "ChatSettings.AutomaticDownloadVideo")
|
||||
self.Notification_Mute1h = getValue(dict, "Notification.Mute1h")
|
||||
self.Settings_TabTitle = getValue(dict, "Settings.TabTitle")
|
||||
self.NetworkUsageSettings_MediaAudioDataSection = getValue(dict, "NetworkUsageSettings.MediaAudioDataSection")
|
||||
@ -6274,6 +6286,7 @@ public final class PresentationStrings {
|
||||
self.Channel_Status = getValue(dict, "Channel.Status")
|
||||
self.Map_ChooseLocationTitle = getValue(dict, "Map.ChooseLocationTitle")
|
||||
self.Map_OpenInYandexNavigator = getValue(dict, "Map.OpenInYandexNavigator")
|
||||
self.ChatSettings_AutomaticDownloadPhoto = getValue(dict, "ChatSettings.AutomaticDownloadPhoto")
|
||||
self.State_WaitingForNetwork = getValue(dict, "State.WaitingForNetwork")
|
||||
self.TwoStepAuth_EmailHelp = getValue(dict, "TwoStepAuth.EmailHelp")
|
||||
self.Conversation_StopLiveLocation = getValue(dict, "Conversation.StopLiveLocation")
|
||||
@ -6491,6 +6504,7 @@ public final class PresentationStrings {
|
||||
self._PINNED_NOTEXT_r = extractArgumentRanges(self._PINNED_NOTEXT)
|
||||
self._Login_EmailCodeBody = getValue(dict, "Login.EmailCodeBody")
|
||||
self._Login_EmailCodeBody_r = extractArgumentRanges(self._Login_EmailCodeBody)
|
||||
self.ChatSettings_AutomaticDownloadVideoMessage = getValue(dict, "ChatSettings.AutomaticDownloadVideoMessage")
|
||||
self.Profile_About = getValue(dict, "Profile.About")
|
||||
self._EncryptionKey_Description = getValue(dict, "EncryptionKey.Description")
|
||||
self._EncryptionKey_Description_r = extractArgumentRanges(self._EncryptionKey_Description)
|
||||
@ -6729,6 +6743,7 @@ public final class PresentationStrings {
|
||||
self.Preview_CopyAddress = getValue(dict, "Preview.CopyAddress")
|
||||
self.Settings_BlockedUsers = getValue(dict, "Settings.BlockedUsers")
|
||||
self.Month_ShortAugust = getValue(dict, "Month.ShortAugust")
|
||||
self.ChatSettings_AutomaticMediaDownload = getValue(dict, "ChatSettings.AutomaticMediaDownload")
|
||||
self.Channel_AdminLogFilter_AdminsTitle = getValue(dict, "Channel.AdminLogFilter.AdminsTitle")
|
||||
self.Channel_EditAdmin_PermissionChangeInfo = getValue(dict, "Channel.EditAdmin.PermissionChangeInfo")
|
||||
self.Notifications_ResetAllNotificationsHelp = getValue(dict, "Notifications.ResetAllNotificationsHelp")
|
||||
@ -6781,6 +6796,7 @@ public final class PresentationStrings {
|
||||
self.Group_ErrorAddBlocked = getValue(dict, "Group.ErrorAddBlocked")
|
||||
self.TwoStepAuth_AdditionalPassword = getValue(dict, "TwoStepAuth.AdditionalPassword")
|
||||
self.MediaPicker_Videos = getValue(dict, "MediaPicker.Videos")
|
||||
self.ChatSettings_AutomaticDownloadReset = getValue(dict, "ChatSettings.AutomaticDownloadReset")
|
||||
self.BlockedUsers_AddNew = getValue(dict, "BlockedUsers.AddNew")
|
||||
self.StickerPacksSettings_StickerPacksSection = getValue(dict, "StickerPacksSettings.StickerPacksSection")
|
||||
self.Channel_NotificationLoading = getValue(dict, "Channel.NotificationLoading")
|
||||
|
@ -31,7 +31,7 @@ final class SecretMediaPreviewFooterContentNode: GalleryFooterContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
let panelHeight: CGFloat = 44.0 + bottomInset
|
||||
|
||||
let sideInset: CGFloat = leftInset + 8.0
|
||||
|
@ -376,7 +376,16 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
|
||||
return
|
||||
}
|
||||
|
||||
self.interaction.present(SecureIdPlaintextFormController(account: self.account, context: context, type: type, value: nil, updatedValue: { [weak self] valueWithContext in
|
||||
var immediatelyAvailableValue: SecureIdValue?
|
||||
switch type {
|
||||
case .phone:
|
||||
if let peer = state.encryptedFormData?.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty {
|
||||
immediatelyAvailableValue = .phone(SecureIdPhoneValue(phone: phone))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
self.interaction.present(SecureIdPlaintextFormController(account: self.account, context: context, type: type, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { [weak self] valueWithContext in
|
||||
if let strongSelf = self {
|
||||
strongSelf.interaction.updateState { state in
|
||||
if let formData = state.formData {
|
||||
|
@ -12,13 +12,12 @@ private enum SecureIdDocumentFormTextField {
|
||||
case street1
|
||||
case street2
|
||||
case city
|
||||
case region
|
||||
case state
|
||||
case postcode
|
||||
}
|
||||
|
||||
private enum SecureIdDocumentFormDateField {
|
||||
case birthdate
|
||||
case issue
|
||||
case expiry
|
||||
}
|
||||
|
||||
@ -87,16 +86,12 @@ private struct SecureIdDocumentFormIdentityDetailsState: Equatable {
|
||||
private struct SecureIdDocumentFormIdentityDocumentState: Equatable {
|
||||
var type: SecureIdRequestedIdentityDocument
|
||||
var identifier: String
|
||||
var issueDate: SecureIdDate?
|
||||
var expiryDate: SecureIdDate?
|
||||
|
||||
func isComplete() -> Bool {
|
||||
if self.identifier.isEmpty {
|
||||
return false
|
||||
}
|
||||
if self.issueDate == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -136,7 +131,7 @@ private struct SecureIdDocumentFormAddressDetailsState: Equatable {
|
||||
var street1: String
|
||||
var street2: String
|
||||
var city: String
|
||||
var region: String
|
||||
var state: String
|
||||
var countryCode: String
|
||||
var postcode: String
|
||||
|
||||
@ -208,8 +203,8 @@ private enum SecureIdDocumentFormDocumentState {
|
||||
state.details?.street2 = value
|
||||
case .city:
|
||||
state.details?.city = value
|
||||
case .region:
|
||||
state.details?.region = value
|
||||
case .state:
|
||||
state.details?.state = value
|
||||
case .postcode:
|
||||
state.details?.postcode = value
|
||||
default:
|
||||
@ -236,8 +231,6 @@ private enum SecureIdDocumentFormDocumentState {
|
||||
switch type {
|
||||
case .birthdate:
|
||||
state.details?.birthdate = value
|
||||
case .issue:
|
||||
state.document?.issueDate = value
|
||||
case .expiry:
|
||||
state.document?.expiryDate = value
|
||||
}
|
||||
@ -310,7 +303,7 @@ struct SecureIdDocumentFormState: FormControllerInnerState {
|
||||
return false
|
||||
}
|
||||
for i in 0 ..< self.documents.count {
|
||||
if !self.documents[i].isEqual(to: to.documents[i]) {
|
||||
if self.documents[i] != to.documents[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -369,7 +362,6 @@ struct SecureIdDocumentFormState: FormControllerInnerState {
|
||||
|
||||
if let document = identity.document {
|
||||
result.append(.entry(SecureIdDocumentFormEntry.identifier(document.identifier)))
|
||||
result.append(.entry(SecureIdDocumentFormEntry.issueDate(document.issueDate)))
|
||||
result.append(.entry(SecureIdDocumentFormEntry.expiryDate(document.expiryDate)))
|
||||
if !self.previousValues.isEmpty {
|
||||
result.append(.spacer)
|
||||
@ -395,7 +387,7 @@ struct SecureIdDocumentFormState: FormControllerInnerState {
|
||||
result.append(.entry(SecureIdDocumentFormEntry.street1(details.street1)))
|
||||
result.append(.entry(SecureIdDocumentFormEntry.street2(details.street2)))
|
||||
result.append(.entry(SecureIdDocumentFormEntry.city(details.city)))
|
||||
result.append(.entry(SecureIdDocumentFormEntry.region(details.region)))
|
||||
result.append(.entry(SecureIdDocumentFormEntry.state(details.state)))
|
||||
result.append(.entry(SecureIdDocumentFormEntry.countryCode(details.countryCode)))
|
||||
result.append(.entry(SecureIdDocumentFormEntry.postcode(details.postcode)))
|
||||
}
|
||||
@ -483,14 +475,12 @@ extension SecureIdDocumentFormState {
|
||||
var selfieDocument: SecureIdVerificationDocument?
|
||||
if let document = document {
|
||||
var identifier: String = ""
|
||||
var issueDate: SecureIdDate?
|
||||
var expiryDate: SecureIdDate?
|
||||
switch document {
|
||||
case .passport:
|
||||
if let value = values[.passport], case let .passport(passport) = value.value {
|
||||
previousValues[value.value.key] = value
|
||||
identifier = passport.identifier
|
||||
issueDate = passport.issueDate
|
||||
expiryDate = passport.expiryDate
|
||||
verificationDocuments = passport.verificationDocuments.compactMap(SecureIdVerificationDocument.init)
|
||||
selfieDocument = passport.selfieDocument.flatMap(SecureIdVerificationDocument.init)
|
||||
@ -499,7 +489,6 @@ extension SecureIdDocumentFormState {
|
||||
if let value = values[.driversLicense], case let .driversLicense(driversLicense) = value.value {
|
||||
previousValues[value.value.key] = value
|
||||
identifier = driversLicense.identifier
|
||||
issueDate = driversLicense.issueDate
|
||||
expiryDate = driversLicense.expiryDate
|
||||
verificationDocuments = driversLicense.verificationDocuments.compactMap(SecureIdVerificationDocument.init)
|
||||
selfieDocument = driversLicense.selfieDocument.flatMap(SecureIdVerificationDocument.init)
|
||||
@ -508,13 +497,12 @@ extension SecureIdDocumentFormState {
|
||||
if let value = values[.idCard], case let .idCard(idCard) = value.value {
|
||||
previousValues[value.value.key] = value
|
||||
identifier = idCard.identifier
|
||||
issueDate = idCard.issueDate
|
||||
expiryDate = idCard.expiryDate
|
||||
verificationDocuments = idCard.verificationDocuments.compactMap(SecureIdVerificationDocument.init)
|
||||
selfieDocument = idCard.selfieDocument.flatMap(SecureIdVerificationDocument.init)
|
||||
}
|
||||
}
|
||||
documentState = SecureIdDocumentFormIdentityDocumentState(type: document, identifier: identifier, issueDate: issueDate, expiryDate: expiryDate)
|
||||
documentState = SecureIdDocumentFormIdentityDocumentState(type: document, identifier: identifier, expiryDate: expiryDate)
|
||||
}
|
||||
let formState = SecureIdDocumentFormIdentityState(details: detailsState, document: documentState)
|
||||
self.init(previousValues: previousValues, documentState: .identity(formState), documents: verificationDocuments, selfieRequired: selfie, selfieDocument: selfieDocument, actionState: .none, errors: errors)
|
||||
@ -527,9 +515,9 @@ extension SecureIdDocumentFormState {
|
||||
if details {
|
||||
if let value = values[.address], case let .address(address) = value.value {
|
||||
previousValues[value.value.key] = value
|
||||
detailsState = SecureIdDocumentFormAddressDetailsState(street1: address.street1, street2: address.street2, city: address.city, region: address.region, countryCode: address.countryCode, postcode: address.postcode)
|
||||
detailsState = SecureIdDocumentFormAddressDetailsState(street1: address.street1, street2: address.street2, city: address.city, state: address.state, countryCode: address.countryCode, postcode: address.postcode)
|
||||
} else {
|
||||
detailsState = SecureIdDocumentFormAddressDetailsState(street1: "", street2: "", city: "", region: "", countryCode: "", postcode: "")
|
||||
detailsState = SecureIdDocumentFormAddressDetailsState(street1: "", street2: "", city: "", state: "", countryCode: "", postcode: "")
|
||||
}
|
||||
}
|
||||
if let document = document {
|
||||
@ -612,17 +600,14 @@ extension SecureIdDocumentFormState {
|
||||
guard !document.identifier.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
guard let issueDate = document.issueDate else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch document.type {
|
||||
case .passport:
|
||||
values[.passport] = .passport(SecureIdPassportValue(identifier: document.identifier, issueDate: issueDate, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument))
|
||||
values[.passport] = .passport(SecureIdPassportValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument))
|
||||
case .driversLicense:
|
||||
values[.driversLicense] = .driversLicense(SecureIdDriversLicenseValue(identifier: document.identifier, issueDate: issueDate, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument))
|
||||
values[.driversLicense] = .driversLicense(SecureIdDriversLicenseValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument))
|
||||
case .idCard:
|
||||
values[.idCard] = .idCard(SecureIdIDCardValue(identifier: document.identifier, issueDate: issueDate, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument))
|
||||
values[.idCard] = .idCard(SecureIdIDCardValue(identifier: document.identifier, expiryDate: document.expiryDate, verificationDocuments: verificationDocuments, selfieDocument: selfieDocument))
|
||||
}
|
||||
}
|
||||
return values
|
||||
@ -641,7 +626,7 @@ extension SecureIdDocumentFormState {
|
||||
guard !details.postcode.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
values[.address] = .address(SecureIdAddressValue(street1: details.street1, street2: details.street2, city: details.city, region: details.region, countryCode: details.countryCode, postcode: details.postcode))
|
||||
values[.address] = .address(SecureIdAddressValue(street1: details.street1, street2: details.street2, city: details.city, state: details.state, countryCode: details.countryCode, postcode: details.postcode))
|
||||
}
|
||||
if let document = address.document {
|
||||
switch document {
|
||||
@ -670,7 +655,6 @@ enum SecureIdDocumentFormEntryId: Hashable {
|
||||
case gender
|
||||
case countryCode
|
||||
case birthdate
|
||||
case issueDate
|
||||
case expiryDate
|
||||
case deleteDocument
|
||||
case selfieHeader
|
||||
@ -681,7 +665,7 @@ enum SecureIdDocumentFormEntryId: Hashable {
|
||||
case street1
|
||||
case street2
|
||||
case city
|
||||
case region
|
||||
case state
|
||||
case postcode
|
||||
|
||||
case error
|
||||
@ -704,7 +688,6 @@ enum SecureIdDocumentFormEntry: FormControllerEntry {
|
||||
case gender(SecureIdGender?)
|
||||
case countryCode(String)
|
||||
case birthdate(SecureIdDate?)
|
||||
case issueDate(SecureIdDate?)
|
||||
case expiryDate(SecureIdDate?)
|
||||
case deleteDocument
|
||||
case selfieHeader
|
||||
@ -716,7 +699,7 @@ enum SecureIdDocumentFormEntry: FormControllerEntry {
|
||||
case street1(String)
|
||||
case street2(String)
|
||||
case city(String)
|
||||
case region(String)
|
||||
case state(String)
|
||||
case postcode(String)
|
||||
|
||||
var stableId: SecureIdDocumentFormEntryId {
|
||||
@ -741,8 +724,6 @@ enum SecureIdDocumentFormEntry: FormControllerEntry {
|
||||
return .countryCode
|
||||
case .birthdate:
|
||||
return .birthdate
|
||||
case .issueDate:
|
||||
return .issueDate
|
||||
case .expiryDate:
|
||||
return .expiryDate
|
||||
case .deleteDocument:
|
||||
@ -753,8 +734,8 @@ enum SecureIdDocumentFormEntry: FormControllerEntry {
|
||||
return .street2
|
||||
case .city:
|
||||
return .city
|
||||
case .region:
|
||||
return .region
|
||||
case .state:
|
||||
return .state
|
||||
case .postcode:
|
||||
return .postcode
|
||||
case .gender:
|
||||
@ -781,7 +762,7 @@ enum SecureIdDocumentFormEntry: FormControllerEntry {
|
||||
return false
|
||||
}
|
||||
case let .scan(lhsId, lhsDocument):
|
||||
if case let .scan(rhsId, rhsDocument) = to, lhsId == rhsId, lhsDocument.isEqual(to: rhsDocument) {
|
||||
if case let .scan(rhsId, rhsDocument) = to, lhsId == rhsId, lhsDocument == rhsDocument {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -840,12 +821,6 @@ enum SecureIdDocumentFormEntry: FormControllerEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .issueDate(lhsValue):
|
||||
if case let .issueDate(rhsValue) = to, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .expiryDate(lhsValue):
|
||||
if case let .expiryDate(rhsValue) = to, lhsValue == rhsValue {
|
||||
return true
|
||||
@ -876,8 +851,8 @@ enum SecureIdDocumentFormEntry: FormControllerEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .region(value):
|
||||
if case .region(value) = to {
|
||||
case let .state(value):
|
||||
if case .state(value) = to {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -984,10 +959,6 @@ enum SecureIdDocumentFormEntry: FormControllerEntry {
|
||||
return FormControllerDetailActionItem(title: "Date of Birth", text: value.flatMap({ stringForDate(timestamp: $0.timestamp, strings: strings) }) ?? "", placeholder: "Date of Birth", activated: {
|
||||
params.activateSelection(.date(value?.timestamp, .birthdate))
|
||||
})
|
||||
case let .issueDate(value):
|
||||
return FormControllerDetailActionItem(title: "Issued", text: value.flatMap({ stringForDate(timestamp: $0.timestamp, strings: strings) }) ?? "", placeholder: "Issued", activated: {
|
||||
params.activateSelection(.date(value?.timestamp, .issue))
|
||||
})
|
||||
case let .expiryDate(value):
|
||||
return FormControllerDetailActionItem(title: "Expires", text: value.flatMap({ stringForDate(timestamp: $0.timestamp, strings: strings) }) ?? "", placeholder: "Expires", activated: {
|
||||
params.activateSelection(.date(value?.timestamp, .expiry))
|
||||
@ -1008,9 +979,9 @@ enum SecureIdDocumentFormEntry: FormControllerEntry {
|
||||
return FormControllerTextInputItem(title: "City", text: value, placeholder: "City", textUpdated: { text in
|
||||
params.updateText(.city, text)
|
||||
})
|
||||
case let .region(value):
|
||||
return FormControllerTextInputItem(title: "Region", text: value, placeholder: "Region", textUpdated: { text in
|
||||
params.updateText(.region, text)
|
||||
case let .state(value):
|
||||
return FormControllerTextInputItem(title: "State", text: value, placeholder: "State", textUpdated: { text in
|
||||
params.updateText(.state, text)
|
||||
})
|
||||
case let .postcode(value):
|
||||
return FormControllerTextInputItem(title: "Postcode", text: value, placeholder: "Postcode", textUpdated: { text in
|
||||
@ -1231,12 +1202,12 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode<SecureIdDocum
|
||||
case .scan:
|
||||
for resource in resources {
|
||||
let id = arc4random64()
|
||||
innerState.documents.append(.local(SecureIdVerificationLocalDocument(id: id, resource: resource, state: .uploading(0.0))))
|
||||
innerState.documents.append(.local(SecureIdVerificationLocalDocument(id: id, resource: SecureIdLocalImageResource(localId: id, source: resource), state: .uploading(0.0))))
|
||||
}
|
||||
case .selfie:
|
||||
loop: for resource in resources {
|
||||
let id = arc4random64()
|
||||
innerState.selfieDocument = .local(SecureIdVerificationLocalDocument(id: id, resource: resource, state: .uploading(0.0)))
|
||||
innerState.selfieDocument = .local(SecureIdVerificationLocalDocument(id: id, resource: SecureIdLocalImageResource(localId: id, source: resource), state: .uploading(0.0)))
|
||||
break loop
|
||||
}
|
||||
}
|
||||
@ -1281,7 +1252,7 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode<SecureIdDocum
|
||||
|
||||
var saveValues: [Signal<SecureIdValueWithContext, SaveSecureIdValueError>] = []
|
||||
for (_, value) in values {
|
||||
saveValues.append(saveSecureIdValue(network: self.account.network, context: self.context, value: value))
|
||||
saveValues.append(saveSecureIdValue(postbox: self.account.postbox, network: self.account.network, context: self.context, value: value, uploadedFiles: self.uploadContext.uploadedFiles))
|
||||
}
|
||||
|
||||
self.actionDisposable.set((combineLatest(saveValues)
|
||||
|
@ -95,7 +95,7 @@ class SecureIdDocumentGalleryController: ViewController {
|
||||
$0.item(account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, context: strongSelf.context)
|
||||
}), centralItemIndex: centralIndex, keepFirst: false)
|
||||
|
||||
let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in
|
||||
let ready = (strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void()))) |> afterNext { [weak strongSelf] _ in
|
||||
strongSelf?.didSetReady = true
|
||||
}
|
||||
strongSelf._ready.set(ready |> map { true })
|
||||
@ -252,6 +252,7 @@ class SecureIdDocumentGalleryController: ViewController {
|
||||
|
||||
let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in
|
||||
self?.didSetReady = true
|
||||
print("ready")
|
||||
}
|
||||
self._ready.set(ready |> map { true })
|
||||
}
|
||||
|
@ -43,6 +43,10 @@ class SecureIdDocumentGalleryItem: GalleryItem {
|
||||
node.setCaption(self.caption)
|
||||
}
|
||||
}
|
||||
|
||||
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
@ -66,6 +70,9 @@ final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
super.init()
|
||||
|
||||
self.imageNode.imageUpdated = { [weak self] in
|
||||
if self?.index == 1 {
|
||||
print("image updated")
|
||||
}
|
||||
self?._ready.set(.single(Void()))
|
||||
}
|
||||
|
||||
@ -92,14 +99,16 @@ final class SecureIdDocumentGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
fileprivate func setResource(context: SecureIdAccessContext, resource: TelegramMediaResource) {
|
||||
if self.accountAndMedia == nil || !self.accountAndMedia!.2.isEqual(to: resource) {
|
||||
let displaySize = CGSize(width: 1280.0, height: 1280.0)
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))()
|
||||
//self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))()
|
||||
self.imageNode.setSignal(securePhotoInternal(account: account, resource: resource, accessContext: context) |> beforeNext { [weak self] value in
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
if let size = value.0() {
|
||||
if let size = value.0(), strongSelf.zoomableContent?.0 != size {
|
||||
strongSelf.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets()))()
|
||||
if strongSelf.index == 1 {
|
||||
print("1")
|
||||
}
|
||||
strongSelf.zoomableContent = (size, strongSelf.imageNode)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ enum SecureIdErrorField: Int32, Hashable {
|
||||
case rentalAgreement
|
||||
}
|
||||
|
||||
struct SecureIdErrorKey: Hashable {
|
||||
struct SecureIdErrorKey1: Hashable {
|
||||
let category: SecureIdErrorCategory
|
||||
let field: SecureIdErrorField
|
||||
}
|
||||
|
129
TelegramUI/SecureIdLocalResource.swift
Normal file
129
TelegramUI/SecureIdLocalResource.swift
Normal file
@ -0,0 +1,129 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
|
||||
public struct SecureIdLocalImageResourceId: MediaResourceId {
|
||||
public let id: Int64
|
||||
|
||||
public var uniqueId: String {
|
||||
return "secure-id-local-\(self.id)"
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.id.hashValue
|
||||
}
|
||||
|
||||
public func isEqual(to: MediaResourceId) -> Bool {
|
||||
if let to = to as? SecureIdLocalImageResourceId {
|
||||
return self.id == to.id
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SecureIdLocalImageResource: TelegramMediaResource {
|
||||
public let localId: Int64
|
||||
public let source: TelegramMediaResource
|
||||
|
||||
public init(localId: Int64, source: TelegramMediaResource) {
|
||||
self.localId = localId
|
||||
self.source = source
|
||||
}
|
||||
|
||||
public required init(decoder: PostboxDecoder) {
|
||||
self.localId = decoder.decodeInt64ForKey("i", orElse: 0)
|
||||
self.source = decoder.decodeObjectForKey("s") as! TelegramMediaResource
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt64(self.localId, forKey: "i")
|
||||
encoder.encodeObject(self.source, forKey: "s")
|
||||
}
|
||||
|
||||
public var id: MediaResourceId {
|
||||
return SecureIdLocalImageResourceId(id: self.localId)
|
||||
}
|
||||
|
||||
public func isEqual(to: TelegramMediaResource) -> Bool {
|
||||
if let to = to as? SecureIdLocalImageResource {
|
||||
return self.localId == to.localId && self.source.isEqual(to:to.source)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class Buffer {
|
||||
var data = Data()
|
||||
}
|
||||
|
||||
func fetchSecureIdLocalImageResource(postbox: Postbox, resource: SecureIdLocalImageResource) -> Signal<MediaResourceDataFetchResult, NoError> {
|
||||
return Signal { subscriber in
|
||||
guard let fetchResource = postbox.mediaBox.fetchResource else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
subscriber.putNext(.reset)
|
||||
|
||||
let fetch = fetchResource(resource.source, .single(IndexSet(integersIn: 0 ..< Int.max)), nil)
|
||||
let buffer = Atomic<Buffer>(value: Buffer())
|
||||
let disposable = fetch.start(next: { result in
|
||||
switch result {
|
||||
case .reset:
|
||||
let _ = buffer.with { buffer in
|
||||
buffer.data.count = 0
|
||||
}
|
||||
case .resourceSizeUpdated:
|
||||
break
|
||||
case let .moveLocalFile(path):
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
|
||||
let _ = buffer.with { buffer in
|
||||
buffer.data = data
|
||||
}
|
||||
let _ = try? FileManager.default.removeItem(atPath: path)
|
||||
}
|
||||
case .copyLocalItem:
|
||||
assertionFailure()
|
||||
break
|
||||
case let .replaceHeader(data, range):
|
||||
let _ = buffer.with { buffer in
|
||||
if buffer.data.count < range.count {
|
||||
buffer.data.count = range.count
|
||||
}
|
||||
buffer.data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
data.copyBytes(to: bytes, from: range)
|
||||
}
|
||||
}
|
||||
case let .dataPart(resourceOffset, data, range, _):
|
||||
let _ = buffer.with { buffer in
|
||||
if buffer.data.count < resourceOffset + range.count {
|
||||
buffer.data.count = resourceOffset + range.count
|
||||
}
|
||||
buffer.data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
data.copyBytes(to: bytes.advanced(by: resourceOffset), from: range)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, completed: {
|
||||
let image = buffer.with { buffer -> UIImage? in
|
||||
return UIImage(data: buffer.data)
|
||||
}
|
||||
if let image = image {
|
||||
if let scaledImage = generateImage(image.size.fitted(CGSize(width: 2048.0, height: 2048.0)), contextGenerator: { size, context in
|
||||
context.setBlendMode(.copy)
|
||||
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
|
||||
}, scale: 1.0), let scaledData = UIImageJPEGRepresentation(scaledImage, 0.6) {
|
||||
subscriber.putNext(.dataPart(resourceOffset: 0, data: scaledData, range: 0 ..< scaledData.count, complete: true))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
}
|
@ -17,17 +17,17 @@ final class SecureIdPlaintextFormController: FormController<SecureIdPlaintextFor
|
||||
|
||||
private let context: SecureIdAccessContext
|
||||
private let type: SecureIdPlaintextFormType
|
||||
private var value: SecureIdValue?
|
||||
private var immediatelyAvailableValue: SecureIdValue?
|
||||
|
||||
private var nextItem: UIBarButtonItem?
|
||||
private var doneItem: UIBarButtonItem?
|
||||
|
||||
init(account: Account, context: SecureIdAccessContext, type: SecureIdPlaintextFormType, value: SecureIdValue?, updatedValue: @escaping (SecureIdValueWithContext?) -> Void) {
|
||||
init(account: Account, context: SecureIdAccessContext, type: SecureIdPlaintextFormType, immediatelyAvailableValue: SecureIdValue?, updatedValue: @escaping (SecureIdValueWithContext?) -> Void) {
|
||||
self.account = account
|
||||
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
self.context = context
|
||||
self.type = type
|
||||
self.value = value
|
||||
self.immediatelyAvailableValue = immediatelyAvailableValue
|
||||
self.updatedValue = updatedValue
|
||||
|
||||
super.init(initParams: SecureIdPlaintextFormControllerNodeInitParams(account: account, context: context), presentationData: self.presentationData)
|
||||
@ -101,6 +101,6 @@ final class SecureIdPlaintextFormController: FormController<SecureIdPlaintextFor
|
||||
self?.dismiss()
|
||||
}
|
||||
|
||||
self.controllerNode.updateInnerState(transition: .immediate, with: SecureIdPlaintextFormInnerState(type: self.type, value: self.value))
|
||||
self.controllerNode.updateInnerState(transition: .immediate, with: SecureIdPlaintextFormInnerState(type: self.type, immediatelyAvailableValue: self.immediatelyAvailableValue))
|
||||
}
|
||||
}
|
||||
|
@ -21,10 +21,12 @@ private func cleanPhoneNumber(_ text: String?) -> String {
|
||||
final class SecureIdPlaintextFormParams {
|
||||
fileprivate let openCountrySelection: () -> Void
|
||||
fileprivate let updateTextField: (SecureIdPlaintextFormTextField, String) -> Void
|
||||
fileprivate let usePhone: (String) -> Void
|
||||
|
||||
fileprivate init(openCountrySelection: @escaping () -> Void, updateTextField: @escaping (SecureIdPlaintextFormTextField, String) -> Void) {
|
||||
fileprivate init(openCountrySelection: @escaping () -> Void, updateTextField: @escaping (SecureIdPlaintextFormTextField, String) -> Void, usePhone: @escaping (String) -> Void) {
|
||||
self.openCountrySelection = openCountrySelection
|
||||
self.updateTextField = updateTextField
|
||||
self.usePhone = usePhone
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +50,7 @@ private struct PhoneInputState {
|
||||
}
|
||||
|
||||
private struct PhoneVerifyState {
|
||||
let phone: String
|
||||
let payload: SecureIdPreparePhoneVerificationPayload
|
||||
var code: String
|
||||
|
||||
@ -111,6 +114,7 @@ private struct EmailInputState {
|
||||
}
|
||||
|
||||
private struct EmailVerifyState {
|
||||
let email: String
|
||||
let payload: SecureIdPrepareEmailVerificationPayload
|
||||
var code: String
|
||||
|
||||
@ -269,6 +273,13 @@ struct SecureIdPlaintextFormInnerState: FormControllerInnerState {
|
||||
switch phone {
|
||||
case let .input(input):
|
||||
result.append(.spacer)
|
||||
|
||||
if let value = self.previousValue, case let .phone(phone) = value {
|
||||
result.append(.entry(SecureIdPlaintextFormEntry.immediatelyAvailablePhone(phone.phone)))
|
||||
result.append(.entry(SecureIdPlaintextFormEntry.immediatelyAvailablePhoneInfo))
|
||||
result.append(.spacer)
|
||||
}
|
||||
|
||||
result.append(.entry(SecureIdPlaintextFormEntry.numberInput(countryCode: input.countryCode, number: input.number)))
|
||||
result.append(.entry(SecureIdPlaintextFormEntry.numberInputInfo))
|
||||
case let .verify(verify):
|
||||
@ -333,13 +344,11 @@ struct SecureIdPlaintextFormInnerState: FormControllerInnerState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .saveAvailable
|
||||
}
|
||||
}
|
||||
|
||||
extension SecureIdPlaintextFormInnerState {
|
||||
init(type: SecureIdPlaintextFormType, value: SecureIdValue?) {
|
||||
init(type: SecureIdPlaintextFormType, immediatelyAvailableValue: SecureIdValue?) {
|
||||
switch type {
|
||||
case .phone:
|
||||
var countryId: String? = nil
|
||||
@ -364,23 +373,16 @@ extension SecureIdPlaintextFormInnerState {
|
||||
}
|
||||
}
|
||||
|
||||
self.init(previousValue: value, data: .phone(.input(PhoneInputState(countryCode: "+\(countryCodeAndId.0)", number: "", countryId: countryCodeAndId.1))), actionState: .none)
|
||||
self.init(previousValue: immediatelyAvailableValue, data: .phone(.input(PhoneInputState(countryCode: "+\(countryCodeAndId.0)", number: "", countryId: countryCodeAndId.1))), actionState: .none)
|
||||
case .email:
|
||||
self.init(previousValue: value, data: .email(.input(EmailInputState(email: ""))), actionState: .none)
|
||||
}
|
||||
}
|
||||
|
||||
func makeValue() -> SecureIdValue? {
|
||||
switch self.data {
|
||||
case let .phone(phone):
|
||||
return .phone(SecureIdPhoneValue(phone: ""))
|
||||
case let .email(email):
|
||||
return .email(SecureIdEmailValue(email: ""))
|
||||
self.init(previousValue: immediatelyAvailableValue, data: .email(.input(EmailInputState(email: ""))), actionState: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SecureIdPlaintextFormEntryId: Hashable {
|
||||
case immediatelyAvailablePhone
|
||||
case immediatelyAvailablePhoneInfo
|
||||
case numberInput
|
||||
case numberInputInfo
|
||||
case numberCode
|
||||
@ -388,75 +390,12 @@ enum SecureIdPlaintextFormEntryId: Hashable {
|
||||
case emailVerifyInfo
|
||||
case emailAddress
|
||||
case emailCode
|
||||
|
||||
static func ==(lhs: SecureIdPlaintextFormEntryId, rhs: SecureIdPlaintextFormEntryId) -> Bool {
|
||||
switch lhs {
|
||||
case .numberInput:
|
||||
if case .numberInput = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .numberInputInfo:
|
||||
if case .numberInputInfo = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .numberCode:
|
||||
if case .numberCode = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .numberVerifyInfo:
|
||||
if case .numberVerifyInfo = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .emailVerifyInfo:
|
||||
if case .emailVerifyInfo = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .emailAddress:
|
||||
if case .emailAddress = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .emailCode:
|
||||
if case .emailCode = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hashValue: Int {
|
||||
switch self {
|
||||
case .numberInput:
|
||||
return 0
|
||||
case .numberInputInfo:
|
||||
return 1
|
||||
case .numberCode:
|
||||
return 2
|
||||
case .numberVerifyInfo:
|
||||
return 3
|
||||
case .emailAddress:
|
||||
return 4
|
||||
case .emailCode:
|
||||
return 5
|
||||
case .emailVerifyInfo:
|
||||
return 6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SecureIdPlaintextFormEntry: FormControllerEntry {
|
||||
case immediatelyAvailablePhone(String)
|
||||
case immediatelyAvailablePhoneInfo
|
||||
|
||||
case numberInput(countryCode: String, number: String)
|
||||
case numberInputInfo
|
||||
case numberCode(String)
|
||||
@ -467,6 +406,10 @@ enum SecureIdPlaintextFormEntry: FormControllerEntry {
|
||||
|
||||
var stableId: SecureIdPlaintextFormEntryId {
|
||||
switch self {
|
||||
case .immediatelyAvailablePhone:
|
||||
return .immediatelyAvailablePhone
|
||||
case .immediatelyAvailablePhoneInfo:
|
||||
return .immediatelyAvailablePhoneInfo
|
||||
case .numberInput:
|
||||
return .numberInput
|
||||
case .numberInputInfo:
|
||||
@ -486,6 +429,18 @@ enum SecureIdPlaintextFormEntry: FormControllerEntry {
|
||||
|
||||
func isEqual(to: SecureIdPlaintextFormEntry) -> Bool {
|
||||
switch self {
|
||||
case let .immediatelyAvailablePhone(value):
|
||||
if case .immediatelyAvailablePhone(value) = to {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .immediatelyAvailablePhoneInfo:
|
||||
if case .immediatelyAvailablePhoneInfo = to {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .numberInput(countryCode, number):
|
||||
if case .numberInput(countryCode, number) = to {
|
||||
return true
|
||||
@ -533,6 +488,12 @@ enum SecureIdPlaintextFormEntry: FormControllerEntry {
|
||||
|
||||
func item(params: SecureIdPlaintextFormParams, strings: PresentationStrings) -> FormControllerItem {
|
||||
switch self {
|
||||
case let .immediatelyAvailablePhone(value):
|
||||
return FormControllerActionItem(type: .accent, title: formatPhoneNumber(value), activated: {
|
||||
params.usePhone(value)
|
||||
})
|
||||
case .immediatelyAvailablePhoneInfo:
|
||||
return FormControllerTextItem(text: "You can use your current Telegram phone number.")
|
||||
case let .numberInput(countryCode, number):
|
||||
var countryName = ""
|
||||
if let codeNumber = Int(countryCode), let codeId = AuthorizationSequenceCountrySelectionController.lookupCountryIdByCode(codeNumber) {
|
||||
@ -620,6 +581,8 @@ final class SecureIdPlaintextFormControllerNode: FormControllerNode<SecureIdPlai
|
||||
}
|
||||
innerState.data.updateTextField(type: type, value: value)
|
||||
strongSelf.updateInnerState(transition: .immediate, with: innerState)
|
||||
}, usePhone: { [weak self] value in
|
||||
self?.savePhone(value)
|
||||
})
|
||||
}
|
||||
|
||||
@ -649,47 +612,7 @@ final class SecureIdPlaintextFormControllerNode: FormControllerNode<SecureIdPlai
|
||||
case let .phone(phone):
|
||||
switch phone {
|
||||
case let .input(input):
|
||||
guard case .nextAvailable = innerState.actionInputState() else {
|
||||
return
|
||||
}
|
||||
innerState.actionState = .saving
|
||||
self.updateInnerState(transition: .immediate, with: innerState)
|
||||
|
||||
self.actionDisposable.set((secureIdPreparePhoneVerification(network: self.account.network, value: SecureIdPhoneValue(phone: cleanPhoneNumber(input.countryCode + input.number)))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
guard var innerState = strongSelf.innerState else {
|
||||
return
|
||||
}
|
||||
guard case .saving = innerState.actionState else {
|
||||
return
|
||||
}
|
||||
innerState.actionState = .none
|
||||
innerState.data = .phone(.verify(PhoneVerifyState(payload: result, code: "")))
|
||||
strongSelf.updateInnerState(transition: .immediate, with: innerState)
|
||||
}
|
||||
}, error: { [weak self] error in
|
||||
if let strongSelf = self {
|
||||
guard var innerState = strongSelf.innerState else {
|
||||
return
|
||||
}
|
||||
guard case .saving = innerState.actionState else {
|
||||
return
|
||||
}
|
||||
innerState.actionState = .none
|
||||
strongSelf.updateInnerState(transition: .immediate, with: innerState)
|
||||
let errorText: String
|
||||
switch error {
|
||||
case .generic:
|
||||
errorText = strongSelf.strings.Login_UnknownError
|
||||
case .flood:
|
||||
errorText = strongSelf.strings.Login_CodeFloodError
|
||||
case .occupied:
|
||||
errorText = "Please provide a number that is not used by another Telegram account."
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil)
|
||||
}
|
||||
}))
|
||||
self.savePhone(input.countryCode + input.number)
|
||||
return
|
||||
case let .verify(verify):
|
||||
guard case .saveAvailable = innerState.actionInputState() else {
|
||||
@ -698,7 +621,7 @@ final class SecureIdPlaintextFormControllerNode: FormControllerNode<SecureIdPlai
|
||||
innerState.actionState = .saving
|
||||
self.updateInnerState(transition: .immediate, with: innerState)
|
||||
|
||||
self.actionDisposable.set((secureIdCommitPhoneVerification(network: self.account.network, context: self.context, payload: verify.payload, code: verify.code)
|
||||
self.actionDisposable.set((secureIdCommitPhoneVerification(postbox: self.account.postbox, network: self.account.network, context: self.context, payload: verify.payload, code: verify.code)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
guard var innerState = strongSelf.innerState else {
|
||||
@ -754,7 +677,7 @@ final class SecureIdPlaintextFormControllerNode: FormControllerNode<SecureIdPlai
|
||||
return
|
||||
}
|
||||
innerState.actionState = .none
|
||||
innerState.data = .email(.verify(EmailVerifyState(payload: result, code: "")))
|
||||
innerState.data = .email(.verify(EmailVerifyState(email: input.email, payload: result, code: "")))
|
||||
strongSelf.updateInnerState(transition: .immediate, with: innerState)
|
||||
}
|
||||
}, error: { [weak self] error in
|
||||
@ -785,7 +708,7 @@ final class SecureIdPlaintextFormControllerNode: FormControllerNode<SecureIdPlai
|
||||
innerState.actionState = .saving
|
||||
self.updateInnerState(transition: .immediate, with: innerState)
|
||||
|
||||
self.actionDisposable.set((secureIdCommitEmailVerification(network: self.account.network, context: self.context, payload: verify.payload, code: verify.code)
|
||||
self.actionDisposable.set((secureIdCommitEmailVerification(postbox: self.account.postbox, network: self.account.network, context: self.context, payload: verify.payload, code: verify.code)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
guard var innerState = strongSelf.innerState else {
|
||||
@ -823,38 +746,54 @@ final class SecureIdPlaintextFormControllerNode: FormControllerNode<SecureIdPlai
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard case .saveAvailable = innerState.actionInputState() else {
|
||||
}
|
||||
|
||||
private func savePhone(_ value: String) {
|
||||
guard var innerState = self.innerState else {
|
||||
return
|
||||
}
|
||||
guard let value = innerState.makeValue() else {
|
||||
guard case .none = innerState.actionState else {
|
||||
return
|
||||
}
|
||||
if let previousValue = innerState.previousValue, value == previousValue {
|
||||
self.dismiss?()
|
||||
return
|
||||
}
|
||||
|
||||
innerState.actionState = .saving
|
||||
let inputPhone = cleanPhoneNumber(value)
|
||||
self.updateInnerState(transition: .immediate, with: innerState)
|
||||
|
||||
self.actionDisposable.set((saveSecureIdValue(network: self.account.network, context: self.context, value: value)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
strongSelf.completedWithValue?(result)
|
||||
}
|
||||
}, error: { [weak self] error in
|
||||
if let strongSelf = self {
|
||||
guard var innerState = strongSelf.innerState else {
|
||||
return
|
||||
self.actionDisposable.set((secureIdPreparePhoneVerification(network: self.account.network, value: SecureIdPhoneValue(phone: inputPhone))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
guard var innerState = strongSelf.innerState else {
|
||||
return
|
||||
}
|
||||
guard case .saving = innerState.actionState else {
|
||||
return
|
||||
}
|
||||
innerState.actionState = .none
|
||||
innerState.data = .phone(.verify(PhoneVerifyState(phone: inputPhone, payload: result, code: "")))
|
||||
strongSelf.updateInnerState(transition: .immediate, with: innerState)
|
||||
}
|
||||
guard case .saving = innerState.actionState else {
|
||||
return
|
||||
}
|
||||
innerState.actionState = .none
|
||||
strongSelf.updateInnerState(transition: .immediate, with: innerState)
|
||||
}
|
||||
}))
|
||||
}, error: { [weak self] error in
|
||||
if let strongSelf = self {
|
||||
guard var innerState = strongSelf.innerState else {
|
||||
return
|
||||
}
|
||||
guard case .saving = innerState.actionState else {
|
||||
return
|
||||
}
|
||||
innerState.actionState = .none
|
||||
strongSelf.updateInnerState(transition: .immediate, with: innerState)
|
||||
let errorText: String
|
||||
switch error {
|
||||
case .generic:
|
||||
errorText = strongSelf.strings.Login_UnknownError
|
||||
case .flood:
|
||||
errorText = strongSelf.strings.Login_CodeFloodError
|
||||
case .occupied:
|
||||
errorText = "Please provide a number that is not used by another Telegram account."
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.theme), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.strings.Common_OK, action: {})]), nil)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func deleteValue() {
|
||||
|
@ -1,26 +1,9 @@
|
||||
import Foundation
|
||||
import TelegramCore
|
||||
|
||||
enum SecureIdVerificationLocalDocumentState {
|
||||
enum SecureIdVerificationLocalDocumentState: Equatable {
|
||||
case uploading(Float)
|
||||
case uploaded(UploadedSecureIdFile)
|
||||
|
||||
func isEqual(to: SecureIdVerificationLocalDocumentState) -> Bool {
|
||||
switch self {
|
||||
case let .uploading(progress):
|
||||
if case .uploading(progress) = to {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .uploaded(file):
|
||||
if case .uploaded(file) = to {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SecureIdVerificationLocalDocument: Equatable {
|
||||
@ -35,20 +18,7 @@ struct SecureIdVerificationLocalDocument: Equatable {
|
||||
if !lhs.resource.isEqual(to: rhs.resource) {
|
||||
return false
|
||||
}
|
||||
if !lhs.state.isEqual(to: rhs.state) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isEqual(to: SecureIdVerificationLocalDocument) -> Bool {
|
||||
if self.id != to.id {
|
||||
return false
|
||||
}
|
||||
if !self.resource.isEqual(to: to.resource) {
|
||||
return false
|
||||
}
|
||||
if !self.state.isEqual(to: to.state) {
|
||||
if lhs.state != rhs.state {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -58,32 +28,6 @@ struct SecureIdVerificationLocalDocument: Equatable {
|
||||
enum SecureIdVerificationDocumentId: Hashable {
|
||||
case remote(Int64)
|
||||
case local(Int64)
|
||||
|
||||
static func ==(lhs: SecureIdVerificationDocumentId, rhs: SecureIdVerificationDocumentId) -> Bool {
|
||||
switch lhs {
|
||||
case let .remote(id):
|
||||
if case .remote(id) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .local(id):
|
||||
if case .local(id) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hashValue: Int {
|
||||
switch self {
|
||||
case let .local(id):
|
||||
return id.hashValue
|
||||
case let .remote(id):
|
||||
return id.hashValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SecureIdVerificationDocument: Equatable {
|
||||
@ -107,23 +51,6 @@ enum SecureIdVerificationDocument: Equatable {
|
||||
return file.resource
|
||||
}
|
||||
}
|
||||
|
||||
func isEqual(to: SecureIdVerificationDocument) -> Bool {
|
||||
switch self {
|
||||
case let .remote(reference):
|
||||
if case .remote(reference) = to {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .local(lhsDocument):
|
||||
if case let .local(rhsDocument) = to, lhsDocument.isEqual(to: rhsDocument) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SecureIdVerificationDocument {
|
||||
|
@ -21,6 +21,7 @@ final class SecureIdVerificationDocumentsContext {
|
||||
private let network: Network
|
||||
private let update: (Int64, SecureIdVerificationLocalDocumentState) -> Void
|
||||
private var contexts: [Int64: DocumentContext] = [:]
|
||||
private(set) var uploadedFiles: [Data: Data] = [:]
|
||||
|
||||
init(postbox: Postbox, network: Network, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) {
|
||||
self.postbox = postbox
|
||||
@ -47,8 +48,9 @@ final class SecureIdVerificationDocumentsContext {
|
||||
if strongSelf.contexts[info.id] != nil {
|
||||
strongSelf.update(info.id, .uploading(value))
|
||||
}
|
||||
case let .result(file):
|
||||
case let .result(file, data):
|
||||
if strongSelf.contexts[info.id] != nil {
|
||||
strongSelf.uploadedFiles[file.fileHash] = data
|
||||
strongSelf.update(info.id, .uploaded(file))
|
||||
}
|
||||
}
|
||||
|
7
TelegramUI/TGEmojiSuggestions.h
Normal file
7
TelegramUI/TGEmojiSuggestions.h
Normal file
@ -0,0 +1,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface TGEmojiSuggestions : NSObject
|
||||
|
||||
+ (NSArray *)suggestionsForQuery:(NSString *)query;
|
||||
|
||||
@end
|
49
TelegramUI/TGEmojiSuggestions.mm
Normal file
49
TelegramUI/TGEmojiSuggestions.mm
Normal file
@ -0,0 +1,49 @@
|
||||
#import "TGEmojiSuggestions.h"
|
||||
|
||||
#import "emoji_suggestions.h"
|
||||
|
||||
#import <LegacyComponents/TGAlphacode.h>
|
||||
|
||||
std::vector<Ui::Emoji::utf16char> convertToUtf16(NSString *string) {
|
||||
auto cf = (__bridge CFStringRef)string;
|
||||
auto range = CFRangeMake(0, CFStringGetLength(cf));
|
||||
auto bufferLength = CFIndex(0);
|
||||
CFStringGetBytes(cf, range, kCFStringEncodingUTF16LE, 0, FALSE, nullptr, 0, &bufferLength);
|
||||
if (!bufferLength) {
|
||||
return std::vector<Ui::Emoji::utf16char>();
|
||||
}
|
||||
auto result = std::vector<Ui::Emoji::utf16char>(bufferLength / 2 + 1, 0);
|
||||
CFStringGetBytes(cf, range, kCFStringEncodingUTF16LE, 0, FALSE, reinterpret_cast<UInt8*>(result.data()), result.size() * 2, &bufferLength);
|
||||
result.resize(bufferLength / 2);
|
||||
return result;
|
||||
}
|
||||
|
||||
NSString *convertFromUtf16(Ui::Emoji::utf16string string) {
|
||||
auto result = CFStringCreateWithBytes(nullptr, reinterpret_cast<const UInt8*>(string.data()), string.size() * 2, kCFStringEncodingUTF16LE, false);
|
||||
return (__bridge NSString*)result;
|
||||
}
|
||||
|
||||
void test() {
|
||||
|
||||
}
|
||||
|
||||
@implementation TGEmojiSuggestions
|
||||
|
||||
+ (NSArray *)suggestionsForQuery:(NSString *)queryText {
|
||||
auto query = convertToUtf16(queryText);
|
||||
auto values = Ui::Emoji::GetSuggestions(Ui::Emoji::utf16string(query.data(), query.size()));
|
||||
|
||||
NSMutableArray *array = [[NSMutableArray alloc] init];
|
||||
|
||||
for (auto &item : values) {
|
||||
NSString *emoji = convertFromUtf16(item.emoji());
|
||||
NSString *label = convertFromUtf16(item.label());
|
||||
NSString *replacement = convertFromUtf16(item.replacement());
|
||||
|
||||
[array addObject:[[TGAlphacodeEntry alloc] initWithEmoji:emoji code:replacement]];
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
@end
|
@ -23,6 +23,8 @@ public let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerC
|
||||
return fetchExternalMusicAlbumArtResource(account: account, resource: resource)
|
||||
} else if let resource = resource as? ICloudFileResource {
|
||||
return fetchICloudFileResource(resource: resource)
|
||||
} else if let resource = resource as? SecureIdLocalImageResource {
|
||||
return fetchSecureIdLocalImageResource(postbox: account.postbox, resource: resource)
|
||||
}
|
||||
return nil
|
||||
}, fetchResourceMediaReferenceHash: { resource in
|
||||
|
@ -25,4 +25,5 @@ module TelegramUIPrivateModule {
|
||||
header "../DeviceProximityManager.h"
|
||||
header "../RaiseToListenActivator.h"
|
||||
header "../TGMimeTypeMap.h"
|
||||
header "../TGEmojiSuggestions.h"
|
||||
}
|
||||
|
@ -27,6 +27,10 @@ class ThemeGalleryItem: GalleryItem {
|
||||
node.setEntry(self.entry)
|
||||
}
|
||||
}
|
||||
|
||||
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
final class ThemeGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
@ -92,7 +92,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode {
|
||||
}, canSetupReply: { _ in
|
||||
return false
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, automaticMediaDownloadSettings: .none)
|
||||
}, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings)
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
|
@ -37,7 +37,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me
|
||||
if let image = UIImage(contentsOfFile: data.path), let scaledImage = generateImage(image.size.fitted(CGSize(width: 90.0, height: 90.0)), contextGenerator: { size, context in
|
||||
context.setBlendMode(.copy)
|
||||
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
|
||||
}), let thumbnailData = UIImageJPEGRepresentation(scaledImage, 0.6) {
|
||||
}, scale: 1.0), let thumbnailData = UIImageJPEGRepresentation(scaledImage, 0.6) {
|
||||
let imageDimensions = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale)
|
||||
|
||||
let thumbnailResource = LocalFileMediaResource(fileId: arc4random64())
|
||||
|
@ -47,6 +47,10 @@ class UniversalVideoGalleryItem: GalleryItem {
|
||||
node.setupItem(self)
|
||||
}
|
||||
}
|
||||
|
||||
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private let pictureInPictureImage = UIImage(bundleImageName: "Media Gallery/PictureInPictureIcon")?.precomposed()
|
||||
|
@ -16,11 +16,12 @@ class ZoomableContentGalleryItemNode: GalleryItemNode, UIScrollViewDelegate {
|
||||
if let node = oldValue?.1 {
|
||||
node.view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
if let node = self.zoomableContent?.1 {
|
||||
self.scrollNode.addSubnode(node)
|
||||
if let node = self.zoomableContent?.1 {
|
||||
self.scrollNode.addSubnode(node)
|
||||
}
|
||||
}
|
||||
self.resetScrollViewContents(transition: .immediate)
|
||||
self.centerScrollViewContents(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
|
432
TelegramUI/emoji_suggestions.cpp
Executable file
432
TelegramUI/emoji_suggestions.cpp
Executable file
@ -0,0 +1,432 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop version of Telegram messaging app, see https://telegram.org
|
||||
|
||||
Telegram Desktop is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
In addition, as a special exception, the copyright holders give permission
|
||||
to link the code of portions of this program with the OpenSSL library.
|
||||
|
||||
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
|
||||
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
*/
|
||||
#include "emoji_suggestions.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include "emoji_suggestions_data.h"
|
||||
|
||||
#ifndef Expects
|
||||
#include <cassert>
|
||||
#define Expects(condition) assert(condition)
|
||||
#endif // Expects
|
||||
|
||||
namespace Ui {
|
||||
namespace Emoji {
|
||||
namespace internal {
|
||||
namespace {
|
||||
|
||||
checksum Crc32Table[256];
|
||||
class Crc32Initializer {
|
||||
public:
|
||||
Crc32Initializer() {
|
||||
checksum poly = 0x04C11DB7U;
|
||||
for (auto i = 0; i != 256; ++i) {
|
||||
Crc32Table[i] = reflect(i, 8) << 24;
|
||||
for (auto j = 0; j != 8; ++j) {
|
||||
Crc32Table[i] = (Crc32Table[i] << 1) ^ (Crc32Table[i] & (1 << 31) ? poly : 0);
|
||||
}
|
||||
Crc32Table[i] = reflect(Crc32Table[i], 32);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
checksum reflect(checksum val, char ch) {
|
||||
checksum result = 0;
|
||||
for (int i = 1; i < (ch + 1); ++i) {
|
||||
if (val & 1) {
|
||||
result |= 1 << (ch - i);
|
||||
}
|
||||
val >>= 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
checksum countChecksum(const void *data, std::size_t size) {
|
||||
static Crc32Initializer InitTable;
|
||||
|
||||
auto buffer = static_cast<const unsigned char*>(data);
|
||||
auto result = checksum(0xFFFFFFFFU);
|
||||
for (auto i = std::size_t(0); i != size; ++i) {
|
||||
result = (result >> 8) ^ Crc32Table[(result & 0xFFU) ^ buffer[i]];
|
||||
}
|
||||
return (result ^ 0xFFFFFFFFU);
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
namespace {
|
||||
|
||||
class string_span {
|
||||
public:
|
||||
string_span() = default;
|
||||
string_span(const utf16string *data, std::size_t size) : begin_(data), size_(size) {
|
||||
}
|
||||
string_span(const std::vector<utf16string> &data) : begin_(data.data()), size_(data.size()) {
|
||||
}
|
||||
string_span(const string_span &other) = default;
|
||||
string_span &operator=(const string_span &other) = default;
|
||||
|
||||
const utf16string *begin() const {
|
||||
return begin_;
|
||||
}
|
||||
const utf16string *end() const {
|
||||
return begin_ + size_;
|
||||
}
|
||||
std::size_t size() const {
|
||||
return size_;
|
||||
}
|
||||
|
||||
string_span subspan(std::size_t offset, std::size_t size) {
|
||||
return string_span(begin_ + offset, size);
|
||||
}
|
||||
|
||||
private:
|
||||
const utf16string *begin_ = nullptr;
|
||||
std::size_t size_ = 0;
|
||||
|
||||
};
|
||||
|
||||
bool IsNumber(utf16char ch) {
|
||||
return (ch >= '0' && ch <= '9');
|
||||
}
|
||||
|
||||
bool IsLetterOrNumber(utf16char ch) {
|
||||
return (ch >= 'a' && ch <= 'z') || IsNumber(ch);
|
||||
}
|
||||
|
||||
using Replacement = internal::Replacement;
|
||||
|
||||
class Completer {
|
||||
public:
|
||||
Completer(utf16string query);
|
||||
|
||||
std::vector<Suggestion> resolve();
|
||||
|
||||
private:
|
||||
struct Result {
|
||||
const Replacement *replacement;
|
||||
int wordsUsed;
|
||||
};
|
||||
|
||||
static std::vector<utf16char> NormalizeQuery(utf16string query);
|
||||
void addResult(const Replacement *replacement);
|
||||
bool isDuplicateOfLastResult(const Replacement *replacement) const;
|
||||
bool isBetterThanLastResult(const Replacement *replacement) const;
|
||||
void processInitialList();
|
||||
void filterInitialList();
|
||||
void initWordsTracking();
|
||||
bool matchQueryForCurrentItem();
|
||||
bool matchQueryTailStartingFrom(int position);
|
||||
string_span findWordsStartingWith(utf16char ch);
|
||||
int findEqualCharsCount(int position, const utf16string *word);
|
||||
std::vector<Suggestion> prepareResult();
|
||||
bool startsWithQuery(utf16string word);
|
||||
bool isExactMatch(utf16string replacement);
|
||||
|
||||
std::vector<Result> _result;
|
||||
|
||||
utf16string _initialQuery;
|
||||
const std::vector<utf16char> _query;
|
||||
const utf16char *_queryBegin = nullptr;
|
||||
int _querySize = 0;
|
||||
|
||||
const std::vector<const Replacement*> *_initialList = nullptr;
|
||||
|
||||
string_span _currentItemWords;
|
||||
int _currentItemWordsUsedCount = 0;
|
||||
|
||||
class UsedWordGuard {
|
||||
public:
|
||||
UsedWordGuard(std::vector<small> &map, int index);
|
||||
UsedWordGuard(const UsedWordGuard &other) = delete;
|
||||
UsedWordGuard(UsedWordGuard &&other);
|
||||
UsedWordGuard &operator=(const UsedWordGuard &other) = delete;
|
||||
UsedWordGuard &operator=(UsedWordGuard &&other) = delete;
|
||||
explicit operator bool() const;
|
||||
~UsedWordGuard();
|
||||
|
||||
private:
|
||||
std::vector<small> &_map;
|
||||
int _index = 0;
|
||||
bool _guarded = false;
|
||||
|
||||
};
|
||||
std::vector<small> _currentItemWordsUsedMap;
|
||||
|
||||
};
|
||||
|
||||
Completer::UsedWordGuard::UsedWordGuard(std::vector<small> &map, int index) : _map(map), _index(index) {
|
||||
Expects(_map.size() > _index);
|
||||
if (!_map[_index]) {
|
||||
_guarded = _map[_index] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
Completer::UsedWordGuard::UsedWordGuard(UsedWordGuard &&other) : _map(other._map), _index(other._index), _guarded(other._guarded) {
|
||||
other._guarded = 0;
|
||||
}
|
||||
|
||||
Completer::UsedWordGuard::operator bool() const {
|
||||
return _guarded;
|
||||
}
|
||||
|
||||
Completer::UsedWordGuard::~UsedWordGuard() {
|
||||
if (_guarded) {
|
||||
_map[_index] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Completer::Completer(utf16string query) : _initialQuery(query), _query(NormalizeQuery(query)) {
|
||||
}
|
||||
|
||||
// Remove all non-letters-or-numbers.
|
||||
// Leave '-' and '+' only if they're followed by a number or
|
||||
// at the end of the query (so it is possibly followed by a number).
|
||||
std::vector<utf16char> Completer::NormalizeQuery(utf16string query) {
|
||||
auto result = std::vector<utf16char>();
|
||||
result.reserve(query.size());
|
||||
auto copyFrom = query.data();
|
||||
auto e = copyFrom + query.size();
|
||||
auto copyTo = result.data();
|
||||
for (auto i = query.data(); i != e; ++i) {
|
||||
if (IsLetterOrNumber(*i)) {
|
||||
continue;
|
||||
} else if (*i == '-' || *i == '+') {
|
||||
if (i + 1 == e || IsNumber(*(i + 1))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (i > copyFrom) {
|
||||
result.resize(result.size() + (i - copyFrom));
|
||||
memcpy(copyTo, copyFrom, (i - copyFrom) * sizeof(utf16char));
|
||||
copyTo += (i - copyFrom);
|
||||
}
|
||||
copyFrom = i + 1;
|
||||
}
|
||||
if (e > copyFrom) {
|
||||
result.resize(result.size() + (e - copyFrom));
|
||||
memcpy(copyTo, copyFrom, (e - copyFrom) * sizeof(utf16char));
|
||||
copyTo += (e - copyFrom);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Suggestion> Completer::resolve() {
|
||||
_queryBegin = _query.data();
|
||||
_querySize = _query.size();
|
||||
if (!_querySize) {
|
||||
return std::vector<Suggestion>();
|
||||
}
|
||||
_initialList = Ui::Emoji::internal::GetReplacements(*_queryBegin);
|
||||
if (!_initialList) {
|
||||
return std::vector<Suggestion>();
|
||||
}
|
||||
_result.reserve(_initialList->size());
|
||||
processInitialList();
|
||||
return prepareResult();
|
||||
}
|
||||
|
||||
bool Completer::isDuplicateOfLastResult(const Replacement *item) const {
|
||||
if (_result.empty()) {
|
||||
return false;
|
||||
}
|
||||
return (_result.back().replacement->emoji == item->emoji);
|
||||
}
|
||||
|
||||
bool Completer::isBetterThanLastResult(const Replacement *item) const {
|
||||
Expects(!_result.empty());
|
||||
auto &last = _result.back();
|
||||
if (_currentItemWordsUsedCount < last.wordsUsed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto firstCharOfQuery = _query[0];
|
||||
auto firstCharAfterColonLast = last.replacement->replacement[1];
|
||||
auto firstCharAfterColonCurrent = item->replacement[1];
|
||||
auto goodLast = (firstCharAfterColonLast == firstCharOfQuery);
|
||||
auto goodCurrent = (firstCharAfterColonCurrent == firstCharOfQuery);
|
||||
return !goodLast && goodCurrent;
|
||||
}
|
||||
|
||||
void Completer::addResult(const Replacement *item) {
|
||||
if (!isDuplicateOfLastResult(item)) {
|
||||
_result.push_back({ item, _currentItemWordsUsedCount });
|
||||
} else if (isBetterThanLastResult(item)) {
|
||||
_result.back() = { item, _currentItemWordsUsedCount };
|
||||
}
|
||||
}
|
||||
|
||||
void Completer::processInitialList() {
|
||||
if (_querySize > 1) {
|
||||
filterInitialList();
|
||||
} else {
|
||||
_currentItemWordsUsedCount = 1;
|
||||
for (auto item : *_initialList) {
|
||||
addResult(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Completer::initWordsTracking() {
|
||||
auto maxWordsCount = 0;
|
||||
for (auto item : *_initialList) {
|
||||
auto wordsCount = item->words.size();
|
||||
if (maxWordsCount < wordsCount) {
|
||||
maxWordsCount = wordsCount;
|
||||
}
|
||||
}
|
||||
_currentItemWordsUsedMap = std::vector<small>(maxWordsCount, 0);
|
||||
}
|
||||
|
||||
void Completer::filterInitialList() {
|
||||
initWordsTracking();
|
||||
for (auto item : *_initialList) {
|
||||
_currentItemWords = string_span(item->words);
|
||||
_currentItemWordsUsedCount = 1;
|
||||
if (matchQueryForCurrentItem()) {
|
||||
addResult(item);
|
||||
}
|
||||
_currentItemWordsUsedCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool Completer::matchQueryForCurrentItem() {
|
||||
Expects(_currentItemWords.size() != 0);
|
||||
if (_currentItemWords.size() < 2) {
|
||||
return startsWithQuery(*_currentItemWords.begin());
|
||||
}
|
||||
return matchQueryTailStartingFrom(0);
|
||||
}
|
||||
|
||||
bool Completer::startsWithQuery(utf16string word) {
|
||||
if (word.size() < _query.size()) {
|
||||
return false;
|
||||
}
|
||||
for (auto i = std::size_t(0), size = _query.size(); i != size; ++i) {
|
||||
if (word[i] != _query[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Completer::isExactMatch(utf16string replacement) {
|
||||
if (replacement.size() != _initialQuery.size() + 1) {
|
||||
return false;
|
||||
}
|
||||
for (auto i = std::size_t(0), size = _initialQuery.size(); i != size; ++i) {
|
||||
if (replacement[i] != _initialQuery[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Completer::matchQueryTailStartingFrom(int position) {
|
||||
auto charsLeftToMatch = (_querySize - position);
|
||||
if (!charsLeftToMatch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto firstCharToMatch = *(_queryBegin + position);
|
||||
auto foundWords = findWordsStartingWith(firstCharToMatch);
|
||||
|
||||
for (auto word = foundWords.begin(), foundWordsEnd = word + foundWords.size(); word != foundWordsEnd; ++word) {
|
||||
auto wordIndex = word - _currentItemWords.begin();
|
||||
if (auto guard = UsedWordGuard(_currentItemWordsUsedMap, wordIndex)) {
|
||||
++_currentItemWordsUsedCount;
|
||||
auto equalCharsCount = findEqualCharsCount(position, word);
|
||||
for (auto check = equalCharsCount; check != 0; --check) {
|
||||
if (matchQueryTailStartingFrom(position + check)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
--_currentItemWordsUsedCount;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int Completer::findEqualCharsCount(int position, const utf16string *word) {
|
||||
auto charsLeft = (_querySize - position);
|
||||
auto wordBegin = word->data();
|
||||
auto wordSize = word->size();
|
||||
auto possibleEqualCharsCount = (charsLeft > wordSize ? wordSize : charsLeft);
|
||||
for (auto equalTill = 1; equalTill != possibleEqualCharsCount; ++equalTill) {
|
||||
auto wordCh = *(wordBegin + equalTill);
|
||||
auto queryCh = *(_queryBegin + position + equalTill);
|
||||
if (wordCh != queryCh) {
|
||||
return equalTill;
|
||||
}
|
||||
}
|
||||
return possibleEqualCharsCount;
|
||||
}
|
||||
|
||||
std::vector<Suggestion> Completer::prepareResult() {
|
||||
auto firstCharOfQuery = _query[0];
|
||||
std::stable_partition(_result.begin(), _result.end(), [firstCharOfQuery](Result &result) {
|
||||
auto firstCharAfterColon = result.replacement->replacement[1];
|
||||
return (firstCharAfterColon == firstCharOfQuery);
|
||||
});
|
||||
std::stable_partition(_result.begin(), _result.end(), [](Result &result) {
|
||||
return (result.wordsUsed < 2);
|
||||
});
|
||||
std::stable_partition(_result.begin(), _result.end(), [](Result &result) {
|
||||
return (result.wordsUsed < 3);
|
||||
});
|
||||
std::stable_partition(_result.begin(), _result.end(), [this](Result &result) {
|
||||
return isExactMatch(result.replacement->replacement);
|
||||
});
|
||||
|
||||
auto result = std::vector<Suggestion>();
|
||||
result.reserve(_result.size());
|
||||
for (auto &item : _result) {
|
||||
result.emplace_back(item.replacement->emoji, item.replacement->replacement, item.replacement->replacement);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
string_span Completer::findWordsStartingWith(utf16char ch) {
|
||||
auto begin = std::lower_bound(_currentItemWords.begin(), _currentItemWords.end(), ch, [](utf16string word, utf16char ch) {
|
||||
return word[0] < ch;
|
||||
});
|
||||
auto end = std::upper_bound(_currentItemWords.begin(), _currentItemWords.end(), ch, [](utf16char ch, utf16string word) {
|
||||
return ch < word[0];
|
||||
});
|
||||
return _currentItemWords.subspan(begin - _currentItemWords.begin(), end - begin);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<Suggestion> GetSuggestions(utf16string query) {
|
||||
return Completer(query).resolve();
|
||||
}
|
||||
|
||||
int GetSuggestionMaxLength() {
|
||||
return internal::kReplacementMaxLength;
|
||||
}
|
||||
|
||||
} // namespace Emoji
|
||||
} // namespace Ui
|
107
TelegramUI/emoji_suggestions.h
Executable file
107
TelegramUI/emoji_suggestions.h
Executable file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop version of Telegram messaging app, see https://telegram.org
|
||||
|
||||
Telegram Desktop is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
In addition, as a special exception, the copyright holders give permission
|
||||
to link the code of portions of this program with the OpenSSL library.
|
||||
|
||||
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
|
||||
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Ui {
|
||||
namespace Emoji {
|
||||
|
||||
using small = unsigned char;
|
||||
using medium = unsigned short;
|
||||
using utf16char = unsigned short;
|
||||
|
||||
static_assert(sizeof(utf16char) == 2, "Bad UTF-16 character size.");
|
||||
|
||||
class utf16string {
|
||||
public:
|
||||
utf16string() = default;
|
||||
utf16string(const utf16char *data, std::size_t size) : data_(data), size_(size) {
|
||||
}
|
||||
utf16string(const utf16string &other) = default;
|
||||
utf16string &operator=(const utf16string &other) = default;
|
||||
|
||||
const utf16char *data() const {
|
||||
return data_;
|
||||
}
|
||||
std::size_t size() const {
|
||||
return size_;
|
||||
}
|
||||
|
||||
utf16char operator[](int index) const {
|
||||
return data_[index];
|
||||
}
|
||||
|
||||
private:
|
||||
const utf16char *data_ = nullptr;
|
||||
std::size_t size_ = 0;
|
||||
|
||||
};
|
||||
|
||||
inline bool operator==(utf16string a, utf16string b) {
|
||||
return (a.size() == b.size()) && (!a.size() || !memcmp(a.data(), b.data(), a.size() * sizeof(utf16char)));
|
||||
}
|
||||
|
||||
namespace internal {
|
||||
|
||||
using checksum = unsigned int;
|
||||
checksum countChecksum(const void *data, std::size_t size);
|
||||
|
||||
utf16string GetReplacementEmoji(utf16string replacement);
|
||||
|
||||
} // namespace internal
|
||||
|
||||
class Suggestion {
|
||||
public:
|
||||
Suggestion() = default;
|
||||
Suggestion(utf16string emoji, utf16string label, utf16string replacement) : emoji_(emoji), label_(label), replacement_(replacement) {
|
||||
}
|
||||
Suggestion(const Suggestion &other) = default;
|
||||
Suggestion &operator=(const Suggestion &other) = default;
|
||||
|
||||
utf16string emoji() const {
|
||||
return emoji_;
|
||||
}
|
||||
utf16string label() const {
|
||||
return label_;
|
||||
}
|
||||
utf16string replacement() const {
|
||||
return replacement_;
|
||||
}
|
||||
|
||||
private:
|
||||
utf16string emoji_;
|
||||
utf16string label_;
|
||||
utf16string replacement_;
|
||||
|
||||
};
|
||||
|
||||
std::vector<Suggestion> GetSuggestions(utf16string query);
|
||||
|
||||
inline utf16string GetSuggestionEmoji(utf16string replacement) {
|
||||
return internal::GetReplacementEmoji(replacement);
|
||||
}
|
||||
|
||||
int GetSuggestionMaxLength();
|
||||
|
||||
|
||||
} // namespace Emoji
|
||||
} // namespace Ui
|
6367
TelegramUI/emoji_suggestions_data.cpp
Executable file
6367
TelegramUI/emoji_suggestions_data.cpp
Executable file
File diff suppressed because it is too large
Load Diff
46
TelegramUI/emoji_suggestions_data.h
Executable file
46
TelegramUI/emoji_suggestions_data.h
Executable file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
WARNING! All changes made in this file will be lost!
|
||||
Created from 'empty' by 'codegen_emoji'
|
||||
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop version of Telegram messaging app, see https://telegram.org
|
||||
|
||||
Telegram Desktop is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
In addition, as a special exception, the copyright holders give permission
|
||||
to link the code of portions of this program with the OpenSSL library.
|
||||
|
||||
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
|
||||
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "emoji_suggestions.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace Emoji {
|
||||
namespace internal {
|
||||
|
||||
struct Replacement {
|
||||
utf16string emoji;
|
||||
utf16string replacement;
|
||||
std::vector<utf16string> words;
|
||||
};
|
||||
|
||||
constexpr auto kReplacementMaxLength = 55;
|
||||
|
||||
void InitReplacements();
|
||||
const std::vector<const Replacement*> *GetReplacements(utf16char first);
|
||||
utf16string GetReplacementEmoji(utf16string replacement);
|
||||
|
||||
} // namespace internal
|
||||
} // namespace Emoji
|
||||
} // namespace Ui
|
Loading…
x
Reference in New Issue
Block a user