no message

This commit is contained in:
Peter 2018-04-11 23:43:31 +04:00
parent 1b09f8430a
commit e93d7791a7
70 changed files with 9580 additions and 710 deletions

View File

@ -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 */,

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,6 +49,10 @@ class ChatDocumentGalleryItem: GalleryItem {
node.setMessage(self.message)
}
}
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
return nil
}
}
private let registeredURLProtocol: Void = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -104,7 +104,7 @@ final class ChatRecentActionsController: ViewController {
}, updateTextInputState: { _ in
}, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in
}, editMessage: {
}, beginMessageSearch: { _ in
}, beginMessageSearch: { _, _ in
}, dismissMessageSearch: {
}, updateMessageSearch: { _ in
}, navigateMessageSearch: { _ in

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -21,4 +21,5 @@ struct GalleryItemIndexData: Equatable {
protocol GalleryItem {
func node() -> GalleryItemNode
func updateNode(node: GalleryItemNode)
func thumbnailItem() -> (Int64, GalleryThumbnailItem)?
}

View File

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

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

View File

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

View File

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

View File

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

View File

@ -41,6 +41,10 @@ class InstantImageGalleryItem: GalleryItem {
node.setCaption(self.caption)
}
}
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
return nil
}
}
final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode {

View File

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

View File

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

View File

@ -137,7 +137,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode {
return textInputState
}
case .search:
interfaceInteraction.beginMessageSearch(.member(peer))
interfaceInteraction.beginMessageSearch(.member(peer), "")
}
}
})

View File

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

View File

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

View File

@ -37,6 +37,10 @@ class PeerAvatarImageGalleryItem: GalleryItem {
node.setEntry(self.entry)
}
}
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
return nil
}
}
final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ enum SecureIdErrorField: Int32, Hashable {
case rentalAgreement
}
struct SecureIdErrorKey: Hashable {
struct SecureIdErrorKey1: Hashable {
let category: SecureIdErrorCategory
let field: SecureIdErrorField
}

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
#import <Foundation/Foundation.h>
@interface TGEmojiSuggestions : NSObject
+ (NSArray *)suggestionsForQuery:(NSString *)query;
@end

View 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

View File

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

View File

@ -25,4 +25,5 @@ module TelegramUIPrivateModule {
header "../DeviceProximityManager.h"
header "../RaiseToListenActivator.h"
header "../TGMimeTypeMap.h"
header "../TGEmojiSuggestions.h"
}

View File

@ -27,6 +27,10 @@ class ThemeGalleryItem: GalleryItem {
node.setEntry(self.entry)
}
}
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
return nil
}
}
final class ThemeGalleryItemNode: ZoomableContentGalleryItemNode {

View File

@ -92,7 +92,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode {
}, canSetupReply: { _ in
return false
}, requestMessageUpdate: { _ in
}, automaticMediaDownloadSettings: .none)
}, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings)
super.init(layerBacked: false, dynamicBounce: false)

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View 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