diff --git a/Images.xcassets/Chat/EmptyChatIcon.imageset/Contents.json b/Images.xcassets/Chat/Empty Chat/Chat.imageset/Contents.json similarity index 100% rename from Images.xcassets/Chat/EmptyChatIcon.imageset/Contents.json rename to Images.xcassets/Chat/Empty Chat/Chat.imageset/Contents.json diff --git a/Images.xcassets/Chat/EmptyChatIcon.imageset/telegram_plane_flat.pdf b/Images.xcassets/Chat/Empty Chat/Chat.imageset/telegram_plane_flat.pdf similarity index 100% rename from Images.xcassets/Chat/EmptyChatIcon.imageset/telegram_plane_flat.pdf rename to Images.xcassets/Chat/Empty Chat/Chat.imageset/telegram_plane_flat.pdf diff --git a/Images.xcassets/Chat/Empty Chat/Cloud.imageset/ChatCloudInfoIcon@2x.png b/Images.xcassets/Chat/Empty Chat/Cloud.imageset/ChatCloudInfoIcon@2x.png new file mode 100644 index 0000000000..5f9f667e25 Binary files /dev/null and b/Images.xcassets/Chat/Empty Chat/Cloud.imageset/ChatCloudInfoIcon@2x.png differ diff --git a/Images.xcassets/Chat/Empty Chat/Cloud.imageset/ChatCloudInfoIcon@3x.png b/Images.xcassets/Chat/Empty Chat/Cloud.imageset/ChatCloudInfoIcon@3x.png new file mode 100644 index 0000000000..9f1be65f30 Binary files /dev/null and b/Images.xcassets/Chat/Empty Chat/Cloud.imageset/ChatCloudInfoIcon@3x.png differ diff --git a/Images.xcassets/Chat/Empty Chat/Cloud.imageset/Contents.json b/Images.xcassets/Chat/Empty Chat/Cloud.imageset/Contents.json new file mode 100644 index 0000000000..1fd78b0e74 --- /dev/null +++ b/Images.xcassets/Chat/Empty Chat/Cloud.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ChatCloudInfoIcon@3x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ChatCloudInfoIcon@2x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Empty Chat/Contents.json b/Images.xcassets/Chat/Empty Chat/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Chat/Empty Chat/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index d49e1a8a6f..1c52dc6bb2 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -41,6 +41,9 @@ D0185E882089ED5F005E1A6C /* ProxyListSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0185E872089ED5F005E1A6C /* ProxyListSettingsController.swift */; }; D0185E8A208A01AF005E1A6C /* ProxySettingsActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0185E89208A01AF005E1A6C /* ProxySettingsActionItem.swift */; }; D0185E8C208A025A005E1A6C /* ProxySettingsServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0185E8B208A025A005E1A6C /* ProxySettingsServerItem.swift */; }; + D0192D3C210A44D00005FA10 /* DeviceContactData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0192D3B210A44D00005FA10 /* DeviceContactData.swift */; }; + D0192D44210A5AA50005FA10 /* DeviceContactDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0192D43210A5AA50005FA10 /* DeviceContactDataManager.swift */; }; + D0192D46210F4F950005FA10 /* FixSearchableListNodeScrolling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0192D45210F4F940005FA10 /* FixSearchableListNodeScrolling.swift */; }; D01A21AF1F39EA2E00DDA104 /* InstantPageTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01A21AE1F39EA2E00DDA104 /* InstantPageTheme.swift */; }; D01A21B11F3A050E00DDA104 /* InstantPageNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01A21B01F3A050E00DDA104 /* InstantPageNavigationBar.swift */; }; D01BAA181ECC8E0000295217 /* CallListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA171ECC8E0000295217 /* CallListController.swift */; }; @@ -196,6 +199,8 @@ D087BFB11F745483003FD209 /* ShareSearchBarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFB01F745483003FD209 /* ShareSearchBarNode.swift */; }; D087BFB31F748752003FD209 /* ShareControllerRecentPeersGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFB21F748752003FD209 /* ShareControllerRecentPeersGridItem.swift */; }; D08803C51F6064CF00DD7951 /* TelegramUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D0FC40821D5B8E7400261D9D /* TelegramUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D08984EE2114964700918162 /* GroupPreHistorySetupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08984ED2114964700918162 /* GroupPreHistorySetupController.swift */; }; + D08984F02114AE0C00918162 /* DataPrivacySettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08984EF2114AE0C00918162 /* DataPrivacySettingsController.swift */; }; D089F78A1F4E0C14000E934D /* InstantPagePresentationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */; }; D08BDF641FA37BEA009D08E1 /* ChatRecordingPreviewInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08BDF631FA37BEA009D08E1 /* ChatRecordingPreviewInputPanelNode.swift */; }; D08BDF661FA8CB10009D08E1 /* EditSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08BDF651FA8CB10009D08E1 /* EditSettingsController.swift */; }; @@ -945,6 +950,8 @@ D0F0AAE61EC21B68005EE2A5 /* CallControllerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0AAE51EC21B68005EE2A5 /* CallControllerButton.swift */; }; D0F19F6220E5694D00EEC860 /* GroupStickerPackCurrentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F19F6120E5694D00EEC860 /* GroupStickerPackCurrentItem.swift */; }; D0F19F6420E5A15B00EEC860 /* ChatMediaInputPeerSpecificItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F19F6320E5A15B00EEC860 /* ChatMediaInputPeerSpecificItem.swift */; }; + D0F4B01A211073C500912B92 /* DeviceContactInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F4B019211073C500912B92 /* DeviceContactInfoController.swift */; }; + D0F4B0222110972300912B92 /* ContactInfoStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F4B0212110972300912B92 /* ContactInfoStrings.swift */; }; D0F67FF01EE6B8A8000E5906 /* ChannelMembersSearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F67FEF1EE6B8A8000E5906 /* ChannelMembersSearchController.swift */; }; D0F67FF21EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F67FF11EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift */; }; D0F67FF41EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F67FF31EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift */; }; @@ -1046,6 +1053,9 @@ D0185E8B208A025A005E1A6C /* ProxySettingsServerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxySettingsServerItem.swift; sourceTree = ""; }; D018D3311E6460B300C5E089 /* ChatUnblockInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatUnblockInputPanelNode.swift; sourceTree = ""; }; D018D3341E6489EC00C5E089 /* CreateChannelController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateChannelController.swift; sourceTree = ""; }; + D0192D3B210A44D00005FA10 /* DeviceContactData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceContactData.swift; sourceTree = ""; }; + D0192D43210A5AA50005FA10 /* DeviceContactDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceContactDataManager.swift; sourceTree = ""; }; + D0192D45210F4F940005FA10 /* FixSearchableListNodeScrolling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixSearchableListNodeScrolling.swift; sourceTree = ""; }; D01A21AE1F39EA2E00DDA104 /* InstantPageTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageTheme.swift; sourceTree = ""; }; D01A21B01F3A050E00DDA104 /* InstantPageNavigationBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageNavigationBar.swift; sourceTree = ""; }; D01AC9171DD5033100E8160F /* ChatMessageActionButtonsNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageActionButtonsNode.swift; sourceTree = ""; }; @@ -1435,6 +1445,8 @@ D087BFAE1F741BB7003FD209 /* ShareLoadingContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLoadingContainerNode.swift; sourceTree = ""; }; D087BFB01F745483003FD209 /* ShareSearchBarNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSearchBarNode.swift; sourceTree = ""; }; D087BFB21F748752003FD209 /* ShareControllerRecentPeersGridItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareControllerRecentPeersGridItem.swift; sourceTree = ""; }; + D08984ED2114964700918162 /* GroupPreHistorySetupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupPreHistorySetupController.swift; sourceTree = ""; }; + D08984EF2114AE0C00918162 /* DataPrivacySettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataPrivacySettingsController.swift; sourceTree = ""; }; D089F7891F4E0C14000E934D /* InstantPagePresentationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePresentationSettings.swift; sourceTree = ""; }; D08BDF631FA37BEA009D08E1 /* ChatRecordingPreviewInputPanelNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecordingPreviewInputPanelNode.swift; sourceTree = ""; }; D08BDF651FA8CB10009D08E1 /* EditSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSettingsController.swift; sourceTree = ""; }; @@ -1875,6 +1887,8 @@ D0F3A8B51E83120A00B4C64C /* FetchResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchResource.swift; sourceTree = ""; }; D0F3A8B71E83125C00B4C64C /* MediaResources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaResources.swift; sourceTree = ""; }; D0F3A8B91E831E6300B4C64C /* FetchVideoMediaResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchVideoMediaResource.swift; sourceTree = ""; }; + D0F4B019211073C500912B92 /* DeviceContactInfoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceContactInfoController.swift; sourceTree = ""; }; + D0F4B0212110972300912B92 /* ContactInfoStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInfoStrings.swift; sourceTree = ""; }; D0F53BEB1E784DA900117362 /* ChangePhoneNumberCodeController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberCodeController.swift; sourceTree = ""; }; D0F53BF61E79593500117362 /* AuthorizationSequenceSignUpController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceSignUpController.swift; sourceTree = ""; }; D0F53BF81E79593F00117362 /* AuthorizationSequenceSignUpControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceSignUpControllerNode.swift; sourceTree = ""; }; @@ -2161,6 +2175,9 @@ isa = PBXGroup; children = ( D01C7EFF1EF9D45B008305F1 /* DeviceContactsManager.swift */, + D0192D3B210A44D00005FA10 /* DeviceContactData.swift */, + D0192D43210A5AA50005FA10 /* DeviceContactDataManager.swift */, + D0F4B0212110972300912B92 /* ContactInfoStrings.swift */, ); name = "Device Contacts"; sourceTree = ""; @@ -3545,6 +3562,7 @@ D0613FC71E5F8AB100202CDB /* ChannelInfoController.swift */, D0613FCC1E60482300202CDB /* ChannelMembersController.swift */, D0613FD41E6064D200202CDB /* ConvertToSupergroupController.swift */, + D08984ED2114964700918162 /* GroupPreHistorySetupController.swift */, D033FEAA1E61BFC100644997 /* GroupAdminsController.swift */, D0528E621E65BECA00E2FEF5 /* UserInfoController.swift */, D099261E1E69791E00D95539 /* GroupsInCommonController.swift */, @@ -3560,6 +3578,7 @@ D097C26720DD0A1D007BB4B8 /* PeerReportController.swift */, D084023320E295F000065674 /* GroupStickerPackSetupController.swift */, D0F19F6120E5694D00EEC860 /* GroupStickerPackCurrentItem.swift */, + D0F4B019211073C500912B92 /* DeviceContactInfoController.swift */, ); name = "Peer Info"; sourceTree = ""; @@ -4046,6 +4065,7 @@ D046142C2004DB1D00EC0EF2 /* Live Location Manager */, D0383ED5207D19BC00C45548 /* Emoji */, D0B69C3A20EBD8B3003632C7 /* Device Access */, + D01C7EFE1EF9D434008305F1 /* Device Contacts */, D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */, D08775081E3E59DE00A97350 /* PeerNotificationSoundStrings.swift */, D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */, @@ -4095,6 +4115,7 @@ D0CAD8FC20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift */, D044A0F220BDA05800326FAC /* ThrottledValue.swift */, D0EC55A2210231D600D1992C /* SearchPeerMembers.swift */, + D0192D45210F4F940005FA10 /* FixSearchableListNodeScrolling.swift */, ); name = Utils; sourceTree = ""; @@ -4134,6 +4155,7 @@ isa = PBXGroup; children = ( D05A32DD1E6F0097002760B4 /* PrivacyAndSecurityController.swift */, + D08984EF2114AE0C00918162 /* DataPrivacySettingsController.swift */, D05A32ED1E6F25A0002760B4 /* ItemListRecentSessionItem.swift */, D05A32E91E6F143C002760B4 /* RecentSessionsController.swift */, D05A32EB1E6F1462002760B4 /* BlockedPeersController.swift */, @@ -4189,7 +4211,6 @@ D07551891DDA4C7C0073E051 /* Legacy Components */, D0F69E911D6B8C8E0046BCD6 /* Utils */, D0B4AF891EC1132400D51FF6 /* Calls */, - D01C7EFE1EF9D434008305F1 /* Device Contacts */, D096A4601EA681720000A7AE /* Presentation Data */, D087750A1E3E7A6D00A97350 /* Settings */, D0F69DBB1D6B88330046BCD6 /* Media */, @@ -4499,6 +4520,7 @@ D0E9BAD21F0573C000F079A4 /* STPToken.m in Sources */, D0EC6CD31EB9F58800EBF1C3 /* GenerateTextEntities.swift in Sources */, D0EC6CD41EB9F58800EBF1C3 /* StringWithAppliedEntities.swift in Sources */, + D0192D46210F4F950005FA10 /* FixSearchableListNodeScrolling.swift in Sources */, D0EC6CD51EB9F58800EBF1C3 /* StoredMessageFromSearchPeer.swift in Sources */, D0471B5E1EFEB5860074D609 /* BotPaymentHeaderItemNode.swift in Sources */, D0EC6CD61EB9F58800EBF1C3 /* PreferencesKeys.swift in Sources */, @@ -4815,6 +4837,7 @@ D0EC6D8B1EB9F58800EBF1C3 /* ChatHistoryNavigationStack.swift in Sources */, D0EC6D8C1EB9F58800EBF1C3 /* NavigateToChatController.swift in Sources */, D0EC6D8D1EB9F58800EBF1C3 /* ChatMessageActionItemNode.swift in Sources */, + D0192D44210A5AA50005FA10 /* DeviceContactDataManager.swift in Sources */, D0EC6D8E1EB9F58800EBF1C3 /* ChatMessageAvatarAccessoryItem.swift in Sources */, D02D60C8206E705D00FEFE1E /* SecureIdValueFormPhoneItem.swift in Sources */, D0EC6D8F1EB9F58800EBF1C3 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */, @@ -4913,6 +4936,7 @@ D0EC6DBA1EB9F58900EBF1C3 /* ChatMediaInputNode.swift in Sources */, D0E9BA2F1F0557D400F079A4 /* STPAddress.m in Sources */, D0EC6DBB1EB9F58900EBF1C3 /* ChatMediaInputStickerPane.swift in Sources */, + D08984EE2114964700918162 /* GroupPreHistorySetupController.swift in Sources */, D0EC6DBC1EB9F58900EBF1C3 /* ChatMediaInputGifPane.swift in Sources */, D0EC6DBD1EB9F58900EBF1C3 /* ChatMediaInputPanelEntries.swift in Sources */, D0471B4F1EFD84600074D609 /* BotCheckoutPriceItem.swift in Sources */, @@ -5001,6 +5025,7 @@ D0EC6DEC1EB9F58900EBF1C3 /* ChatToastAlertPanelNode.swift in Sources */, D06D37B22077E77F009219B6 /* AutodownloadSizeLimitItem.swift in Sources */, D0EC6DED1EB9F58900EBF1C3 /* ChatHistoryNavigationButtonNode.swift in Sources */, + D0F4B01A211073C500912B92 /* DeviceContactInfoController.swift in Sources */, D0FB87B21F7C4C19004DE005 /* FetchMediaUtils.swift in Sources */, D0E9BA0C1F04580700F079A4 /* BotCheckoutWebInteractionControllerNode.swift in Sources */, D0EC6DF11EB9F58900EBF1C3 /* ShareController.swift in Sources */, @@ -5014,6 +5039,7 @@ D0EC6DF91EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceStateButtons.swift in Sources */, D07E413B208A432100FCA8F0 /* ChatListTitleProxyNode.swift in Sources */, D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */, + D08984F02114AE0C00918162 /* DataPrivacySettingsController.swift in Sources */, D087BFAD1F741B9D003FD209 /* ShareContentContainerNode.swift in Sources */, D0EC6DFB1EB9F58900EBF1C3 /* AvatarGalleryController.swift in Sources */, D0EC6DFC1EB9F58900EBF1C3 /* GalleryController.swift in Sources */, @@ -5115,6 +5141,7 @@ D0E9BA651F055B4500F079A4 /* BotCheckoutNativeCardEntryController.swift in Sources */, D02D60B3206C18A600FEFE1E /* SecureIdPlaintextFormControllerNode.swift in Sources */, D00ACA5A2022897D0045D427 /* ProcessedPeerRestrictionText.swift in Sources */, + D0192D3C210A44D00005FA10 /* DeviceContactData.swift in Sources */, D0EC6E391EB9F58900EBF1C3 /* ItemListCheckboxItem.swift in Sources */, D0EC6E3A1EB9F58900EBF1C3 /* ItemListSwitchItem.swift in Sources */, D04203152037162700490EA5 /* MediaInputPaneTrendingItem.swift in Sources */, @@ -5175,6 +5202,7 @@ D0EC6E591EB9F58900EBF1C3 /* PeerSelectionControllerNode.swift in Sources */, D0EC6E5B1EB9F58900EBF1C3 /* CallController.swift in Sources */, D0EC6E5C1EB9F58900EBF1C3 /* CallControllerNode.swift in Sources */, + D0F4B0222110972300912B92 /* ContactInfoStrings.swift in Sources */, D0EC6E5D1EB9F58900EBF1C3 /* PrivacyAndSecurityController.swift in Sources */, D04281F8200E5C17009DDE36 /* ChatControllerBackground.swift in Sources */, D0EC6E5E1EB9F58900EBF1C3 /* ItemListRecentSessionItem.swift in Sources */, diff --git a/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift b/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift index bf68f7368d..74f9eac621 100644 --- a/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift @@ -7,18 +7,13 @@ import SwiftSignalKit func authorizationCurrentOptionText(_ type: SentAuthorizationCodeType, strings: PresentationStrings, theme: AuthorizationTheme) -> NSAttributedString { switch type { case .sms: - return NSAttributedString(string: "We have sent you an SMS with a code to the number", font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center) + return NSAttributedString(string: strings.Login_CodeSentSms, font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center) case .otherSession: - let string = NSMutableAttributedString() - string.append(NSAttributedString(string: "We've sent the code to the ", font: Font.regular(16.0), textColor: theme.primaryColor)) - string.append(NSAttributedString(string: "Telegram", font: Font.medium(16.0), textColor: theme.primaryColor)) - string.append(NSAttributedString(string: " app on your other device.", font: Font.regular(16.0), textColor: theme.primaryColor)) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .center - string.addAttribute(NSAttributedStringKey.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, string.length)) - return string + let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: theme.primaryColor) + let bold = MarkdownAttributeSet(font: Font.semibold(16.0), textColor: theme.primaryColor) + return parseMarkdownIntoAttributedString(strings.Login_CodeSentInternal, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil }), textAlignment: .center) case .call, .flashCall: - return NSAttributedString(string: "Telegram dialed your number", font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center) + return NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: Font.regular(16.0), textColor: theme.primaryColor, paragraphAlignment: .center) } } diff --git a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift index 65556b54ab..baa4137d1a 100644 --- a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift +++ b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift @@ -51,285 +51,43 @@ private func loadCountryCodes() -> [(String, Int)] { private let countryCodes: [(String, Int)] = loadCountryCodes() -private final class InnerCoutrySearchResultsController: UIViewController, UITableViewDelegate, UITableViewDataSource { - private let displayCodes: Bool - private let needsSubtitle: Bool - private let theme: AuthorizationTheme +final class AuthorizationSequenceCountrySelectionTheme { + let statusBar: PresentationThemeStatusBarStyle + let searchBar: SearchBarNodeTheme + let listBackground: UIColor + let listSeparator: UIColor + let listAccent: UIColor + let listPrimary: UIColor + let listItemHighlight: UIColor - private let tableView: UITableView + init(statusBar: PresentationThemeStatusBarStyle, searchBar: SearchBarNodeTheme, listBackground: UIColor, listSeparator: UIColor, listAccent: UIColor, listPrimary: UIColor, listItemHighlight: UIColor) { + self.statusBar = statusBar + self.searchBar = searchBar + self.listBackground = listBackground + self.listSeparator = listSeparator + self.listAccent = listAccent + self.listPrimary = listPrimary + self.listItemHighlight = listItemHighlight + } - var searchResults: [((String, String), String, Int)] = [] { - didSet { - self.tableView.reloadData() + convenience init(presentationTheme: PresentationTheme) { + self.init(statusBar: presentationTheme.rootController.statusBar.style, searchBar: SearchBarNodeTheme(theme: presentationTheme), listBackground: presentationTheme.list.plainBackgroundColor, listSeparator: presentationTheme.list.itemPlainSeparatorColor, listAccent: presentationTheme.list.itemAccentColor, listPrimary: presentationTheme.list.itemPrimaryTextColor, listItemHighlight: presentationTheme.list.itemHighlightedBackgroundColor) + } + + convenience init(authorizationTheme: AuthorizationTheme) { + let keyboard: PresentationThemeKeyboardColor + switch authorizationTheme.keyboardAppearance { + case .dark: + keyboard = .dark + default: + keyboard = .light } - } - - var itemSelected: ((((String, String), String, Int)) -> Void)? - - init(strings: PresentationStrings, theme: AuthorizationTheme, displayCodes: Bool, needsSubtitle: Bool) { - self.displayCodes = displayCodes - self.theme = theme - self.needsSubtitle = needsSubtitle - - self.tableView = UITableView(frame: CGRect(), style: .plain) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - self.view.backgroundColor = .white - - self.view.addSubview(self.tableView) - self.tableView.frame = self.view.bounds - self.tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - self.tableView.dataSource = self - self.tableView.delegate = self - - self.tableView.backgroundColor = self.theme.backgroundColor - self.tableView.separatorColor = self.theme.separatorColor - self.tableView.backgroundView = UIView() - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.searchResults.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: UITableViewCell - if let currentCell = tableView.dequeueReusableCell(withIdentifier: "CountryCell") { - cell = currentCell - } else { - cell = UITableViewCell(style: self.needsSubtitle ? .subtitle : .default, reuseIdentifier: "CountryCell") - let label = UILabel() - label.font = Font.medium(17.0) - cell.accessoryView = label - cell.selectedBackgroundView = UIView() - } - cell.textLabel?.text = self.searchResults[indexPath.row].0.1 - cell.detailTextLabel?.text = self.searchResults[indexPath.row].0.0 - if self.displayCodes, let label = cell.accessoryView as? UILabel { - label.text = "+\(self.searchResults[indexPath.row].2)" - label.sizeToFit() - label.textColor = self.theme.primaryColor - } - cell.textLabel?.textColor = self.theme.primaryColor - cell.detailTextLabel?.textColor = self.theme.primaryColor - cell.backgroundColor = self.theme.backgroundColor - cell.selectedBackgroundView?.backgroundColor = self.theme.itemHighlightedBackgroundColor - return cell - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - self.itemSelected?(self.searchResults[indexPath.row]) - } -} - -private final class InnerCountrySelectionController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdating, UISearchBarDelegate { - private let strings: PresentationStrings - private let theme: AuthorizationTheme - private let displayCodes: Bool - private let needsSubtitle: Bool - - private let tableView: UITableView - - private let sections: [(String, [((String, String), String, Int)])] - private let sectionTitles: [String] - - private var searchController: UISearchController! - private var searchResultsController: InnerCoutrySearchResultsController! - - var dismiss: (() -> Void)? - var itemSelected: ((((String, String), String, Int)) -> Void)? - - init(strings: PresentationStrings, theme: AuthorizationTheme, displayCodes: Bool) { - self.strings = strings - self.theme = theme - self.displayCodes = displayCodes - self.needsSubtitle = strings.languageCode != "en" - - self.tableView = UITableView(frame: CGRect(), style: .plain) - - let countryNamesAndCodes = localizedContryNamesAndCodes(strings: strings) - - var sections: [(String, [((String, String), String, Int)])] = [] - for (names, id, code) in countryNamesAndCodes.sorted(by: { lhs, rhs in - return lhs.0 < rhs.0 - }) { - let title = String(names.1[names.1.startIndex ..< names.1.index(after: names.1.startIndex)]).uppercased() - if sections.isEmpty || sections[sections.count - 1].0 != title { - sections.append((title, [])) - } - sections[sections.count - 1].1.append((names, id, code)) - } - self.sections = sections - var sectionTitles = sections.map { $0.0 } - sectionTitles.insert(UITableViewIndexSearch, at: 0) - self.sectionTitles = sectionTitles - - super.init(nibName: nil, bundle: nil) - - self.title = strings.Login_SelectCountry_Title - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) - - self.definesPresentationContext = true - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - self.view.backgroundColor = .white - - self.searchResultsController = InnerCoutrySearchResultsController(strings: self.strings, theme: self.theme, displayCodes: self.displayCodes, needsSubtitle: self.needsSubtitle) - self.searchResultsController.itemSelected = { [weak self] item in - self?.itemSelected?(item) - } - - self.searchController = UISearchController(searchResultsController: self.searchResultsController) - self.searchController.searchResultsUpdater = self - self.searchController.dimsBackgroundDuringPresentation = false - self.searchController.searchBar.delegate = self - self.searchController.searchBar.keyboardAppearance = self.theme.keyboardAppearance - self.searchController.hidesNavigationBarDuringPresentation = true - - self.view.addSubview(self.tableView) - self.tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - self.tableView.tableHeaderView = self.searchController.searchBar - self.tableView.dataSource = self - self.tableView.delegate = self - self.tableView.sectionIndexColor = self.theme.accentColor - - self.tableView.backgroundColor = self.theme.backgroundColor - self.tableView.separatorColor = self.theme.separatorColor - self.tableView.backgroundView = UIView() - - self.tableView.frame = self.view.bounds - self.view.addSubview(self.tableView) - - self.searchController.searchBar.barTintColor = self.theme.searchBarBackgroundColor - self.searchController.searchBar.tintColor = self.theme.accentColor - self.searchController.searchBar.backgroundColor = self.theme.searchBarBackgroundColor - self.searchController.searchBar.setTextColor(theme.searchBarTextColor) - - - let searchImage = generateImage(CGSize(width: 8.0, height: 28.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(self.theme.searchBarFillColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - size.width), size: CGSize(width: size.width, height: size.width))) - context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.width / 2.0), size: CGSize(width: size.width, height: size.height - size.width))) - }) - self.searchController.searchBar.setSearchFieldBackgroundImage(searchImage, for: []) - self.searchController.searchBar.backgroundImage = UIImage() - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - - if #available(iOSApplicationExtension 11.0, *) { - var frame = self.searchController.view.frame - frame.origin.y = 12.0 - self.searchController.view.frame = frame - } - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - - } - - func numberOfSections(in tableView: UITableView) -> Int { - return self.sections.count - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.sections[section].1.count - } - - func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - return self.sections[section].0 - } - - func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { - (view as? UITableViewHeaderFooterView)?.backgroundView?.backgroundColor = self.theme.backgroundColor - (view as? UITableViewHeaderFooterView)?.textLabel?.textColor = self.theme.primaryColor - } - - func sectionIndexTitles(for tableView: UITableView) -> [String]? { - return self.sectionTitles - } - - func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { - if index == 0 { - return 0 - } else { - return max(0, index - 1) - } - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: UITableViewCell - if let currentCell = tableView.dequeueReusableCell(withIdentifier: "CountryCell") { - cell = currentCell - } else { - cell = UITableViewCell(style: self.needsSubtitle ? .subtitle : .default, reuseIdentifier: "CountryCell") - let label = UILabel() - label.font = Font.medium(17.0) - cell.accessoryView = label - cell.selectedBackgroundView = UIView() - } - cell.textLabel?.text = self.sections[indexPath.section].1[indexPath.row].0.1 - cell.detailTextLabel?.text = self.sections[indexPath.section].1[indexPath.row].0.0 - if self.displayCodes, let label = cell.accessoryView as? UILabel { - label.text = "+\(self.sections[indexPath.section].1[indexPath.row].2)" - label.sizeToFit() - label.textColor = self.theme.primaryColor - } - cell.textLabel?.textColor = self.theme.primaryColor - cell.detailTextLabel?.textColor = self.theme.primaryColor - cell.backgroundColor = self.theme.backgroundColor - cell.selectedBackgroundView?.backgroundColor = self.theme.itemHighlightedBackgroundColor - return cell - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - self.itemSelected?(self.sections[indexPath.section].1[indexPath.row]) - } - - func updateSearchResults(for searchController: UISearchController) { - guard let normalizedQuery = searchController.searchBar.text?.lowercased() else { - self.searchResultsController.searchResults = [] - return - } - - var results: [((String, String), String, Int)] = [] - for (_, items) in self.sections { - for item in items { - if item.0.0.lowercased().hasPrefix(normalizedQuery) || item.0.1.lowercased().hasPrefix(normalizedQuery) { - results.append(item) - } - } - } - self.searchResultsController.searchResults = results - } - - @objc func cancelPressed() { - self.dismiss?() + self.init(statusBar: PresentationThemeStatusBarStyle(authorizationTheme.statusBarStyle), searchBar: SearchBarNodeTheme(background: authorizationTheme.navigationBarBackgroundColor, separator: authorizationTheme.navigationBarSeparatorColor, inputFill: authorizationTheme.searchBarFillColor, primaryText: authorizationTheme.searchBarTextColor, placeholder: authorizationTheme.searchBarPlaceholderColor, inputIcon: authorizationTheme.searchBarPlaceholderColor, inputClear: authorizationTheme.searchBarPlaceholderColor, accent: authorizationTheme.accentColor, keyboard: keyboard), listBackground: authorizationTheme.backgroundColor, listSeparator: authorizationTheme.separatorColor, listAccent: authorizationTheme.accentColor, listPrimary: authorizationTheme.primaryColor, listItemHighlight: authorizationTheme.itemHighlightedBackgroundColor) } } private final class AuthorizationSequenceCountrySelectionNavigationContentNode: NavigationBarContentNode { - private let theme: AuthorizationTheme + private let theme: AuthorizationSequenceCountrySelectionTheme private let strings: PresentationStrings private let cancel: () -> Void @@ -338,17 +96,17 @@ private final class AuthorizationSequenceCountrySelectionNavigationContentNode: private var queryUpdated: ((String) -> Void)? - init(theme: AuthorizationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) { + init(theme: AuthorizationSequenceCountrySelectionTheme, strings: PresentationStrings, cancel: @escaping () -> Void) { self.theme = theme self.strings = strings self.cancel = cancel - self.searchBar = SearchBarNode(theme: defaultDarkPresentationTheme, strings: strings) + self.searchBar = SearchBarNode(theme: theme.searchBar, strings: strings) let placeholderText = strings.Common_Search let searchBarFont = Font.regular(14.0) - self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.searchBarTextColor) + self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.searchBar.placeholder) super.init() @@ -410,7 +168,7 @@ final class AuthorizationSequenceCountrySelectionController: ViewController { return nil } - private let theme: AuthorizationTheme + private let theme: AuthorizationSequenceCountrySelectionTheme private let strings: PresentationStrings private let displayCodes: Bool @@ -423,14 +181,14 @@ final class AuthorizationSequenceCountrySelectionController: ViewController { var completeWithCountryCode: ((Int, String) -> Void)? var dismissed: (() -> Void)? - init(strings: PresentationStrings, theme: AuthorizationTheme, displayCodes: Bool = true) { + init(strings: PresentationStrings, theme: AuthorizationSequenceCountrySelectionTheme, displayCodes: Bool = true) { self.theme = theme self.strings = strings self.displayCodes = displayCodes - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: AuthorizationSequenceController.navigationBarTheme(theme), strings: NavigationBarStrings(presentationStrings: strings))) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(buttonColor: theme.searchBar.accent, primaryTextColor: theme.searchBar.primaryText, backgroundColor: theme.searchBar.background, separatorColor: theme.searchBar.separator, badgeBackgroundColor: theme.searchBar.accent, badgeStrokeColor: .clear, badgeTextColor: theme.searchBar.background), strings: NavigationBarStrings(presentationStrings: strings))) - self.statusBar.statusBarStyle = theme.statusBarStyle + self.statusBar.statusBarStyle = theme.statusBar.style let navigationContentNode = AuthorizationSequenceCountrySelectionNavigationContentNode(theme: theme, strings: strings, cancel: { [weak self] in self?.dismissed?() diff --git a/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift b/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift index b85791435a..922a9d1dc9 100644 --- a/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift @@ -68,7 +68,7 @@ func localizedContryNamesAndCodes(strings: PresentationStrings) -> [((String, St final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, UITableViewDelegate, UITableViewDataSource { let itemSelected: (((String, String), String, Int)) -> Void - private let theme: AuthorizationTheme + private let theme: AuthorizationSequenceCountrySelectionTheme private let strings: PresentationStrings private let displayCodes: Bool private let needsSubtitle: Bool @@ -81,7 +81,7 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, private var searchResults: [((String, String), String, Int)] = [] - init(theme: AuthorizationTheme, strings: PresentationStrings, displayCodes: Bool, itemSelected: @escaping (((String, String), String, Int)) -> Void) { + init(theme: AuthorizationSequenceCountrySelectionTheme, strings: PresentationStrings, displayCodes: Bool, itemSelected: @escaping (((String, String), String, Int)) -> Void) { self.theme = theme self.strings = strings self.displayCodes = displayCodes @@ -116,21 +116,21 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, return UITracingLayerView() }) - self.backgroundColor = theme.backgroundColor + self.backgroundColor = theme.listBackground - self.tableView.backgroundColor = theme.backgroundColor + self.tableView.backgroundColor = theme.listBackground - self.tableView.backgroundColor = self.theme.backgroundColor - self.tableView.separatorColor = self.theme.separatorColor + self.tableView.backgroundColor = self.theme.listBackground + self.tableView.separatorColor = self.theme.listSeparator self.tableView.backgroundView = UIView() - self.tableView.sectionIndexColor = self.theme.accentColor + self.tableView.sectionIndexColor = self.theme.searchBar.accent - self.searchTableView.backgroundColor = theme.backgroundColor + self.searchTableView.backgroundColor = theme.listBackground - self.searchTableView.backgroundColor = self.theme.backgroundColor - self.searchTableView.separatorColor = self.theme.separatorColor + self.searchTableView.backgroundColor = self.theme.listBackground + self.searchTableView.separatorColor = self.theme.listSeparator self.searchTableView.backgroundView = UIView() - self.searchTableView.sectionIndexColor = self.theme.accentColor + self.searchTableView.sectionIndexColor = self.theme.searchBar.accent self.tableView.delegate = self self.tableView.dataSource = self @@ -205,8 +205,8 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, } func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { - (view as? UITableViewHeaderFooterView)?.backgroundView?.backgroundColor = self.theme.backgroundColor - (view as? UITableViewHeaderFooterView)?.textLabel?.textColor = self.theme.primaryColor + (view as? UITableViewHeaderFooterView)?.backgroundView?.backgroundColor = self.theme.listBackground + (view as? UITableViewHeaderFooterView)?.textLabel?.textColor = self.theme.listPrimary } func sectionIndexTitles(for tableView: UITableView) -> [String]? { @@ -259,12 +259,12 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, if self.displayCodes, let label = cell.accessoryView as? UILabel { label.text = code label.sizeToFit() - label.textColor = self.theme.primaryColor + label.textColor = self.theme.listPrimary } - cell.textLabel?.textColor = self.theme.primaryColor - cell.detailTextLabel?.textColor = self.theme.primaryColor - cell.backgroundColor = self.theme.backgroundColor - cell.selectedBackgroundView?.backgroundColor = self.theme.itemHighlightedBackgroundColor + cell.textLabel?.textColor = self.theme.listPrimary + cell.detailTextLabel?.textColor = self.theme.listPrimary + cell.backgroundColor = self.theme.listBackground + cell.selectedBackgroundView?.backgroundColor = self.theme.listItemHighlight return cell } diff --git a/TelegramUI/AuthorizationSequencePhoneEntryController.swift b/TelegramUI/AuthorizationSequencePhoneEntryController.swift index bcb9cf0965..cc54aafd46 100644 --- a/TelegramUI/AuthorizationSequencePhoneEntryController.swift +++ b/TelegramUI/AuthorizationSequencePhoneEntryController.swift @@ -57,11 +57,9 @@ final class AuthorizationSequencePhoneEntryController: ViewController { } func updateData(countryCode: Int32, number: String) { - if self.currentData == nil || self.currentData! != (countryCode, number) { - self.currentData = (countryCode, number) - if self.isNodeLoaded { - self.controllerNode.codeAndNumber = (countryCode, number) - } + self.currentData = (countryCode, number) + if self.isNodeLoaded { + self.controllerNode.codeAndNumber = (countryCode, number) } } @@ -73,7 +71,7 @@ final class AuthorizationSequencePhoneEntryController: ViewController { self.displayNodeDidLoad() self.controllerNode.selectCountryCode = { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.strings, theme: strongSelf.theme) + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.strings, theme: AuthorizationSequenceCountrySelectionTheme(authorizationTheme: strongSelf.theme)) controller.completeWithCountryCode = { code, _ in if let strongSelf = self, let currentData = strongSelf.currentData { strongSelf.updateData(countryCode: Int32(code), number: currentData.1) @@ -111,7 +109,7 @@ final class AuthorizationSequencePhoneEntryController: ViewController { } @objc func nextPressed() { - let (code, number) = self.controllerNode.codeAndNumber + let (_, number) = self.controllerNode.codeAndNumber if !number.isEmpty { self.loginWithNumber?(self.controllerNode.currentNumber) } else { diff --git a/TelegramUI/AutomaticMediaDownloadSettings.swift b/TelegramUI/AutomaticMediaDownloadSettings.swift index da689ef73d..8e294b9522 100644 --- a/TelegramUI/AutomaticMediaDownloadSettings.swift +++ b/TelegramUI/AutomaticMediaDownloadSettings.swift @@ -205,7 +205,7 @@ private func categoriesForPeerType(_ type: AutomaticMediaDownloadPeerType, setti private func categoryForPeerAndMedia(settings: AutomaticMediaDownloadSettings, peer: Peer, media: Media) -> (AutomaticMediaDownloadCategory, Int32?)? { let categories = categoriesForPeerType(tempPeerTypeForPeer(peer), settings: settings) - if let _ = media as? TelegramMediaImage { + if media is TelegramMediaImage || media is TelegramMediaWebFile { return (categories.photo, nil) } else if let file = media as? TelegramMediaFile { for attribute in file.attributes { @@ -224,6 +224,8 @@ private func categoryForPeerAndMedia(settings: AutomaticMediaDownloadSettings, p if isVoice { return (categories.voiceMessage, file.size.flatMap(Int32.init)) } + case .Animated: + return (categories.videoMessage, file.size.flatMap(Int32.init)) default: break } diff --git a/TelegramUI/AvatarGalleryController.swift b/TelegramUI/AvatarGalleryController.swift index 83304ef6cf..1cac731df0 100644 --- a/TelegramUI/AvatarGalleryController.swift +++ b/TelegramUI/AvatarGalleryController.swift @@ -79,7 +79,7 @@ func fetchedAvatarGalleryEntries(account: Account, peer: Peer) -> Signal<[Avatar for photo in photos { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) if result.isEmpty, let first = initialEntries.first { - let image = TelegramMediaImage(imageId: photo.image.imageId, representations: first.representations, reference: photo.reference) + let image = TelegramMediaImage(imageId: photo.image.imageId, representations: first.representations, reference: photo.reference, partialReference: nil) result.append(.image(image, indexData)) } else { result.append(.image(photo.image, indexData)) diff --git a/TelegramUI/AvatarNode.swift b/TelegramUI/AvatarNode.swift index f43f1940a5..ee3cc2d0c0 100644 --- a/TelegramUI/AvatarNode.swift +++ b/TelegramUI/AvatarNode.swift @@ -14,13 +14,15 @@ private class AvatarNodeParameters: NSObject { let letters: [String] let font: UIFont let savedMessagesIcon: Bool + let explicitColorIndex: Int? - init(accountPeerId: PeerId?, peerId: PeerId?, letters: [String], font: UIFont, savedMessagesIcon: Bool) { + init(accountPeerId: PeerId?, peerId: PeerId?, letters: [String], font: UIFont, savedMessagesIcon: Bool, explicitColorIndex: Int?) { self.accountPeerId = accountPeerId self.peerId = peerId self.letters = letters self.font = font self.savedMessagesIcon = savedMessagesIcon + self.explicitColorIndex = explicitColorIndex super.init() } @@ -47,7 +49,7 @@ private let savedMessagesColors: NSArray = [ private enum AvatarNodeState: Equatable { case empty case peerAvatar(PeerId, [String], TelegramMediaImageRepresentation?) - case custom([String]) + case custom(letter: [String], explicitColorIndex: Int?) } private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool { @@ -56,8 +58,8 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool { return true case let (.peerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations), .peerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations)): return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations - case let (.custom(lhsLetters), .custom(rhsLetters)): - return lhsLetters == rhsLetters + case let (.custom(lhsLetters, lhsIndex), .custom(rhsLetters, rhsIndex)): + return lhsLetters == rhsLetters && lhsIndex == rhsIndex default: return false } @@ -69,12 +71,16 @@ public enum AvatarNodeImageOverride { case savedMessagesIcon } +public enum AvatarNodeColorOverride { + case blue +} + public final class AvatarNode: ASDisplayNode { var font: UIFont { didSet { if oldValue !== font { if let parameters = self.parameters { - self.parameters = AvatarNodeParameters(accountPeerId: parameters.accountPeerId, peerId: parameters.peerId, letters: parameters.letters, font: self.font, savedMessagesIcon: parameters.savedMessagesIcon) + self.parameters = AvatarNodeParameters(accountPeerId: parameters.accountPeerId, peerId: parameters.peerId, letters: parameters.letters, font: self.font, savedMessagesIcon: parameters.savedMessagesIcon, explicitColorIndex: parameters.explicitColorIndex) } if !self.displaySuspended { @@ -126,7 +132,7 @@ public final class AvatarNode: ASDisplayNode { } } - public func setPeer(account: Account, peer: Peer, overrideImage: AvatarNodeImageOverride? = nil) { + public func setPeer(account: Account, peer: Peer, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil) { var representation: TelegramMediaImageRepresentation? var savedMessagesIcon = false if let overrideImage = overrideImage { @@ -146,12 +152,12 @@ public final class AvatarNode: ASDisplayNode { if updatedState != self.state { self.state = updatedState - let parameters = AvatarNodeParameters(accountPeerId: account.peerId, peerId: peer.id, letters: peer.displayLetters, font: self.font, savedMessagesIcon: savedMessagesIcon) + let parameters = AvatarNodeParameters(accountPeerId: account.peerId, peerId: peer.id, letters: peer.displayLetters, font: self.font, savedMessagesIcon: savedMessagesIcon, explicitColorIndex: nil) self.displaySuspended = true self.contents = nil - if let signal = peerAvatarImage(account: account, peer: peer, representation: representation) { + if let signal = peerAvatarImage(account: account, peer: peer, authorOfMessage: authorOfMessage, representation: representation) { self.imageReady.set(self.imageNode.ready) self.imageNode.setSignal(signal) } else { @@ -168,12 +174,19 @@ public final class AvatarNode: ASDisplayNode { } } - public func setCustomLetters(_ letters: [String]) { - let updatedState: AvatarNodeState = .custom(letters) + public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil) { + var explicitIndex: Int? + if let explicitColor = explicitColor { + switch explicitColor { + case .blue: + explicitIndex = 5 + } + } + let updatedState: AvatarNodeState = .custom(letter: letters, explicitColorIndex: explicitIndex) if updatedState != self.state { self.state = updatedState - let parameters = AvatarNodeParameters(accountPeerId: nil, peerId: nil, letters: letters, font: self.font, savedMessagesIcon: false) + let parameters = AvatarNodeParameters(accountPeerId: nil, peerId: nil, letters: letters, font: self.font, savedMessagesIcon: false, explicitColorIndex: explicitIndex) self.displaySuspended = true self.contents = nil @@ -210,14 +223,18 @@ public final class AvatarNode: ASDisplayNode { let colorIndex: Int if let parameters = parameters as? AvatarNodeParameters { - if let accountPeerId = parameters.accountPeerId, let peerId = parameters.peerId { - if peerId.namespace == -1 { - colorIndex = -1 - } else { - colorIndex = abs(Int(accountPeerId.id + peerId.id)) - } + if let explicitColorIndex = parameters.explicitColorIndex { + colorIndex = explicitColorIndex } else { - colorIndex = -1 + if let accountPeerId = parameters.accountPeerId, let peerId = parameters.peerId { + if peerId.namespace == -1 { + colorIndex = -1 + } else { + colorIndex = abs(Int(accountPeerId.id + peerId.id)) + } + } else { + colorIndex = -1 + } } } else { colorIndex = -1 diff --git a/TelegramUI/BotCheckoutActionButton.swift b/TelegramUI/BotCheckoutActionButton.swift index c431a0d73d..ede3d2ccf1 100644 --- a/TelegramUI/BotCheckoutActionButton.swift +++ b/TelegramUI/BotCheckoutActionButton.swift @@ -111,11 +111,19 @@ final class BotCheckoutActionButton: HighlightTrackingButtonNode { if let validLayout = self.validLayout, let previousState = previousState { switch state { case .loading: - self.inactiveBackgroundNode.layer.animateFrame(from: self.activeBackgroundNode.frame, to: self.inactiveBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - - self.activeBackgroundNode.layer.animateFrame(from: self.activeBackgroundNode.frame, to: self.inactiveBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.inactiveBackgroundNode.layer.animateFrame(from: self.inactiveBackgroundNode.frame, to: self.progressBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + if !self.inactiveBackgroundNode.alpha.isZero { + self.inactiveBackgroundNode.alpha = 0.0 + self.inactiveBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + self.activeBackgroundNode.layer.animateFrame(from: self.activeBackgroundNode.frame, to: self.progressBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) self.activeBackgroundNode.alpha = 0.0 self.activeBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + self.labelNode.alpha = 0.0 + self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + + self.progressBackgroundNode.alpha = 1.0 + self.progressBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) diff --git a/TelegramUI/BotCheckoutControllerNode.swift b/TelegramUI/BotCheckoutControllerNode.swift index 389aacc8dc..98e326b231 100644 --- a/TelegramUI/BotCheckoutControllerNode.swift +++ b/TelegramUI/BotCheckoutControllerNode.swift @@ -163,7 +163,7 @@ enum BotCheckoutEntry: ItemListNodeEntry { }) case let .shippingInfo(theme, text, value): return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { - arguments.openInfo(.address) + arguments.openInfo(.address(.street1)) }) case let .shippingMethod(theme, text, value): return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { @@ -194,6 +194,35 @@ private struct BotCheckoutControllerState: Equatable { } } +private func currentTotalPrice(paymentForm: BotPaymentForm?, validatedFormInfo: BotPaymentValidatedFormInfo?, currentShippingOptionId: String?) -> Int64 { + guard let paymentForm = paymentForm else { + return 0 + } + + var totalPrice: Int64 = 0 + + var index = 0 + for price in paymentForm.invoice.prices { + totalPrice += price.amount + index += 1 + } + + if let validatedFormInfo = validatedFormInfo, let shippingOptions = validatedFormInfo.shippingOptions { + if let currentShippingOptionId = currentShippingOptionId { + for option in shippingOptions { + if option.id == currentShippingOptionId { + for price in option.prices { + totalPrice += price.amount + } + break + } + } + } + } + + return totalPrice +} + private func botCheckoutControllerEntries(presentationData: PresentationData, state: BotCheckoutControllerState, invoice: TelegramMediaInvoice, paymentForm: BotPaymentForm?, formInfo: BotPaymentRequestedInfo?, validatedFormInfo: BotPaymentValidatedFormInfo?, currentShippingOptionId: String?, currentPaymentMethod: BotCheckoutPaymentMethod?, botPeer: Peer?) -> [BotCheckoutEntry] { var entries: [BotCheckoutEntry] = [] @@ -351,13 +380,13 @@ final class BotCheckoutControllerNode: ItemListControllerNode, return (presentationData.theme, (nodeState, arguments)) } - self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.plainBackgroundColor) + self.actionButton = BotCheckoutActionButton(inactiveFillColor: self.presentationData.theme.list.plainBackgroundColor, activeFillColor: self.presentationData.theme.list.itemAccentColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor) self.actionButton.setState(.loading) self.inProgressDimNode = ASDisplayNode() self.inProgressDimNode.alpha = 0.0 self.inProgressDimNode.isUserInteractionEnabled = false - self.inProgressDimNode.backgroundColor = UIColor.white.withAlphaComponent(0.5) + self.inProgressDimNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor.withAlphaComponent(0.5) super.init(navigationBar: navigationBar, updateNavigationOffset: updateNavigationOffset, state: signal) @@ -376,6 +405,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, } } strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, formInfo, validatedInfo, updatedCurrentShippingOptionId, strongSelf.currentPaymentMethod))) + + strongSelf.updateActionButton() } }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } @@ -463,6 +494,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { strongSelf.currentShippingOptionId = id strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod))) + + strongSelf.updateActionButton() } }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } @@ -502,7 +535,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, } strongSelf.actionButton.isEnabled = true strongSelf.paymentFormAndInfo.set(.single((form, savedInfo, validatedInfo, nil, strongSelf.currentPaymentMethod))) - strongSelf.actionButton.setState(.active(strongSelf.presentationData.strings.CheckoutInfo_Pay)) + + strongSelf.updateActionButton() } }, error: { _ in @@ -521,12 +555,27 @@ final class BotCheckoutControllerNode: ItemListControllerNode, self.paymentAuthDisposable.dispose() } + private func updateActionButton() { + let totalAmount = currentTotalPrice(paymentForm: self.paymentFormValue, validatedFormInfo: self.currentValidatedFormInfo, currentShippingOptionId: self.currentShippingOptionId) + let payString: String + if let paymentForm = self.paymentFormValue, totalAmount > 0 { + payString = self.presentationData.strings.Checkout_PayPrice(formatCurrencyAmount(totalAmount, currency: paymentForm.invoice.currency)).0 + } else { + payString = self.presentationData.strings.CheckoutInfo_Pay + } + if self.actionButton.isEnabled { + self.actionButton.setState(.active(payString)) + } else { + self.actionButton.setState(.loading) + } + } + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { var updatedInsets = layout.intrinsicInsets updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0 super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), navigationBarHeight: navigationBarHeight, transition: transition) - let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter)) + let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter)) transition.updateFrame(node: self.actionButton, frame: actionButtonFrame) self.actionButton.updateLayout(size: actionButtonFrame.size, transition: transition) @@ -545,7 +594,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, if !paymentForm.invoice.requestedFields.isEmpty { guard let validatedFormInfo = self.currentValidatedFormInfo else { if paymentForm.invoice.requestedFields.contains(.shippingAddress) { - self.arguments?.openInfo(.address) + self.arguments?.openInfo(.address(.street1)) } else if paymentForm.invoice.requestedFields.contains(.name) { self.arguments?.openInfo(.name) } else if paymentForm.invoice.requestedFields.contains(.email) { @@ -580,12 +629,19 @@ final class BotCheckoutControllerNode: ItemListControllerNode, if let savedCredentialsToken = savedCredentialsToken { credentials = .saved(id: id, tempPassword: savedCredentialsToken.token) } else { - let _ = (cachedTwoStepPasswordToken(postbox: self.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] token in + let _ = (cachedTwoStepPasswordToken(postbox: self.account.postbox) + |> deliverOnMainQueue).start(next: { [weak self] token in if let strongSelf = self { let timestamp = strongSelf.account.network.getApproximateRemoteTimestamp() if let token = token, token.validUntilDate > timestamp - 1 * 60 { if token.requiresBiometrics { - let _ = (LocalAuth.auth(reason: strongSelf.presentationData.strings.Checkout_PayWithTouchId) |> deliverOnMainQueue).start(next: { value in + let reasonText: String + if let biometricAuthentication = LocalAuth.biometricAuthentication, case .faceId = biometricAuthentication { + reasonText = strongSelf.presentationData.strings.Checkout_PayWithFaceId + } else { + reasonText = strongSelf.presentationData.strings.Checkout_PayWithTouchId + } + let _ = (LocalAuth.auth(reason: reasonText) |> deliverOnMainQueue).start(next: { value in if let strongSelf = self { if value { strongSelf.pay(savedCredentialsToken: token) @@ -670,7 +726,8 @@ final class BotCheckoutControllerNode: ItemListControllerNode, } return nil } - let _ = (combineLatest(ApplicationSpecificNotice.getBotPaymentLiability(postbox: self.account.postbox, peerId: self.messageId.peerId), botPeer, self.account.postbox.loadedPeerWithId(paymentForm.providerId)) |> deliverOnMainQueue).start(next: { [weak self] value, botPeer, providerPeer in + let _ = (combineLatest(ApplicationSpecificNotice.getBotPaymentLiability(postbox: self.account.postbox, peerId: self.messageId.peerId), botPeer, self.account.postbox.loadedPeerWithId(paymentForm.providerId)) + |> deliverOnMainQueue).start(next: { [weak self] value, botPeer, providerPeer in if let strongSelf = self, let botPeer = botPeer { if value { strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) @@ -688,6 +745,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, self.inProgressDimNode.isUserInteractionEnabled = true self.inProgressDimNode.alpha = 1.0 self.actionButton.isEnabled = false + self.updateActionButton() self.payDisposable.set((sendBotPaymentForm(account: self.account, messageId: self.messageId, validatedInfoId: self.currentValidatedFormInfo?.id, shippingOptionId: self.currentShippingOptionId, credentials: credentials) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { strongSelf.inProgressDimNode.isUserInteractionEnabled = false @@ -706,6 +764,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, case .done: strongSelf.dismissAnimated() case let .externalVerificationRequired(url): + strongSelf.updateActionButton() var dismissImpl: (() -> Void)? let controller = BotCheckoutWebInteractionController(account: strongSelf.account, url: url, intent: .externalVerification({ _ in dismissImpl?() @@ -722,6 +781,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, strongSelf.inProgressDimNode.isUserInteractionEnabled = false strongSelf.inProgressDimNode.alpha = 0.0 strongSelf.actionButton.isEnabled = true + strongSelf.updateActionButton() if let applePayAuthrorizationCompletion = strongSelf.applePayAuthrorizationCompletion { strongSelf.applePayAuthrorizationCompletion = nil applePayAuthrorizationCompletion(.failure) diff --git a/TelegramUI/BotCheckoutInfoController.swift b/TelegramUI/BotCheckoutInfoController.swift index cf9971ec41..bbc079bdac 100644 --- a/TelegramUI/BotCheckoutInfoController.swift +++ b/TelegramUI/BotCheckoutInfoController.swift @@ -4,8 +4,16 @@ import Display import TelegramCore import Postbox +enum BotCheckoutInfoControllerAddressFocus { + case street1 + case street2 + case city + case state + case postcode +} + enum BotCheckoutInfoControllerFocus { - case address + case address(BotCheckoutInfoControllerAddressFocus) case name case phone case email @@ -65,7 +73,7 @@ final class BotCheckoutInfoController: ViewController { self?.presentingViewController?.dismiss(animated: false, completion: nil) }, openCountrySelection: { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: defaultLightAuthorizationTheme, displayCodes: false) + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: AuthorizationSequenceCountrySelectionTheme(presentationTheme: strongSelf.presentationData.theme), displayCodes: false) controller.completeWithCountryCode = { _, id in if let strongSelf = self { strongSelf.controllerNode.updateCountry(id) diff --git a/TelegramUI/BotCheckoutInfoControllerNode.swift b/TelegramUI/BotCheckoutInfoControllerNode.swift index 6917af0614..30df199673 100644 --- a/TelegramUI/BotCheckoutInfoControllerNode.swift +++ b/TelegramUI/BotCheckoutInfoControllerNode.swift @@ -181,7 +181,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi self.emailItem = nil } if invoice.requestedFields.contains(.phone) { - let phoneItem = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ReceiverInfoPhone, placeholder: strings.CheckoutInfo_ReceiverInfoPhone) + let phoneItem = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ReceiverInfoPhone, placeholder: strings.CheckoutInfo_ReceiverInfoPhone, contentType: .phoneNumber) phoneItem.text = formInfo.phone ?? "" self.phoneItem = phoneItem sectionItems.append(phoneItem) @@ -231,6 +231,49 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi item.textUpdated = { [weak self] in self?.updateDone() } + item.returnPressed = { [weak self, weak item] in + guard let strongSelf = self, let item = item else { + return + } + + var fieldsAndTypes: [(BotPaymentFieldItemNode, BotCheckoutInfoControllerFocus)] = [] + if let addressItems = strongSelf.addressItems { + fieldsAndTypes.append((addressItems.address1, .address(.street1))) + fieldsAndTypes.append((addressItems.address2, .address(.street2))) + fieldsAndTypes.append((addressItems.city, .address(.city))) + fieldsAndTypes.append((addressItems.state, .address(.state))) + fieldsAndTypes.append((addressItems.postcode, .address(.postcode))) + } + if let nameItem = strongSelf.nameItem { + fieldsAndTypes.append((nameItem, .name)) + } + if let phoneItem = strongSelf.phoneItem { + fieldsAndTypes.append((phoneItem, .phone)) + } + if let emailItem = strongSelf.emailItem { + fieldsAndTypes.append((emailItem, .email)) + } + + var activateNext = true + outer: for section in strongSelf.itemNodes { + for i in 0 ..< section.count { + if section[i] === item { + activateNext = true + } else if activateNext, let field = section[i] as? BotPaymentFieldItemNode { + + for (node, focus) in fieldsAndTypes { + if node === field { + strongSelf.focus = focus + if let containerLayout = strongSelf.containerLayout { + strongSelf.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) + } + break outer + } + } + } + } + } + } } } } @@ -385,7 +428,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi self.scrollNode.view.scrollIndicatorInsets = insets self.scrollNode.view.ignoreUpdateBounds = false - if let focus = focus { + if let focus = self.focus { var focusItem: ASDisplayNode? switch focus { case .address: @@ -417,7 +460,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi contentOffset.y = min(contentOffset.y, scrollContentSize.height + insets.bottom - layout.size.height) contentOffset.y = max(contentOffset.y, -insets.top) - transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size)) + //transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size)) } else { let contentOffset = CGPoint(x: 0.0, y: -insets.top) transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size)) diff --git a/TelegramUI/BotCheckoutNativeCardEntryController.swift b/TelegramUI/BotCheckoutNativeCardEntryController.swift index 15a80f4c26..e79b91b747 100644 --- a/TelegramUI/BotCheckoutNativeCardEntryController.swift +++ b/TelegramUI/BotCheckoutNativeCardEntryController.swift @@ -70,7 +70,7 @@ final class BotCheckoutNativeCardEntryController: ViewController { self?.presentingViewController?.dismiss(animated: false, completion: nil) }, openCountrySelection: { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: defaultLightAuthorizationTheme, displayCodes: false) + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: AuthorizationSequenceCountrySelectionTheme(presentationTheme: strongSelf.presentationData.theme), displayCodes: false) controller.completeWithCountryCode = { _, id in if let strongSelf = self { strongSelf.controllerNode.updateCountry(id) @@ -120,6 +120,8 @@ final class BotCheckoutNativeCardEntryController: ViewController { if case .modalSheet = presentationArguments.presentationAnimation { self.controllerNode.animateIn() } + + self.controllerNode.activate() } } @@ -137,11 +139,12 @@ final class BotCheckoutNativeCardEntryController: ViewController { self.dismiss() } - @objc func donePressed() { + @objc private func donePressed() { self.controllerNode.verify() } - override open func dismiss(completion: (() -> Void)? = nil) { + override public func dismiss(completion: (() -> Void)? = nil) { + self.view.endEditing(true) self.controllerNode.animateOut(completion: completion) } } diff --git a/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift b/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift index 91cee1a003..f008a36ab6 100644 --- a/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift +++ b/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift @@ -98,7 +98,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_CardholderNameTitle)) - let cardholderItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_CardholderNamePlaceholder) + let cardholderItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_CardholderNamePlaceholder, contentType: .creditCardholderName) self.cardholderItem = cardholderItem sectionItems.append(cardholderItem) @@ -136,7 +136,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, self.zipCodeItem = nil } - self.saveInfoItem = BotPaymentSwitchItemNode(title: strings.Checkout_NewCard_SaveInfo, isOn: true) + self.saveInfoItem = BotPaymentSwitchItemNode(title: strings.Checkout_NewCard_SaveInfo, isOn: false) itemNodes.append([self.saveInfoItem, BotPaymentTextItemNode(text: strings.Checkout_NewCard_SaveInfoHelp)]) self.itemNodes = itemNodes @@ -179,6 +179,22 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, item.textUpdated = { [weak self] in self?.updateDone() } + item.returnPressed = { [weak self, weak item] in + guard let strongSelf = self, let item = item else { + return + } + var activateNext = true + outer: for section in strongSelf.itemNodes { + for i in 0 ..< section.count { + if section[i] === item { + activateNext = true + } else if activateNext, let field = section[i] as? BotPaymentFieldItemNode { + field.activateInput() + break outer + } + } + } + } } } } @@ -320,10 +336,18 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, let previousBoundsOrigin = self.scrollNode.bounds.origin self.scrollNode.view.ignoreUpdateBounds = true - transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - self.scrollNode.view.contentSize = scrollContentSize - self.scrollNode.view.contentInset = insets - self.scrollNode.view.scrollIndicatorInsets = insets + if self.scrollNode.frame != CGRect(origin: CGPoint(), size: layout.size) { + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + } + if self.scrollNode.view.contentSize != scrollContentSize { + self.scrollNode.view.contentSize = scrollContentSize + } + if self.scrollNode.view.contentInset != insets { + self.scrollNode.view.contentInset = insets + } + if self.scrollNode.view.scrollIndicatorInsets != insets { + self.scrollNode.view.scrollIndicatorInsets = insets + } self.scrollNode.view.ignoreUpdateBounds = false if let previousLayout = previousLayout { @@ -333,9 +357,9 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, var contentOffset = CGPoint(x: 0.0, y: previousBoundsOrigin.y + insetsScrollOffset) contentOffset.y = min(contentOffset.y, scrollContentSize.height + insets.bottom - layout.size.height) - contentOffset.y = max(contentOffset.y, -insets.top) + //contentOffset.y = max(contentOffset.y, -insets.top) - transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size)) + //transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size)) } else { let contentOffset = CGPoint(x: 0.0, y: -insets.top) transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: layout.size)) @@ -354,4 +378,8 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, completion?() }) } + + func activate() { + self.cardItem.activateInput() + } } diff --git a/TelegramUI/BotCheckoutPasswordEntryController.swift b/TelegramUI/BotCheckoutPasswordEntryController.swift index 4ef6e9dbfd..2b683e6087 100644 --- a/TelegramUI/BotCheckoutPasswordEntryController.swift +++ b/TelegramUI/BotCheckoutPasswordEntryController.swift @@ -149,6 +149,7 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { self.textFieldNode.textField.textColor = theme.actionSheet.primaryTextColor self.textFieldNode.textField.font = Font.regular(12.0) self.textFieldNode.textField.typingAttributes = [NSAttributedStringKey.font.rawValue: Font.regular(12.0)] + self.textFieldNode.textField.keyboardAppearance = theme.chatList.searchBarKeyboardColor.keyboardAppearance self.textFieldNode.textField.isSecureTextEntry = true super.init() diff --git a/TelegramUI/BotPaymentFieldItemNode.swift b/TelegramUI/BotPaymentFieldItemNode.swift index 9359e6be38..1afe6716a3 100644 --- a/TelegramUI/BotPaymentFieldItemNode.swift +++ b/TelegramUI/BotPaymentFieldItemNode.swift @@ -4,7 +4,13 @@ import Display private let titleFont = Font.regular(17.0) -final class BotPaymentFieldItemNode: BotPaymentItemNode { +enum BotPaymentFieldContentType { + case generic + case creditCardholderName + case phoneNumber +} + +final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate { private let title: String var text: String { get { @@ -13,6 +19,7 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode { self.textField.textField.text = value } } + private let contentType: BotPaymentFieldContentType private let placeholder: String private let titleNode: ASTextNode @@ -21,10 +28,12 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode { private var theme: PresentationTheme? var textUpdated: (() -> Void)? + var returnPressed: (() -> Void)? - init(title: String, placeholder: String, text: String = "") { + init(title: String, placeholder: String, text: String = "", contentType: BotPaymentFieldContentType = .generic) { self.title = title self.placeholder = placeholder + self.contentType = contentType self.titleNode = ASTextNode() self.titleNode.maximumNumberOfLines = 1 @@ -32,8 +41,18 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode { self.textField = TextFieldNode() self.textField.textField.font = titleFont self.textField.textField.returnKeyType = .next - self.textField.textField.text = text + switch contentType { + case .generic: + break + case .creditCardholderName: + self.textField.textField.autocorrectionType = .no + case .phoneNumber: + self.textField.textField.keyboardType = .numberPad + if #available(iOSApplicationExtension 10.0, *) { + self.textField.textField.textContentType = .telephoneNumber + } + } super.init(needsBackground: true) @@ -41,6 +60,7 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode { self.addSubnode(self.textField) self.textField.textField.addTarget(self, action: #selector(self.editingChanged), for: [.editingChanged]) + self.textField.textField.delegate = self } override func measureInset(theme: PresentationTheme, width: CGFloat) -> CGFloat { @@ -94,4 +114,22 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode { @objc func editingChanged() { self.textUpdated?() } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.returnPressed?() + return false + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if !string.isEmpty { + if case .creditCardholderName = self.contentType { + if let lowerBound = textField.position(from: textField.beginningOfDocument, offset: range.lowerBound), let upperBound = textField.position(from: textField.beginningOfDocument, offset: range.upperBound), let fieldRange = textField.textRange(from: lowerBound, to: upperBound) { + textField.replace(fieldRange, withText: string.uppercased()) + self.editingChanged() + return false + } + } + } + return true + } } diff --git a/TelegramUI/CallListController.swift b/TelegramUI/CallListController.swift index cd6f42ee78..a10dc1e398 100644 --- a/TelegramUI/CallListController.swift +++ b/TelegramUI/CallListController.swift @@ -185,10 +185,10 @@ public final class CallListController: ViewController { let controller = ContactSelectionController(account: self.account, title: { $0.Calls_NewCall }) self.createActionDisposable.set((controller.result |> take(1) - |> deliverOnMainQueue).start(next: { [weak controller, weak self] peerId in + |> deliverOnMainQueue).start(next: { [weak controller, weak self] peer in controller?.dismissSearch() - if let strongSelf = self, let peerId = peerId { - strongSelf.call(peerId, began: { + if let strongSelf = self, let contactPeer = peer, case let .peer(peer, _) = contactPeer { + strongSelf.call(peer.id, began: { if let strongSelf = self { if let hasOngoingCall = strongSelf.account.telegramApplicationContext.hasOngoingCall { let _ = (hasOngoingCall diff --git a/TelegramUI/ChangePhoneNumberController.swift b/TelegramUI/ChangePhoneNumberController.swift index 7afe172223..5df48e9f47 100644 --- a/TelegramUI/ChangePhoneNumberController.swift +++ b/TelegramUI/ChangePhoneNumberController.swift @@ -66,7 +66,7 @@ final class ChangePhoneNumberController: ViewController { self.displayNodeDidLoad() self.controllerNode.selectCountryCode = { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: defaultLightAuthorizationTheme) + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: AuthorizationSequenceCountrySelectionTheme(presentationTheme: strongSelf.presentationData.theme)) controller.completeWithCountryCode = { code, _ in if let strongSelf = self { strongSelf.updateData(countryCode: Int32(code), number: strongSelf.controllerNode.codeAndNumber.1) diff --git a/TelegramUI/ChannelAdminController.swift b/TelegramUI/ChannelAdminController.swift index 4f74f54839..7457d44b13 100644 --- a/TelegramUI/ChannelAdminController.swift +++ b/TelegramUI/ChannelAdminController.swift @@ -407,7 +407,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s entries.append(.addAdminsInfo(presentationData.theme, currentRightsFlags.contains(.canAddAdmins) ? presentationData.strings.Channel_EditAdmin_PermissinAddAdminOn : presentationData.strings.Channel_EditAdmin_PermissinAddAdminOff)) } - if let initialParticipant = initialParticipant { + if let initialParticipant = initialParticipant, case let .member(participant) = initialParticipant, let adminInfo = participant.adminInfo, !adminInfo.rights.flags.isEmpty { var canDismiss = false if channel.flags.contains(.isCreator) { canDismiss = true @@ -475,13 +475,13 @@ public func channelAdminController(account: Account, peerId: PeerId, adminId: Pe })) }) - let combinedView = account.postbox.combinedView(keys: [.peer(peerId: peerId), .peer(peerId: adminId)]) + let combinedView = account.postbox.combinedView(keys: [.peer(peerId: peerId, components: .all), .peer(peerId: adminId, components: .all)]) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), combinedView) |> deliverOnMainQueue |> map { presentationData, state, combinedView -> (ItemListControllerState, (ItemListNodeState, ChannelAdminEntry.ItemGenerationArguments)) in - let channelView = combinedView.views[.peer(peerId: peerId)] as! PeerView - let adminView = combinedView.views[.peer(peerId: adminId)] as! PeerView + let channelView = combinedView.views[.peer(peerId: peerId, components: .all)] as! PeerView + let adminView = combinedView.views[.peer(peerId: adminId, components: .all)] as! PeerView let canEdit = canEditAdminRights(accountPeerId: account.peerId, channelView: channelView, initialParticipant: initialParticipant) let leftNavigationButton: ItemListNavigationButton diff --git a/TelegramUI/ChannelBannedMemberController.swift b/TelegramUI/ChannelBannedMemberController.swift index a8c36c4cd2..73ceb477d6 100644 --- a/TelegramUI/ChannelBannedMemberController.swift +++ b/TelegramUI/ChannelBannedMemberController.swift @@ -421,15 +421,15 @@ public func channelBannedMemberController(account: Account, peerId: PeerId, memb presentControllerImpl?(actionSheet, nil) }) - let combinedView = account.postbox.combinedView(keys: [.peer(peerId: peerId), .peer(peerId: memberId)]) + let combinedView = account.postbox.combinedView(keys: [.peer(peerId: peerId, components: .all), .peer(peerId: memberId, components: .all)]) let canEdit = true let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), combinedView) |> deliverOnMainQueue |> map { presentationData, state, combinedView -> (ItemListControllerState, (ItemListNodeState, ChannelBannedMemberEntry.ItemGenerationArguments)) in - let channelView = combinedView.views[.peer(peerId: peerId)] as! PeerView - let memberView = combinedView.views[.peer(peerId: memberId)] as! PeerView + let channelView = combinedView.views[.peer(peerId: peerId, components: .all)] as! PeerView + let memberView = combinedView.views[.peer(peerId: memberId, components: .all)] as! PeerView let leftNavigationButton: ItemListNavigationButton leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { diff --git a/TelegramUI/ChannelMembersController.swift b/TelegramUI/ChannelMembersController.swift index 6005edc818..554526d290 100644 --- a/TelegramUI/ChannelMembersController.swift +++ b/TelegramUI/ChannelMembersController.swift @@ -279,9 +279,9 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo let arguments = ChannelMembersControllerArguments(account: account, addMember: { var confirmationImpl: ((PeerId) -> Signal)? - let contactsController = ContactSelectionController(account: account, title: { $0.GroupInfo_AddParticipantTitle }, confirmation: { peerId in - if let confirmationImpl = confirmationImpl { - return confirmationImpl(peerId) + let contactsController = ContactSelectionController(account: account, title: { $0.GroupInfo_AddParticipantTitle }, confirmation: { peer in + if let confirmationImpl = confirmationImpl, case let .peer(peer, _) = peer { + return confirmationImpl(peer.id) } else { return .single(false) } @@ -311,8 +311,9 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo let addMember = contactsController.result |> mapError { _ -> AddPeerMemberError in return .generic } |> deliverOnMainQueue - |> mapToSignal { memberId -> Signal in - if let memberId = memberId { + |> mapToSignal { memberPeer -> Signal in + if let memberPeer = memberPeer, case let .peer(selectedPeer, _) = memberPeer { + let memberId = selectedPeer.id let applyMembers: Signal = peersPromise.get() |> filter { $0 != nil } |> take(1) @@ -343,7 +344,7 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo |> mapError { _ -> AddPeerMemberError in return .generic } return addPeerMember(account: account, peerId: peerId, memberId: memberId) - |> then(applyMembers) + |> then(applyMembers) } else { return .complete() } diff --git a/TelegramUI/ChannelMembersSearchContainerNode.swift b/TelegramUI/ChannelMembersSearchContainerNode.swift index 421724cdd3..2dfc5b3958 100644 --- a/TelegramUI/ChannelMembersSearchContainerNode.swift +++ b/TelegramUI/ChannelMembersSearchContainerNode.swift @@ -88,7 +88,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer, RenderedChannelParticipant?) -> Void) -> ListViewItem { switch self.content { case let .peer(peer): - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in peerSelected(peer, nil) }) case let .participant(participant, label, enabled): @@ -98,7 +98,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { } else { status = .none } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: participant.peer, chatPeer: participant.peer, status: status, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: participant.peer), status: status, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: theme, strings: strings, actionTitle: nil, action: nil) }), action: { _ in peerSelected(participant.peer, participant) }) } diff --git a/TelegramUI/ChannelMembersSearchController.swift b/TelegramUI/ChannelMembersSearchController.swift index b277fb0167..4fd788a395 100644 --- a/TelegramUI/ChannelMembersSearchController.swift +++ b/TelegramUI/ChannelMembersSearchController.swift @@ -80,6 +80,7 @@ final class ChannelMembersSearchController: ViewController { } override func dismiss(completion: (() -> Void)? = nil) { + self.view.endEditing(true) self.controllerNode.animateOut(completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) completion?() diff --git a/TelegramUI/ChannelMembersSearchControllerNode.swift b/TelegramUI/ChannelMembersSearchControllerNode.swift index 60fe01729b..d066426f22 100644 --- a/TelegramUI/ChannelMembersSearchControllerNode.swift +++ b/TelegramUI/ChannelMembersSearchControllerNode.swift @@ -80,7 +80,7 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable { } else { status = .none } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: participant.peer, chatPeer: nil, status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: nil, action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: nil, action: { _ in interaction.openPeer(participant.peer, participant) }) } diff --git a/TelegramUI/ChannelVisibilityController.swift b/TelegramUI/ChannelVisibilityController.swift index 39c710bf2b..590e1fe5df 100644 --- a/TelegramUI/ChannelVisibilityController.swift +++ b/TelegramUI/ChannelVisibilityController.swift @@ -728,25 +728,43 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: } |> deliverOnMainQueue).start(next: { link in if let link = link { UIPasteboard.general.string = link + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Username_LinkCopied, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } }) }, revokePrivateLink: { - var revoke = false - updateState { state in - if !state.revokingPrivateLink { - revoke = true - return state.withUpdatedRevokingPrivateLink(true) - } else { - return state - } - } - if revoke { - revokeLinkDisposable.set((ensuredExistingPeerExportedInvitation(account: account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: { - updateState { - $0.withUpdatedRevokingPrivateLink(false) - } - })) + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(presentationTheme: presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text), + ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: { + dismissAction() + + var revoke = false + updateState { state in + if !state.revokingPrivateLink { + revoke = true + return state.withUpdatedRevokingPrivateLink(true) + } else { + return state + } + } + if revoke { + revokeLinkDisposable.set((ensuredExistingPeerExportedInvitation(account: account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: { + updateState { + $0.withUpdatedRevokingPrivateLink(false) + } + })) + } + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, sharePrivateLink: { let _ = (account.postbox.transaction { transaction -> String? in if let cachedData = transaction.getPeerCachedData(peerId: peerId) { @@ -766,7 +784,9 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: }) let peerView = account.viewTracker.peerView(peerId) - |> deliverOnMainQueue + |> deliverOnMainQueue + + let previousHadNamesToRevoke = Atomic(value: nil) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, peerView, peersDisablingAddressNameAssignment.get() |> deliverOnMainQueue) |> deliverOnMainQueue @@ -833,6 +853,10 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: }) } + if state.revokingPeerId != nil { + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) + } + var isGroup = false if let peer = peer as? TelegramChannel { if case .group = peer.info { @@ -850,8 +874,32 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: }) } + var crossfade: Bool = false + let hasNamesToRevoke = publicChannelsToRevoke != nil && !publicChannelsToRevoke!.isEmpty + let hadNamesToRevoke = previousHadNamesToRevoke.swap(hasNamesToRevoke) + if let peer = view.peers[view.peerId] as? TelegramChannel { + let selectedType: CurrentChannelType + if case .privateLink = mode { + selectedType = .privateChannel + } else { + if let current = state.selectedType { + selectedType = current + } else { + if let addressName = peer.addressName, !addressName.isEmpty { + selectedType = .publicChannel + } else { + selectedType = .privateChannel + } + } + } + + if selectedType == .publicChannel, let hadNamesToRevoke = hadNamesToRevoke { + crossfade = hadNamesToRevoke != hasNamesToRevoke + } + } + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(isGroup ? presentationData.strings.GroupInfo_GroupType : presentationData.strings.Channel_TypeSetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: channelVisibilityControllerEntries(presentationData: presentationData, mode: mode, view: view, publicChannelsToRevoke: publicChannelsToRevoke, state: state), style: .blocks, animateChanges: false) + let listState = ItemListNodeState(entries: channelVisibilityControllerEntries(presentationData: presentationData, mode: mode, view: view, publicChannelsToRevoke: publicChannelsToRevoke, state: state), style: .blocks, crossfadeState: crossfade, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/ChatContextResultPeekContentNode.swift b/TelegramUI/ChatContextResultPeekContentNode.swift index be500bfe74..9fc3094fe2 100644 --- a/TelegramUI/ChatContextResultPeekContentNode.swift +++ b/TelegramUI/ChatContextResultPeekContentNode.swift @@ -140,7 +140,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont var videoFileReference: FileMediaReference? var imageDimensions: CGSize? switch self.contextResult { - case let .externalReference(_, type, title, _, url, content, thumbnail, _): + case let .externalReference(_, _, type, title, _, url, content, thumbnail, _): if let content = content { imageResource = content.resource } else if let thumbnail = thumbnail { @@ -149,10 +149,10 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont imageDimensions = content?.dimensions if let content = content, type == "gif", let thumbnailResource = imageResource , let dimensions = content.dimensions { - videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), reference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])) + videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])) imageResource = nil } - case let .internalReference(_, _, title, _, image, file, _): + case let .internalReference(_, _, _, title, _, image, file, _): if let image = image { if let largestRepresentation = largestImageRepresentation(image.representations) { imageDimensions = largestRepresentation.dimensions @@ -212,7 +212,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont if updatedImageResource { if let imageResource = imageResource { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0), resource: imageResource) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil, partialReference: nil) updateImageSignal = chatMessagePhoto(postbox: self.account.postbox, photoReference: .standalone(media: tmpImage)) } else { updateImageSignal = .complete() diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 08506d11e8..e681f9caca 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -375,7 +375,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: text, attributes: attributes, media: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, sendSticker: { [weak self] fileReference in if let strongSelf = self { @@ -393,9 +393,9 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } }) - strongSelf.sendMessages([.message(text: "", attributes: [], media: fileReference.media, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } - }, sendGif: { [weak self] file in + }, sendGif: { [weak self] fileReference in if let strongSelf = self { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { @@ -409,7 +409,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } }) - strongSelf.sendMessages([.message(text: "", attributes: [], media: file.media, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, requestMessageActionCallback: { [weak self] messageId, data, isGame in if let strongSelf = self { @@ -499,7 +499,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin |> deliverOnMainQueue).start(next: { coordinate in if let strongSelf = self { if let coordinate = coordinate { - strongSelf.sendMessages([.message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil), replyToMessageId: nil, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil)), replyToMessageId: nil, localGroupingKey: nil)]) } else { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})]), in: .window(.root)) } @@ -515,7 +515,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin let _ = (strongSelf.account.postbox.loadedPeerWithId(strongSelf.account.peerId) |> deliverOnMainQueue).start(next: { peer in if let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { - strongSelf.sendMessages([.message(text: "", attributes: [], media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil), replyToMessageId: nil, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), replyToMessageId: nil, localGroupingKey: nil)]) } }) } @@ -548,7 +548,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: command, attributes: attributes, media: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: command, attributes: attributes, mediaReference: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil, localGroupingKey: nil)]) } }, openInstantPage: { [weak self] message in if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.navigationController as? NavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) { @@ -735,7 +735,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.sendMessages([.message(text: command, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: command, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]) } })) } @@ -791,8 +791,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { for media in message.media { if let invoice = media as? TelegramMediaInvoice { + strongSelf.chatDisplayNode.dismissInput() if let receiptMessageId = invoice.receiptMessageId { - strongSelf.chatDisplayNode.dismissInput() strongSelf.present(BotReceiptController(account: strongSelf.account, invoice: invoice, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else { strongSelf.present(BotCheckoutController(account: strongSelf.account, invoice: invoice, messageId: messageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) @@ -1798,8 +1798,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } let media: RequestEditMessageMedia - if let editMedia = strongSelf.presentationInterfaceState.editMessageState?.media { - media = .update(editMedia) + if let editMediaReference = strongSelf.presentationInterfaceState.editMessageState?.mediaReference { + media = .update(editMediaReference) } else { media = .keep } @@ -1982,7 +1982,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - strongSelf.sendMessages([.message(text: messageText, attributes: attributes, media: nil, replyToMessageId: replyMessageId, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: messageText, attributes: attributes, mediaReference: nil, replyToMessageId: replyMessageId, localGroupingKey: nil)]) } } }, sendBotStart: { [weak self] payload in @@ -2019,13 +2019,25 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } } if let strongSelf = self { - authorizeDeviceAccess(to: .microphone(isVideo ? .video : .audio), presentationData: strongSelf.presentationData, present: { c, a in + DeviceAccess.authorizeAccess(to: .microphone(isVideo ? .video : .audio), presentationData: strongSelf.presentationData, present: { c, a in self?.present(c, in: .window(.root), with: a) }, openSettings: { self?.account.telegramApplicationContext.applicationBindings.openSettings() }, { granted in if granted { - begin() + if isVideo, let strongSelf = self { + DeviceAccess.authorizeAccess(to: .camera, presentationData: strongSelf.presentationData, present: { c, a in + self?.present(c, in: .window(.root), with: a) + }, openSettings: { + self?.account.telegramApplicationContext.applicationBindings.openSettings() + }, { granted in + if granted { + begin() + } + }) + } else { + begin() + } } }) } @@ -2166,7 +2178,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } }) - strongSelf.sendMessages([.message(text: "", attributes: [], media: file.media, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: file.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } }, unblockPeer: { [weak self] in self?.unblockPeer() @@ -2338,8 +2350,9 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin switch self.chatLocation { case let .peer(peerId): let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.peer(peerId), .total(.filtered, .messages)]) - let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerId: peerId) - self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey, notificationSettingsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in + let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerIds: Set([peerId])) + self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey, notificationSettingsKey]) + |> deliverOnMainQueue).start(next: { [weak self] views in if let strongSelf = self { var unreadCount: Int32 = 0 var totalChatCount: Int32 = 0 @@ -2355,7 +2368,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount - if let view = views.views[notificationSettingsKey] as? PeerNotificationSettingsView, let notificationSettings = view.notificationSettings { + if let view = views.views[notificationSettingsKey] as? PeerNotificationSettingsView, let notificationSettings = view.notificationSettings[peerId] { var globalRemainingUnreadChatCount = totalChatCount if !notificationSettings.isRemovedFromTotalUnreadCount && unreadCount > 0 { globalRemainingUnreadChatCount -= 1 @@ -2426,7 +2439,6 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin })) case let .group(groupId): let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.group(groupId), .total(.filtered, .messages)]) - //let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerId: peerId) self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in if let strongSelf = self { var unreadCount: Int32 = 0 @@ -2503,6 +2515,10 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if case .standard(false) = self.presentationInterfaceState.mode, self.raiseToListen == nil { self.raiseToListen = RaiseToListenManager(shouldActivate: { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded && strongSelf.canReadHistoryValue, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil, strongSelf.playlistStateAndType == nil { + if strongSelf.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { + return false + } + if strongSelf.firstLoadedMessageToListen() != nil || strongSelf.chatDisplayNode.isTextInputPanelActive { if strongSelf.account.telegramApplicationContext.immediateHasOngoingCall { return false @@ -2961,11 +2977,11 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } private func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) { - if let message = messages.first, case let .message(desc) = message, let media = desc.media { + if let message = messages.first, case let .message(desc) = message, let mediaReference = desc.mediaReference { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in var state = state if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty { - state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, media: media)) + state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, mediaReference: mediaReference)) } if !desc.text.isEmpty { state = state.updatedInterfaceState { state in @@ -3124,8 +3140,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin for item in results { if let item = item { let fileId = arc4random64() - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), reference: nil, resource: ICloudFileResource(urlData: item.urlData), previewRepresentations: [], mimeType: guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension), size: item.fileSize, attributes: [.FileName(fileName: item.fileName)]) - let message: EnqueueMessage = .message(text: "", attributes: [], media: file, replyToMessageId: replyMessageId, localGroupingKey: nil) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData), previewRepresentations: [], mimeType: guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension), size: item.fileSize, attributes: [.FileName(fileName: item.fileName)]) + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, localGroupingKey: nil) messages.append(message) } } @@ -3222,7 +3238,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin return } let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let message: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: venue, liveBroadcastingTimeout: nil), replyToMessageId: replyMessageId, localGroupingKey: nil) + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: venue, liveBroadcastingTimeout: nil)), replyToMessageId: replyMessageId, localGroupingKey: nil) if editingMessage { strongSelf.editMessageMediaWithMessages([message]) @@ -3241,7 +3257,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin return } let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let message: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period), replyToMessageId: replyMessageId, localGroupingKey: nil) + let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period)), replyToMessageId: replyMessageId, localGroupingKey: nil) if editingMessage { strongSelf.editMessageMediaWithMessages([message]) } else { @@ -3259,26 +3275,87 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } private func presentContactPicker() { - let contactsController = ContactSelectionController(account: self.account, title: { $0.Contacts_Title }) + let contactsController = ContactSelectionController(account: self.account, title: { $0.Contacts_Title }, displayDeviceContacts: true) self.chatDisplayNode.dismissInput() self.present(contactsController, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - self.controllerNavigationDisposable.set((contactsController.result |> deliverOnMainQueue).start(next: { [weak self] peerId in - if let strongSelf = self, let peerId = peerId { - let peer = strongSelf.account.postbox.loadedPeerWithId(peerId) - |> take(1) - strongSelf.controllerNavigationDisposable.set((peer |> deliverOnMainQueue).start(next: { peer in - if let strongSelf = self, let user = peer as? TelegramUser, let phone = user.phone, !phone.isEmpty { - let media = TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil) - let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } - }) + self.controllerNavigationDisposable.set((contactsController.result |> deliverOnMainQueue).start(next: { [weak self] peer in + if let strongSelf = self, let peer = peer { + let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> + switch peer { + case let .peer(contact, _): + guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + return + } + let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []) + let account = strongSelf.account + dataSignal = strongSelf.account.telegramApplicationContext.contactDataManager.basicData() + |> take(1) + |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in + var stableId: String? + let queryPhoneNumber = formatPhoneNumber(phoneNumber) + outer: for (id, data) in basicData { + for phoneNumber in data.phoneNumbers { + if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber { + stableId = id + break outer + } + } } - }) - let message = EnqueueMessage.message(text: "", attributes: [], media: media, replyToMessageId: replyMessageId, localGroupingKey: nil) - strongSelf.sendMessages([message]) + + if let stableId = stableId { + return account.telegramApplicationContext.contactDataManager.extendedData(stableId: stableId) + |> take(1) + |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in + return (contact, extendedData) + } + } else { + return .single((contact, contactData)) + } + } + case let .deviceContact(id, _): + dataSignal = strongSelf.account.telegramApplicationContext.contactDataManager.extendedData(stableId: id) + |> take(1) + |> map { extendedData -> (Peer?, DeviceContactExtendedData?) in + return (nil, extendedData) + } + } + strongSelf.controllerNavigationDisposable.set((dataSignal + |> deliverOnMainQueue).start(next: { peerAndContactData in + if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 { + if contactData.basicData.phoneNumbers.count == 1, false { + let phone = contactData.basicData.phoneNumbers[0].value + let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil) + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil) + strongSelf.sendMessages([message]) + } else { + strongSelf.present(deviceContactInfoController(account: strongSelf.account, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in + guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { + return + } + let phone = contactData.basicData.phoneNumbers[0].value + if let vCardData = contactData.serializedVCard() { + let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil) + strongSelf.sendMessages([message]) + } + })), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } } })) } @@ -3467,7 +3544,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } }) - strongSelf.sendMessages([.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), reference: nil, resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) strongSelf.audioRecorderFeedback?.tap() strongSelf.audioRecorderFeedback = nil @@ -3541,7 +3618,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin var randomId: Int64 = 0 arc4random_buf(&randomId, 8) - self.sendMessages([.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), reference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], mimeType: "audio/ogg", size: Int(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) + self.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], mimeType: "audio/ogg", size: Int(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]) } } @@ -3597,7 +3674,9 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin searchDisposable = MetaDisposable() self.searchDisposable = searchDisposable } - searchDisposable.set((searchMessages(account: self.account, location: location, query: query) |> deliverOnMainQueue).start(next: { [weak self] results in + searchDisposable.set((searchMessages(account: self.account, location: location, query: query) + |> delay(0.2, queue: Queue.mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] results in if let strongSelf = self { var navigateIndex: MessageIndex? strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in @@ -3993,7 +4072,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin Queue.mainQueue().async { unblockingPeer.set(false) if let strongSelf = self, restartBot { - let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: [.message(text: "/start", attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() + let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: [.message(text: "/start", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() } } })).start()) @@ -4107,6 +4186,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } }, present: { c, a in self?.present(c, in: .window(.root), with: a) + }, dismissInput: { + self?.chatDisplayNode.dismissInput() }) } })) @@ -4254,7 +4335,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if let _ = data.peer as? TelegramUser { items.append(UIPreviewAction(title: "👍", style: .default, handler: { _, _ in if let strongSelf = self { - let _ = enqueueMessages(account: strongSelf.account, peerId: peer.id, messages: strongSelf.transformEnqueueMessages([.message(text: "👍", attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)])).start() + let _ = enqueueMessages(account: strongSelf.account, peerId: peer.id, messages: strongSelf.transformEnqueueMessages([.message(text: "👍", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)])).start() } })) } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 2d5e5943e0..1eabee7121 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -275,7 +275,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { webpage = strongSelf.chatPresentationInterfaceState.urlPreview?.1 } - messages.append(.message(text: text.string, attributes: attributes, media: webpage, replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)) + messages.append(.message(text: text.string, attributes: attributes, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)) } } @@ -1619,6 +1619,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let menuHeight = self.messageActionSheetController?.0.controllerNode.updateLayout(layout: layout, horizontalOrigin: globalSelfOrigin.x, transition: .immediate) if let stableId = self.messageActionSheetController?.1 { var resultItemNode: ListViewItemNode? + var resultItemSubnode: ASDisplayNode? self.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { switch item.content { @@ -1630,6 +1631,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { for (message, _, _, _) in messages { if message.stableId == stableId { resultItemNode = itemNode + if let media = message.media.first { + resultItemSubnode = itemNode.transitionNode(id: message.id, media: media)?.0 + } break } } @@ -1637,9 +1641,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } if let resultItemNode = resultItemNode, let menuHeight = menuHeight { - if resultItemNode.frame.size.height < self.historyNode.bounds.size.height - self.historyNode.insets.top - self.historyNode.insets.bottom { - if resultItemNode.frame.minY < menuHeight { - messageActionSheetControllerAdditionalInset = menuHeight - resultItemNode.frame.minY + var resultItemFrame = resultItemNode.frame + if let resultItemSubnode = resultItemSubnode { + resultItemFrame = resultItemSubnode.view.convert(resultItemSubnode.bounds, to: resultItemNode.view.superview) + } + if resultItemFrame.size.height < self.historyNode.bounds.size.height - self.historyNode.insets.top - self.historyNode.insets.bottom { + if resultItemFrame.minY < menuHeight { + messageActionSheetControllerAdditionalInset = menuHeight - resultItemFrame.minY } } } diff --git a/TelegramUI/ChatDocumentGalleryItem.swift b/TelegramUI/ChatDocumentGalleryItem.swift index cc5daeaad5..42ef4c76d8 100644 --- a/TelegramUI/ChatDocumentGalleryItem.swift +++ b/TelegramUI/ChatDocumentGalleryItem.swift @@ -106,7 +106,7 @@ class ChatDocumentGalleryItemNode: GalleryItemNode, WKNavigationDelegate { private var status: MediaResourceStatus? init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { - /*if #available(iOS 9.0, *) { + if #available(iOSApplicationExtension 11.0, *) { let preferences = WKPreferences() preferences.javaScriptEnabled = false let configuration = WKWebViewConfiguration() @@ -115,13 +115,13 @@ class ChatDocumentGalleryItemNode: GalleryItemNode, WKNavigationDelegate { webView.allowsLinkPreview = false webView.allowsBackForwardNavigationGestures = false self.webView = webView - } else {*/ + } else { let _ = registeredURLProtocol let webView = UIWebView() webView.scalesPageToFit = true self.webView = webView - //} + } self.footerContentNode = ChatItemGalleryFooterContentNode(account: account, theme: theme, strings: strings) self.statusNodeContainer = HighlightableButtonNode() @@ -237,8 +237,40 @@ class ChatDocumentGalleryItemNode: GalleryItemNode, WKNavigationDelegate { if let strongSelf = self { if data.complete { if let webView = strongSelf.webView as? WKWebView { - if #available(iOS 9.0, *) { - webView.loadFileURL(URL(fileURLWithPath: data.path), allowingReadAccessTo: URL(fileURLWithPath: data.path)) + if #available(iOSApplicationExtension 11.0, *) { + let blockRules = """ + [{ + "trigger": { + "url-filter": ".*" + }, + "action": { + "type": "block" + } + }, + { + "trigger": { + "url-filter": "file://\(data.path)" + }, + "action": { + "type": "ignore-previous-rules" + } + }] +""" + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { [weak webView] contentRuleList, error in + guard let webView = webView, let contentRuleList = contentRuleList else { + return + } + if let _ = error { + return + } + + let configuration = webView.configuration + configuration.userContentController.add(contentRuleList) + + webView.loadFileURL(URL(fileURLWithPath: data.path), allowingReadAccessTo: URL(fileURLWithPath: data.path)) + } } } else if let webView = strongSelf.webView as? UIWebView { webView.loadRequest(URLRequest(url: URL(fileURLWithPath: data.path))) diff --git a/TelegramUI/ChatEditInterfaceMessageState.swift b/TelegramUI/ChatEditInterfaceMessageState.swift index 5451515d9e..769adcca12 100644 --- a/TelegramUI/ChatEditInterfaceMessageState.swift +++ b/TelegramUI/ChatEditInterfaceMessageState.swift @@ -9,22 +9,22 @@ enum ChatEditInterfaceMessageStateContent: Equatable { final class ChatEditInterfaceMessageState: Equatable { let content: ChatEditInterfaceMessageStateContent - let media: Media? + let mediaReference: AnyMediaReference? - init(content: ChatEditInterfaceMessageStateContent, media: Media?) { + init(content: ChatEditInterfaceMessageStateContent, mediaReference: AnyMediaReference?) { self.content = content - self.media = media + self.mediaReference = mediaReference } static func ==(lhs: ChatEditInterfaceMessageState, rhs: ChatEditInterfaceMessageState) -> Bool { if lhs.content != rhs.content { return false } - if let lhsMedia = lhs.media, let rhsMedia = rhs.media { - if !lhsMedia.isEqual(rhsMedia) { + if let lhsMedia = lhs.mediaReference, let rhsMedia = rhs.mediaReference { + if !lhsMedia.media.isEqual(rhsMedia.media) { return false } - } else if (lhs.media != nil) != (rhs.media != nil) { + } else if (lhs.mediaReference != nil) != (rhs.mediaReference != nil) { return false } return true diff --git a/TelegramUI/ChatEmptyNode.swift b/TelegramUI/ChatEmptyNode.swift index b451ffecf7..0a0dec772b 100644 --- a/TelegramUI/ChatEmptyNode.swift +++ b/TelegramUI/ChatEmptyNode.swift @@ -218,7 +218,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings - + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Empty Chat/Cloud"), color: interfaceState.theme.chat.serviceMessage.serviceMessagePrimaryTextColor) let titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: interfaceState.theme.chat.serviceMessage.serviceMessagePrimaryTextColor) @@ -247,11 +247,19 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC } let insets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) + + let imageSpacing: CGFloat = 12.0 let titleSpacing: CGFloat = 4.0 var contentWidth: CGFloat = 100.0 var contentHeight: CGFloat = 0.0 + if let image = self.iconNode.image { + contentHeight += image.size.height + contentHeight += imageSpacing + contentWidth = max(contentWidth, image.size.width) + } + var lineNodes: [(CGSize, ImmediateTextNode)] = [] for textNode in self.lineNodes { let textSize = textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right - 10.0, height: CGFloat.greatestFiniteMagnitude)) @@ -266,7 +274,14 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC contentHeight += titleSize.height + titleSpacing - let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: contentWidth, height: contentHeight)) + var imageAreaHeight: CGFloat = 0.0 + if let image = self.iconNode.image { + imageAreaHeight += image.size.height + imageAreaHeight += imageSpacing + transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - image.size.width) / 2.0), y: insets.top), size: image.size)) + } + + let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top + imageAreaHeight), size: CGSize(width: contentWidth, height: contentHeight)) let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - titleSize.width) / 2.0), y: contentRect.minY), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) diff --git a/TelegramUI/ChatImageGalleryItem.swift b/TelegramUI/ChatImageGalleryItem.swift index 7ed5d0d314..d62cf526ba 100644 --- a/TelegramUI/ChatImageGalleryItem.swift +++ b/TelegramUI/ChatImageGalleryItem.swift @@ -225,8 +225,16 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { func setFile(account: Account, fileReference: FileMediaReference) { if self.accountAndMedia == nil || !self.accountAndMedia!.1.media.isEqual(fileReference.media) { - if let largestSize = fileReference.media.dimensions { - let displaySize = largestSize.dividedByScreenScale() + if var largestSize = fileReference.media.dimensions { + var displaySize = largestSize.dividedByScreenScale() + if let previewDimensions = largestImageRepresentation(fileReference.media.previewRepresentations)?.dimensions { + let previewAspect = previewDimensions.width / previewDimensions.height + let aspect = displaySize.width / displaySize.height + if abs(previewAspect - 1.0 / aspect) < 0.1 { + displaySize = CGSize(width: displaySize.height, height: displaySize.width) + largestSize = CGSize(width: largestSize.height, height: largestSize.width) + } + } self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() self.imageNode.setSignal(chatMessageImageFile(account: account, fileReference: fileReference, thumbnail: false), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize, self.imageNode) diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index a29c1a21b8..4f7d052e3d 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -144,7 +144,7 @@ func updatedChatEditInterfaceMessagetState(state: ChatPresentationInterfaceState } else { content = .media(mediaOptions: messageMediaEditingOptions(message: message)) } - updated = updated.updatedEditMessageState(ChatEditInterfaceMessageState(content: content, media: nil)) + updated = updated.updatedEditMessageState(ChatEditInterfaceMessageState(content: content, mediaReference: nil)) return updated } @@ -450,7 +450,7 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag optionsMap[id]!.insert(.deleteLocally) } else if let peer = transaction.getPeer(id.peerId), let message = transaction.getMessage(id) { if let channel = peer as? TelegramChannel { - if message.flags.contains(.Incoming) { + if message.flags.contains(.Incoming), channel.adminRights == nil, !channel.flags.contains(.isCreator) { optionsMap[id]!.insert(.report) } if channel.hasAdminRights(.canBanUsers), case .group = channel.info { diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index 75ed69d47a..8235d1906f 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -168,7 +168,7 @@ private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNode enabled = false } } - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, account: account, peerMode: .generalSearch, peer: itemPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, account: account, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } @@ -229,7 +229,7 @@ private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNode enabled = false } } - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, account: account, peerMode: .generalSearch, peer: itemPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, account: account, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } @@ -705,6 +705,12 @@ final class ChatListNode: ListView { } } } + self.didEndScrolling = { [weak self] in + guard let strongSelf = self else { + return + } + fixSearchableListNodeScrolling(strongSelf) + } self.scrollToTopOptionPromise.set(combineLatest( renderedTotalUnreadCount(postbox: account.postbox) |> deliverOnMainQueue, diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index 8843a29fa4..587ec0a6d5 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -38,14 +38,14 @@ private enum ChatListRecentEntryStableId: Hashable { private enum ChatListRecentEntry: Comparable, Identifiable { case topPeers([Peer], PresentationTheme, PresentationStrings) - case peer(index: Int, peer: Peer, associatedPeer: Peer?, PresentationTheme, PresentationStrings, Bool) + case peer(index: Int, peer: RecentlySearchedPeer, PresentationTheme, PresentationStrings, Bool) var stableId: ChatListRecentEntryStableId { switch self { case .topPeers: return .topPeers - case let .peer(_, peer, _, _, _, _): - return .peerId(peer.id) + case let .peer(_, peer, _, _, _): + return .peerId(peer.peer.peerId) } } @@ -71,8 +71,8 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } else { return false } - case let .peer(lhsIndex, lhsPeer, lhsAssociatedPeer, lhsTheme, lhsStrings, lhsHasRevealControls): - if case let .peer(rhsIndex, rhsPeer, rhsAssociatedPeer, rhsTheme, rhsStrings, rhsHasRevealControls) = rhs, lhsPeer.isEqual(rhsPeer) && arePeersEqual(lhsAssociatedPeer, rhsAssociatedPeer) && lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings && lhsHasRevealControls == rhsHasRevealControls { + case let .peer(lhsIndex, lhsPeer, lhsTheme, lhsStrings, lhsHasRevealControls): + if case let .peer(rhsIndex, rhsPeer, rhsTheme, rhsStrings, rhsHasRevealControls) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings && lhsHasRevealControls == rhsHasRevealControls { return true } else { return false @@ -84,11 +84,11 @@ private enum ChatListRecentEntry: Comparable, Identifiable { switch lhs { case .topPeers: return true - case let .peer(lhsIndex, _, _, _, _, _): + case let .peer(lhsIndex, _, _, _, _): switch rhs { case .topPeers: return false - case let .peer(rhsIndex, _, _, _, _, _): + case let .peer(rhsIndex, _, _, _, _): return lhsIndex <= rhsIndex } } @@ -102,15 +102,15 @@ private enum ChatListRecentEntry: Comparable, Identifiable { }, peerLongTapped: { peer in peerLongTapped(peer) }) - case let .peer(_, peer, associatedPeer, theme, strings, hasRevealControls): + case let .peer(_, peer, theme, strings, hasRevealControls): let primaryPeer: Peer var chatPeer: Peer? - if let associatedPeer = associatedPeer { + let maybeChatPeer = peer.peer.peers[peer.peer.peerId]! + if let associatedPeerId = maybeChatPeer.associatedPeerId, let associatedPeer = peer.peer.peers[associatedPeerId] { primaryPeer = associatedPeer - chatPeer = peer + chatPeer = maybeChatPeer } else { - primaryPeer = peer - chatPeer = peer + primaryPeer = maybeChatPeer } var enabled = true @@ -118,7 +118,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { if let peer = chatPeer { enabled = canSendMessagesToPeer(peer) } else { - enabled = false + enabled = canSendMessagesToPeer(primaryPeer) } } if filter.contains(.onlyUsers) { @@ -142,10 +142,50 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .generalSearch, peer: primaryPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: true, editing: false, revealed: hasRevealControls), index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear.uppercased(), action: { + let status: ContactsPeerItemStatus + if let user = primaryPeer as? TelegramUser { + if let _ = user.botInfo { + status = .custom(strings.Bot_GenericBotStatus) + } else if let presence = peer.presence { + status = .presence(presence) + } else { + status = .none + } + } else if let group = primaryPeer as? TelegramGroup { + status = .custom(strings.GroupInfo_ParticipantCount(Int32(group.participantCount))) + } else if let channel = primaryPeer as? TelegramChannel { + if case .group = channel.info { + if let count = peer.subpeerSummary?.count { + status = .custom(strings.GroupInfo_ParticipantCount(Int32(count))) + } else { + status = .custom(strings.Group_Status) + } + } else { + if let count = peer.subpeerSummary?.count { + status = .custom(strings.Conversation_StatusMembers(Int32(count))) + } else { + status = .custom(strings.Channel_Status) + } + } + } else { + status = .none + } + + var isMuted = false + if let notificationSettings = peer.notificationSettings { + isMuted = notificationSettings.isRemovedFromTotalUnreadCount + } + var badge: ContactsPeerItemBadge? + if peer.unreadCount > 0 { + badge = ContactsPeerItemBadge(count: peer.unreadCount, type: isMuted ? .inactive : .active) + } + + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .generalSearch, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: status, badge: badge, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: true, editing: false, revealed: hasRevealControls), index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings, actionTitle: strings.WebSearch_RecentSectionClear.uppercased(), action: { clearRecentlySearchedPeers() }), action: { _ in - peerSelected(peer) + if let chatPeer = peer.peer.peers[peer.peer.peerId] { + peerSelected(chatPeer) + } }, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer) } } @@ -308,7 +348,7 @@ enum ChatListSearchEntry: Comparable, Identifiable { } } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .generalSearch, peer: primaryPeer, chatPeer: chatPeer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .generalSearch, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in interaction.peerSelected(peer) }) case let .globalPeer(peer, _, theme, strings): @@ -340,7 +380,7 @@ enum ChatListSearchEntry: Comparable, Identifiable { } } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .generalSearch, peer: peer.peer, chatPeer: peer.peer, status: .addressName(suffixString), enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .generalSearch, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .addressName(suffixString), enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in interaction.peerSelected(peer.peer) }) case let .message(message, presentationData): @@ -605,8 +645,8 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } var peerIds = Set() var index = 0 - loop: for renderedPeer in peers { - if let peer = renderedPeer.peers[renderedPeer.peerId] { + loop: for searchedPeer in peers { + if let peer = searchedPeer.peer.peers[searchedPeer.peer.peerId] { if peerIds.contains(peer.id) { continue loop } @@ -615,11 +655,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } peerIds.insert(peer.id) - var associatedPeer: Peer? - if let associatedPeerId = peer.associatedPeerId { - associatedPeer = renderedPeer.peers[associatedPeerId] - } - entries.append(.peer(index: index, peer: peer, associatedPeer: associatedPeer, presentationData.theme, presentationData.strings, state.peerIdWithRevealedOptions == peer.id)) + entries.append(.peer(index: index, peer: searchedPeer, presentationData.theme, presentationData.strings, state.peerIdWithRevealedOptions == peer.id)) index += 1 } } @@ -838,7 +874,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { if let result = selectedItemNode.viewAndPeerAtPoint(self.convert(location, to: selectedItemNode)) { return (result.0, result.1) } - } else if let selectedItemNode = selectedItemNode as? ContactsPeerItemNode, let peer = selectedItemNode.peer { + } else if let selectedItemNode = selectedItemNode as? ContactsPeerItemNode, let peer = selectedItemNode.chatPeer { return (selectedItemNode.view, peer.id) } else if let selectedItemNode = selectedItemNode as? ChatListItemNode, let item = selectedItemNode.item { switch item.content { diff --git a/TelegramUI/ChatListSearchItemHeader.swift b/TelegramUI/ChatListSearchItemHeader.swift index 0e11981f3b..26cbb4e276 100644 --- a/TelegramUI/ChatListSearchItemHeader.swift +++ b/TelegramUI/ChatListSearchItemHeader.swift @@ -6,6 +6,7 @@ enum ChatListSearchItemHeaderType: Int32 { case members case contacts case globalPeers + case deviceContacts case recentPeers case messages } @@ -64,6 +65,8 @@ final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode { self.sectionHeaderNode.title = strings.Contacts_TopSection.uppercased() case .globalPeers: self.sectionHeaderNode.title = strings.DialogList_SearchSectionGlobal.uppercased() + case .deviceContacts: + self.sectionHeaderNode.title = strings.Contacts_NotRegisteredSection.uppercased() case .messages: self.sectionHeaderNode.title = strings.DialogList_SearchSectionMessages.uppercased() case .recentPeers: diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index 194c9e4fb9..928877b34e 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -738,7 +738,7 @@ final class ChatMediaInputNode: ChatInputNode { if pane.supernode != nil, pane.frame.contains(point) { if let pane = pane as? ChatMediaInputGifPane { if let file = pane.fileAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) { - return .single((strongSelf, ChatContextResultPeekContent(account: strongSelf.account, contextResult: .internalReference(id: "", type: "gif", title: nil, description: nil, image: nil, file: file.media, message: .auto(caption: "", entities: nil, replyMarkup: nil)), menu: [ + return .single((strongSelf, ChatContextResultPeekContent(account: strongSelf.account, contextResult: .internalReference(queryId: 0, id: "", type: "gif", title: nil, description: nil, image: nil, file: file.media, message: .auto(caption: "", entities: nil, replyMarkup: nil)), menu: [ PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: { if let strongSelf = self { strongSelf.controllerInteraction.sendGif(file) diff --git a/TelegramUI/ChatMediaInputStickerGridItem.swift b/TelegramUI/ChatMediaInputStickerGridItem.swift index 0ee9ec1407..ca181e51e6 100644 --- a/TelegramUI/ChatMediaInputStickerGridItem.swift +++ b/TelegramUI/ChatMediaInputStickerGridItem.swift @@ -188,7 +188,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { if let (_, _, mediaDimensions) = self.currentState { let imageSize = mediaDimensions.aspectFitted(boundingSize) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) } } diff --git a/TelegramUI/ChatMediaInputStickerPackItem.swift b/TelegramUI/ChatMediaInputStickerPackItem.swift index 878632d845..1776ee5869 100644 --- a/TelegramUI/ChatMediaInputStickerPackItem.swift +++ b/TelegramUI/ChatMediaInputStickerPackItem.swift @@ -107,7 +107,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { if let item = item, let dimensions = item.file.dimensions { let imageSize = dimensions.aspectFitted(boundingImageSize) - let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingImageSize, intrinsicInsets: UIEdgeInsets())) + let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) imageApply() self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true)) self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file)).start()) diff --git a/TelegramUI/ChatMessageActionButtonsNode.swift b/TelegramUI/ChatMessageActionButtonsNode.swift index 2eb3c62b7a..a11dd56914 100644 --- a/TelegramUI/ChatMessageActionButtonsNode.swift +++ b/TelegramUI/ChatMessageActionButtonsNode.swift @@ -73,7 +73,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } } - let (titleSize, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: incoming ? theme.chat.bubble.actionButtonsIncomingTextColor : theme.chat.bubble.actionButtonsOutgoingTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let (titleSize, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: incoming ? theme.chat.bubble.actionButtonsIncomingTextColor : theme.chat.bubble.actionButtonsOutgoingTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0))) let backgroundImage: UIImage? switch position { diff --git a/TelegramUI/ChatMessageAttachedContentNode.swift b/TelegramUI/ChatMessageAttachedContentNode.swift index c40fd64b1e..9154028b24 100644 --- a/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/TelegramUI/ChatMessageAttachedContentNode.swift @@ -32,7 +32,7 @@ struct ChatMessageAttachedContentNodeMediaFlags: OptionSet { static let preferMediaBeforeText = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 1) } -private final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode { +final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode { private let textNode: TextNode private let iconNode: ASImageNode private let highlightedTextNode: TextNode @@ -379,12 +379,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } else if file.isVideo { var automaticDownload = false 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) + let (_, initialImageWidth, refineLayout) = contentImageLayout(account, presentationData.theme, presentationData.strings, message, file, automaticDownload, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if file.isSticker, let _ = file.dimensions { let 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) + let (_, initialImageWidth, refineLayout) = contentImageLayout(account, presentationData.theme, presentationData.strings, message, file, automaticDownload, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else { @@ -410,7 +410,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } else if let image = media as? TelegramMediaImage { if !flags.contains(.preferMediaInline) { 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) + let (_, initialImageWidth, refineLayout) = contentImageLayout(account, presentationData.theme, presentationData.strings, message, image, automaticDownload, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { @@ -422,7 +422,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } else if let image = media as? TelegramMediaWebFile { 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) + let (_, initialImageWidth, refineLayout) = contentImageLayout(account, presentationData.theme, presentationData.strings, message, image, automaticDownload, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } @@ -460,7 +460,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { var skipStandardStatus = false if let count = webpageGalleryMediaCount { - additionalImageBadgeContent = .text(backgroundColor: presentationData.theme.chat.bubble.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.chat.bubble.mediaDateAndStatusTextColor, shape: .corners(2.0), text: "1 \(presentationData.strings.Common_of) \(count)") + additionalImageBadgeContent = .text(backgroundColor: presentationData.theme.chat.bubble.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.chat.bubble.mediaDateAndStatusTextColor, shape: .corners(2.0), text: NSAttributedString(string: "1 \(presentationData.strings.Common_of) \(count)")) skipStandardStatus = imageMode } diff --git a/TelegramUI/ChatMessageAvatarAccessoryItem.swift b/TelegramUI/ChatMessageAvatarAccessoryItem.swift index 62dc59e223..481c5b7a49 100644 --- a/TelegramUI/ChatMessageAvatarAccessoryItem.swift +++ b/TelegramUI/ChatMessageAvatarAccessoryItem.swift @@ -9,12 +9,14 @@ final class ChatMessageAvatarAccessoryItem: ListViewAccessoryItem { private let account: Account private let peerId: PeerId private let peer: Peer? + private let messageReference: MessageReference? private let messageTimestamp: Int32 - init(account: Account, peerId: PeerId, peer: Peer?, messageTimestamp: Int32) { + init(account: Account, peerId: PeerId, peer: Peer?, messageReference: MessageReference?, messageTimestamp: Int32) { self.account = account self.peerId = peerId self.peer = peer + self.messageReference = messageReference self.messageTimestamp = messageTimestamp } @@ -30,7 +32,7 @@ final class ChatMessageAvatarAccessoryItem: ListViewAccessoryItem { let node = ChatMessageAvatarAccessoryItemNode() node.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0)) if let peer = self.peer { - node.setPeer(account: account, peer: peer) + node.setPeer(account: account, peer: peer, authorOfMessage: self.messageReference) } return node } @@ -50,7 +52,7 @@ final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode { self.addSubnode(self.avatarNode) } - func setPeer(account: Account, peer: Peer) { - self.avatarNode.setPeer(account: account, peer: peer) + func setPeer(account: Account, peer: Peer, authorOfMessage: MessageReference?) { + self.avatarNode.setPeer(account: account, peer: peer, authorOfMessage: authorOfMessage) } } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 37e250fc4d..298776a67a 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -131,7 +131,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private var actionButtonsNode: ChatMessageActionButtonsNode? private var shareButtonNode: HighlightableButtonNode? - + private var backgroundType: ChatMessageBackgroundType? private var highlightedState: Bool = false @@ -262,6 +262,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if strongSelf.selectionNode != nil { return false } + for media in item.content.firstMessage.media { + if media is TelegramMediaAction { + return false + } + } return item.controllerInteraction.canSetupReply(item.message) } return false @@ -274,6 +279,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { for contentNode in self.contentNodes { if let message = contentNode.item?.message { currentContentClassesPropertiesAndLayouts.append((message, type(of: contentNode) as AnyClass, contentNode.supportsMosaic, contentNode.asyncLayoutContent())) + } else { + assertionFailure() } } @@ -1587,12 +1594,21 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { case .call: break case .openMessage: - foundTapAction = true + foundTapAction = false break } } if !foundTapAction, let tapMessage = tapMessage { - item.controllerInteraction.openMessageContextMenu(tapMessage, self, self.backgroundNode.frame) + var subFrame = self.backgroundNode.frame + if case .group = item.content { + for contentNode in self.contentNodes { + if contentNode.item?.message.stableId == tapMessage.stableId { + subFrame = contentNode.frame.insetBy(dx: 0.0, dy: -4.0) + break + } + } + } + item.controllerInteraction.openMessageContextMenu(tapMessage, self, subFrame) } } default: diff --git a/TelegramUI/ChatMessageContactBubbleContentNode.swift b/TelegramUI/ChatMessageContactBubbleContentNode.swift index 41c9ea7cec..f785ecfb4e 100644 --- a/TelegramUI/ChatMessageContactBubbleContentNode.swift +++ b/TelegramUI/ChatMessageContactBubbleContentNode.swift @@ -19,17 +19,23 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { private var contact: TelegramMediaContact? private var contactPhone: String? + private let buttonNode: ChatMessageAttachedContentButtonNode + required init() { self.avatarNode = AvatarNode(font: avatarFont) self.dateAndStatusNode = ChatMessageDateAndStatusNode() self.titleNode = TextNode() self.textNode = TextNode() + self.buttonNode = ChatMessageAttachedContentButtonNode() super.init() self.addSubnode(self.avatarNode) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } required init?(coder aDecoder: NSCoder) { @@ -47,6 +53,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { let statusLayout = self.dateAndStatusNode.asyncLayout() let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode) let previousContact = self.contact let previousContactPhone = self.contactPhone @@ -140,7 +147,31 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { statusApply = apply } - let contentWidth = avatarSize.width + max(statusSize.width, max(titleLayout.size.width, textLayout.size.width)) + layoutConstants.text.bubbleInsets.right + 8.0 + let buttonImage: UIImage + let buttonHighlightedImage: UIImage + let titleColor: UIColor + let titleHighlightedColor: UIColor + if item.message.effectivelyIncoming(item.account.peerId) { + buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(item.presentationData.theme)! + buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(item.presentationData.theme)! + titleColor = item.presentationData.theme.chat.bubble.incomingAccentTextColor + titleHighlightedColor = item.presentationData.theme.chat.bubble.incomingFillColor + } else { + buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(item.presentationData.theme)! + buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(item.presentationData.theme)! + titleColor = item.presentationData.theme.chat.bubble.outgoingAccentTextColor + titleHighlightedColor = item.presentationData.theme.chat.bubble.outgoingFillColor + } + + let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, item.presentationData.strings.Conversation_ViewContactDetails, titleColor, titleHighlightedColor) + + var maxContentWidth: CGFloat = 0.0 + maxContentWidth = max(maxContentWidth, statusSize.width) + maxContentWidth = max(maxContentWidth, titleLayout.size.width) + maxContentWidth = max(maxContentWidth, textLayout.size.width) + maxContentWidth = max(maxContentWidth, buttonWidth) + + let contentWidth = avatarSize.width + maxContentWidth + layoutConstants.text.bubbleInsets.right + 8.0 return (contentWidth, { boundingWidth in let layoutSize: CGSize @@ -148,8 +179,12 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { let baseAvatarFrame = CGRect(origin: CGPoint(), size: avatarSize) - layoutSize = CGSize(width: contentWidth, height: 63.0) - statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - layoutConstants.text.bubbleInsets.right, y: layoutSize.height - statusSize.height - 5.0), size: statusSize) + let (buttonSize, buttonApply) = continueLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0) + let buttonSpacing: CGFloat = 4.0 + + layoutSize = CGSize(width: contentWidth, height: 66.0 + buttonSize.height + buttonSpacing) + statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - layoutConstants.text.bubbleInsets.right, y: layoutSize.height - statusSize.height - 9.0 - buttonSpacing - buttonSize.height), size: statusSize) + let buttonFrame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.right, y: layoutSize.height - 9.0 - buttonSize.height), size: buttonSize) let avatarFrame = baseAvatarFrame.offsetBy(dx: 5.0, dy: 5.0) var customLetters: [String] = [] @@ -157,11 +192,11 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { let firstName = selectedContact.firstName let lastName = selectedContact.lastName if !firstName.isEmpty && !lastName.isEmpty { - customLetters = [firstName.substring(to: firstName.index(after: firstName.startIndex)).uppercased(), lastName.substring(to: lastName.index(after: lastName.startIndex)).uppercased()] + customLetters = [String(firstName[.. ChatMessageBubbleContentTapAction { + if self.buttonNode.frame.contains(point) { + return .openMessage + } return .none } @objc func contactTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let item = self.item { - item.controllerInteraction.openMessage(item.message) + let _ = item.controllerInteraction.openMessage(item.message) } } } + + @objc private func buttonPressed() { + if let item = self.item { + let _ = item.controllerInteraction.openMessage(item.message) + } + } } diff --git a/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift b/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift index c2c1529570..425c930cb5 100644 --- a/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift +++ b/TelegramUI/ChatMessageInteractiveInstantVideoNode.swift @@ -136,14 +136,14 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let file = media as? TelegramMediaFile { updatedFile = file if let previousFile = previousFile { - updatedMedia = !previousFile.isEqual(file) + updatedMedia = !previousFile.resource.isEqual(to: file.resource) } else if previousFile == nil { updatedMedia = true } } else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let file = content.file { updatedFile = file if let previousFile = previousFile { - updatedMedia = !previousFile.isEqual(file) + updatedMedia = !previousFile.resource.isEqual(to: file.resource) } else if previousFile == nil { updatedMedia = true } @@ -425,7 +425,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if strongSelf.infoBackgroundNode.alpha.isZero { item.account.telegramApplicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: .voice) } else { - let _ = item.controllerInteraction.openMessage(item.message) + //let _ = item.controllerInteraction.openMessage(item.message) } } } @@ -491,6 +491,9 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let statusNode = self.statusNode, statusNode.supernode != nil, !statusNode.isHidden, statusNode.frame.contains(point) { return self.view } + if let videoNode = self.videoNode, videoNode.frame.contains(point) { + return self.view + } return super.hitTest(point, with: event) } diff --git a/TelegramUI/ChatMessageInteractiveMediaBadge.swift b/TelegramUI/ChatMessageInteractiveMediaBadge.swift index 98959141b6..0d1171b48c 100644 --- a/TelegramUI/ChatMessageInteractiveMediaBadge.swift +++ b/TelegramUI/ChatMessageInteractiveMediaBadge.swift @@ -25,12 +25,12 @@ enum ChatMessageInteractiveMediaBadgeShape: Equatable { } enum ChatMessageInteractiveMediaBadgeContent: Equatable { - case text(backgroundColor: UIColor, foregroundColor: UIColor, shape: ChatMessageInteractiveMediaBadgeShape, text: String) + case text(backgroundColor: UIColor, foregroundColor: UIColor, shape: ChatMessageInteractiveMediaBadgeShape, text: NSAttributedString) static func ==(lhs: ChatMessageInteractiveMediaBadgeContent, rhs: ChatMessageInteractiveMediaBadgeContent) -> Bool { switch lhs { case let .text(lhsBackgroundColor, lhsForegroundColor, lhsShape, lhsText): - if case let .text(rhsBackgroundColor, rhsForegroundColor, rhsShape, rhsText) = rhs, lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsShape == rhsShape, lhsText == rhsText { + if case let .text(rhsBackgroundColor, rhsForegroundColor, rhsShape, rhsText) = rhs, lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsShape == rhsShape, lhsText.isEqual(to: rhsText) { return true } else { return false @@ -40,6 +40,7 @@ enum ChatMessageInteractiveMediaBadgeContent: Equatable { } private let font = Font.regular(11.0) +private let boldFont = Font.semibold(11.0) private final class ChatMessageInteractiveMediaBadgeParams: NSObject { let content: ChatMessageInteractiveMediaBadgeContent? @@ -74,8 +75,13 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode { if let content = (withParameters as? ChatMessageInteractiveMediaBadgeParams)?.content { switch content { case let .text(backgroundColor, foregroundColor, shape, text): - let nsText: NSString = text as NSString - let textRect = nsText.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil) + let convertedText = NSMutableAttributedString(string: text.string, attributes: [.font: font, .foregroundColor: foregroundColor]) + text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: []) { attributes, range, _ in + if let _ = attributes[ChatTextInputAttributes.bold] { + convertedText.addAttribute(.font, value: boldFont, range: range) + } + } + let textRect = convertedText.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) let imageSize = CGSize(width: ceil(textRect.size.width) + 10.0, height: 18.0) return generateImage(imageSize, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) @@ -98,7 +104,7 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode { } context.setBlendMode(.normal) UIGraphicsPushContext(context) - nsText.draw(at: CGPoint(x: floor((size.width - textRect.size.width) / 2.0) + textRect.origin.x, y: 2.0 + textRect.origin.y), withAttributes: [.font: font, .foregroundColor: foregroundColor]) + convertedText.draw(at: CGPoint(x: floor((size.width - textRect.size.width) / 2.0) + textRect.origin.x, y: 2.0 + textRect.origin.y)) UIGraphicsPopContext() }) } diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index ea55b37b65..87753f2a17 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -6,7 +6,7 @@ import Display import TelegramCore private struct FetchControls { - let fetch: () -> Void + let fetch: (Bool) -> Void let cancel: () -> Void } @@ -24,9 +24,11 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { private var tapRecognizer: UITapGestureRecognizer? private var account: Account? - private var messageIdAndFlags: (MessageId, MessageFlags)? + private var message: Message? private var media: Media? private var themeAndStrings: (PresentationTheme, PresentationStrings)? + private var sizeCalculation: InteractiveMediaNodeSizeCalculation? + private var automaticPlayback: Bool? private let statusDisposable = MetaDisposable() private let fetchControls = Atomic(value: nil) @@ -78,9 +80,9 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if let fetchStatus = self.fetchStatus { switch fetchStatus { case .Fetching: - if let account = self.account, let (messageId, flags) = self.messageIdAndFlags, flags.isSending { + if let account = self.account, let message = self.message, message.flags.isSending { let _ = account.postbox.transaction({ transaction -> Void in - deleteMessages(transaction: transaction, mediaBox: account.postbox.mediaBox, ids: [messageId]) + deleteMessages(transaction: transaction, mediaBox: account.postbox.mediaBox, ids: [message.id]) }).start() } if let cancel = self.fetchControls.with({ return $0?.cancel }) { @@ -88,7 +90,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } case .Remote: if let fetch = self.fetchControls.with({ return $0?.fetch }) { - fetch() + fetch(true) } case .Local: break @@ -102,7 +104,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { self.activateLocalContent() } else { - if let (_, flags) = self.messageIdAndFlags, flags.isSending { + if let message = self.message, message.flags.isSending { if let statusNode = self.statusNode, statusNode.frame.contains(point) { self.progressPressed() } @@ -113,25 +115,17 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - func asyncLayout() -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition) -> Void))) { - let currentMessageIdAndFlags = self.messageIdAndFlags + func asyncLayout() -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: Bool, _ automaticPlayback: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition) -> Void))) { + let currentMessage = self.message let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() let currentVideoNode = self.videoNode let hasCurrentVideoNode = currentVideoNode != nil - let currentTheme = self.themeAndStrings?.0 - - return { [weak self] account, theme, strings, message, media, automaticDownload, sizeCalculation, layoutConstants in + return { [weak self] account, theme, strings, message, media, automaticDownload, automaticPlayback, sizeCalculation, layoutConstants in var nativeSize: CGSize - var updatedTheme: PresentationTheme? - - if theme !== currentTheme { - updatedTheme = theme - } - let isSecretMedia = message.containsSecretMedia var secretBeginTimeAndTimeout: (Double, Double)? if isSecretMedia { @@ -145,13 +139,6 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - var webpage: TelegramMediaWebpage? - for m in message.media { - if let m = m as? TelegramMediaWebpage { - webpage = m - } - } - var isInlinePlayableVideo = false var unboundSize: CGSize @@ -164,7 +151,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } else if file.isSticker { unboundSize = unboundSize.aspectFilled(CGSize(width: 162.0, height: 162.0)) } - isInlinePlayableVideo = file.isVideo && file.isAnimated + isInlinePlayableVideo = file.isVideo && file.isAnimated && !isSecretMedia && automaticPlayback } else if let image = media as? TelegramMediaWebFile, let dimensions = image.dimensions { unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) } else { @@ -184,10 +171,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } else { maxWidth = layoutConstants.image.maxDimensions.width } - - var secretProgressIcon: UIImage? if isSecretMedia { - secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) + let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) } return (nativeSize, maxWidth, { constrainedSize, corners in @@ -240,7 +225,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } var statusUpdated = mediaUpdated - if currentMessageIdAndFlags?.0 != message.id || currentMessageIdAndFlags?.1 != message.flags { + if currentMessage?.id != message.id || currentMessage?.flags != message.flags { statusUpdated = true } @@ -258,12 +243,19 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { updateImageSignal = chatMessagePhoto(postbox: account.postbox, photoReference: .message(message: MessageReference(message), media: image)) } - updatedFetchControls = FetchControls(fetch: { + updatedFetchControls = FetchControls(fetch: { manual in if let strongSelf = self { - strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photoReference: .message(message: MessageReference(message), media: image)).start()) + if !manual { + strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photoReference: .message(message: MessageReference(message), media: image)).start()) + } else if let resource = largestRepresentationForPhoto(image)?.resource { + strongSelf.fetchDisposable.set(messageMediaImageInteractiveFetched(account: account, message: message, image: image, resource: resource).start()) + } } }, cancel: { chatMessagePhotoCancelInteractiveFetch(account: account, photoReference: .message(message: MessageReference(message), media: image)) + if let resource = largestRepresentationForPhoto(image)?.resource { + messageMediaImageCancelInteractiveFetch(account: account, messageId: message.id, image: image, resource: resource) + } }) } else if let image = media as? TelegramMediaWebFile { if hasCurrentVideoNode { @@ -271,7 +263,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } updateImageSignal = chatWebFileImage(account: account, file: image) - updatedFetchControls = FetchControls(fetch: { + updatedFetchControls = FetchControls(fetch: { _ in if let strongSelf = self { strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: account, image: image).start()) } @@ -289,7 +281,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - if isInlinePlayableVideo { + if file.isVideo && file.isAnimated && !isSecretMedia && automaticPlayback { updateVideoFile = file if hasCurrentVideoNode { } else { @@ -301,7 +293,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - updatedFetchControls = FetchControls(fetch: { + updatedFetchControls = FetchControls(fetch: { _ in if let strongSelf = self { if file.isAnimated { strongSelf.fetchDisposable.set(fetchedMediaResource(postbox: account.postbox, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes)).start()) @@ -359,19 +351,14 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { let imageApply = imageLayout(arguments) - let radialStatusSize: CGFloat - if case .unconstrained = sizeCalculation { - radialStatusSize = 32.0 - } else { - radialStatusSize = 50.0 - } - return (boundingSize, { transition in if let strongSelf = self { strongSelf.account = account - strongSelf.messageIdAndFlags = (message.id, message.flags) + strongSelf.message = message strongSelf.media = media strongSelf.themeAndStrings = (theme, strings) + strongSelf.sizeCalculation = sizeCalculation + strongSelf.automaticPlayback = automaticPlayback transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) strongSelf.statusNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) @@ -383,7 +370,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } if replaceVideoNode, let updatedVideoFile = updateVideoFile { - let videoNode = UniversalVideoNode(postbox: account.postbox, audioSession: account.telegramApplicationContext.mediaManager.audioSession, manager: account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: ChatBubbleVideoDecoration(cornerRadius: 17.0, nativeSize: nativeSize), content: NativeVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), enableSound: false, fetchAutomatically: false), priority: .embedded) + let cornerRadius: CGFloat = arguments.corners.topLeft.radius + let videoNode = UniversalVideoNode(postbox: account.postbox, audioSession: account.telegramApplicationContext.mediaManager.audioSession, manager: account.telegramApplicationContext.mediaManager.universalVideoManager, decoration: ChatBubbleVideoDecoration(cornerRadius: cornerRadius, nativeSize: nativeSize), content: NativeVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), enableSound: false, fetchAutomatically: false), priority: .embedded) videoNode.isUserInteractionEnabled = false strongSelf.videoNode = videoNode @@ -423,116 +411,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { displayLinkDispatcher.dispatch { if let strongSelf = strongSelf { strongSelf.fetchStatus = status - - var progressRequired = false - if let _ = secretBeginTimeAndTimeout { - progressRequired = true - } else { - if case .Local = status { - if let file = media as? TelegramMediaFile, file.isVideo { - progressRequired = true - } else if isSecretMedia { - progressRequired = true - } else if let webpage = webpage, case let .Loaded(content) = webpage.content, content.embedUrl != nil { - progressRequired = true - } - } else { - progressRequired = true - } - } - - if progressRequired { - if strongSelf.statusNode == nil { - let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.bubble.mediaOverlayControlBackgroundColor) - statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: radialStatusSize, height: radialStatusSize)) - statusNode.position = strongSelf.imageNode.position - strongSelf.statusNode = statusNode - strongSelf.addSubnode(statusNode) - } - } else { - if let statusNode = strongSelf.statusNode { - statusNode.transitionToState(.none, completion: { [weak statusNode] in - statusNode?.removeFromSupernode() - }) - strongSelf.statusNode = nil - } - } - - var state: RadialStatusNodeState - var badgeContent: ChatMessageInteractiveMediaBadgeContent? - let bubbleTheme = theme.chat.bubble - switch status { - case let .Fetching(isActive, progress): - var adjustedProgress = progress - if isActive { - adjustedProgress = max(adjustedProgress, 0.027) - } - if let (_, flags) = strongSelf.messageIdAndFlags, flags.isSending && adjustedProgress.isEqual(to: 1.0), case .unconstrained = sizeCalculation { - state = .check(bubbleTheme.mediaOverlayControlForegroundColor) - } else { - state = .progress(color: bubbleTheme.mediaOverlayControlForegroundColor, value: CGFloat(adjustedProgress), cancelEnabled: true) - } - if case .constrained = sizeCalculation { - if let file = media as? TelegramMediaFile, (!file.isAnimated || message.flags.contains(.Unsent)) { - if let size = file.size { - badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: "\(dataSizeString(Int(Float(size) * progress))) / \(dataSizeString(size))") - } else if let _ = file.duration { - badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: strings.Conversation_Processing) - } - } - } - case .Local: - state = .none - if isSecretMedia, let (beginTime, timeout) = secretBeginTimeAndTimeout { - state = .secretTimeout(color: bubbleTheme.mediaOverlayControlForegroundColor, icon: secretProgressIcon, beginTime: beginTime, timeout: timeout) - } else if isSecretMedia, let secretProgressIcon = secretProgressIcon { - state = .customIcon(secretProgressIcon) - } else if let file = media as? TelegramMediaFile { - if !isInlinePlayableVideo && file.isVideo { - state = .play(bubbleTheme.mediaOverlayControlForegroundColor) - } else { - state = .none - } - } else if let webpage = webpage, case let .Loaded(content) = webpage.content, content.embedUrl != nil { - state = .play(bubbleTheme.mediaOverlayControlForegroundColor) - } - if case .constrained = sizeCalculation { - if let file = media as? TelegramMediaFile, let duration = file.duration, !file.isAnimated { - let durationString = String(format: "%d:%02d", duration / 60, duration % 60) - badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: durationString) - } - } - case .Remote: - state = .download(bubbleTheme.mediaOverlayControlForegroundColor) - if case .constrained = sizeCalculation { - if let file = media as? TelegramMediaFile, let duration = file.duration, !file.isAnimated { - let durationString = String(format: "%d:%02d", duration / 60, duration % 60) - badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: durationString) - } - } - } - if let statusNode = strongSelf.statusNode { - if state == .none { - strongSelf.statusNode = nil - } - statusNode.transitionToState(state, completion: { [weak statusNode] in - if state == .none { - statusNode?.removeFromSupernode() - } - }) - } - if let badgeContent = badgeContent { - if strongSelf.badgeNode == nil { - let badgeNode = ChatMessageInteractiveMediaBadge() - badgeNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: radialStatusSize, height: radialStatusSize)) - strongSelf.badgeNode = badgeNode - strongSelf.addSubnode(badgeNode) - } - strongSelf.badgeNode?.content = badgeContent - } else if let badgeNode = strongSelf.badgeNode { - strongSelf.badgeNode = nil - badgeNode.removeFromSupernode() - } + strongSelf.updateFetchStatus() } } })) @@ -542,7 +421,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { let _ = strongSelf.fetchControls.swap(updatedFetchControls) if automaticDownload { if let image = media as? TelegramMediaImage { - strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photoReference: .message(message: MessageReference(message), media: image)).start()) + updatedFetchControls.fetch(false) } else if let image = media as? TelegramMediaWebFile { strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: account, image: image).start()) } else if let file = media as? TelegramMediaFile { @@ -552,6 +431,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } imageApply() + + strongSelf.updateFetchStatus() } }) }) @@ -559,12 +440,173 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: Bool, _ sizeCalcilation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition) -> ChatMessageInteractiveMediaNode))) { + private func updateFetchStatus() { + guard let (theme, strings) = self.themeAndStrings, let sizeCalculation = self.sizeCalculation, let message = self.message, let automaticPlayback = self.automaticPlayback else { + return + } + + var secretBeginTimeAndTimeout: (Double, Double)? + let isSecretMedia = message.containsSecretMedia + if isSecretMedia { + for attribute in message.attributes { + if let attribute = attribute as? AutoremoveTimeoutMessageAttribute { + if let countdownBeginTime = attribute.countdownBeginTime { + secretBeginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) + } + break + } + } + } + + var webpage: TelegramMediaWebpage? + var invoice: TelegramMediaInvoice? + for m in message.media { + if let m = m as? TelegramMediaWebpage { + webpage = m + } else if let m = m as? TelegramMediaInvoice { + invoice = m + } + } + + var progressRequired = false + if let _ = secretBeginTimeAndTimeout { + progressRequired = true + } else if let fetchStatus = self.fetchStatus { + if case .Local = fetchStatus { + if let file = media as? TelegramMediaFile, file.isVideo { + progressRequired = true + } else if isSecretMedia { + progressRequired = true + } else if let webpage = webpage, case let .Loaded(content) = webpage.content, content.embedUrl != nil { + progressRequired = true + } + } else { + progressRequired = true + } + } + + let radialStatusSize: CGFloat + if case .unconstrained = sizeCalculation { + radialStatusSize = 32.0 + } else { + radialStatusSize = 50.0 + } + + if progressRequired { + if self.statusNode == nil { + let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.bubble.mediaOverlayControlBackgroundColor) + statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: radialStatusSize, height: radialStatusSize)) + statusNode.position = self.imageNode.position + self.statusNode = statusNode + self.addSubnode(statusNode) + } + } else { + if let statusNode = self.statusNode { + statusNode.transitionToState(.none, completion: { [weak statusNode] in + statusNode?.removeFromSupernode() + }) + self.statusNode = nil + } + } + + var state: RadialStatusNodeState = .none + var badgeContent: ChatMessageInteractiveMediaBadgeContent? + let bubbleTheme = theme.chat.bubble + if let invoice = invoice { + let string = NSMutableAttributedString() + if invoice.receiptMessageId != nil { + string.append(NSAttributedString(string: strings.Checkout_Receipt_Title.uppercased())) + } else { + string.append(NSAttributedString(string: "\(formatCurrencyAmount(invoice.totalAmount, currency: invoice.currency)) ", attributes: [ChatTextInputAttributes.bold: true as NSNumber])) + string.append(NSAttributedString(string: strings.Message_InvoiceLabel)) + } + badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: string) + } + if let fetchStatus = self.fetchStatus { + switch fetchStatus { + case let .Fetching(isActive, progress): + var adjustedProgress = progress + if isActive { + adjustedProgress = max(adjustedProgress, 0.027) + } + if message.flags.isSending && adjustedProgress.isEqual(to: 1.0), case .unconstrained = sizeCalculation { + state = .check(bubbleTheme.mediaOverlayControlForegroundColor) + } else { + state = .progress(color: bubbleTheme.mediaOverlayControlForegroundColor, value: CGFloat(adjustedProgress), cancelEnabled: true) + } + if case .constrained = sizeCalculation { + if let file = media as? TelegramMediaFile, (!file.isAnimated || message.flags.contains(.Unsent)) { + if let size = file.size { + badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: "\(dataSizeString(Int(Float(size) * progress))) / \(dataSizeString(size))")) + } else if let _ = file.duration { + badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: strings.Conversation_Processing)) + } + } + } + case .Local: + state = .none + let secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) + if isSecretMedia, let (beginTime, timeout) = secretBeginTimeAndTimeout { + state = .secretTimeout(color: bubbleTheme.mediaOverlayControlForegroundColor, icon: secretProgressIcon, beginTime: beginTime, timeout: timeout) + } else if isSecretMedia, let secretProgressIcon = secretProgressIcon { + state = .customIcon(secretProgressIcon) + } else if let file = media as? TelegramMediaFile { + let isInlinePlayableVideo = file.isVideo && file.isAnimated && !isSecretMedia && automaticPlayback + + if !isInlinePlayableVideo && file.isVideo { + state = .play(bubbleTheme.mediaOverlayControlForegroundColor) + } else { + state = .none + } + } else if let webpage = webpage, case let .Loaded(content) = webpage.content, content.embedUrl != nil { + state = .play(bubbleTheme.mediaOverlayControlForegroundColor) + } + if case .constrained = sizeCalculation { + if let file = media as? TelegramMediaFile, let duration = file.duration, !file.isAnimated { + let durationString = String(format: "%d:%02d", duration / 60, duration % 60) + badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) + } + } + case .Remote: + state = .download(bubbleTheme.mediaOverlayControlForegroundColor) + if case .constrained = sizeCalculation { + if let file = media as? TelegramMediaFile, let duration = file.duration, !file.isAnimated { + let durationString = String(format: "%d:%02d", duration / 60, duration % 60) + badgeContent = .text(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) + } + } + } + } + if let statusNode = self.statusNode { + if state == .none { + self.statusNode = nil + } + statusNode.transitionToState(state, completion: { [weak statusNode] in + if state == .none { + statusNode?.removeFromSupernode() + } + }) + } + if let badgeContent = badgeContent { + if self.badgeNode == nil { + let badgeNode = ChatMessageInteractiveMediaBadge() + badgeNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: radialStatusSize, height: radialStatusSize)) + self.badgeNode = badgeNode + self.addSubnode(badgeNode) + } + self.badgeNode?.content = badgeContent + } else if let badgeNode = self.badgeNode { + self.badgeNode = nil + badgeNode.removeFromSupernode() + } + } + + static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: Bool, _ automaticPlayback: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition) -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() - return { account, theme, strings, message, media, automaticDownload, sizeCalculation, layoutConstants in + return { account, theme, strings, message, media, automaticDownload, automaticPlayback, sizeCalculation, layoutConstants in var imageNode: ChatMessageInteractiveMediaNode - var imageLayout: (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition) -> Void))) + var imageLayout: (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: Bool, _ automaticPlayback: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition) -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { imageNode = node @@ -574,7 +616,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { imageLayout = imageNode.asyncLayout() } - let (unboundSize, initialWidth, continueLayout) = imageLayout(account, theme, strings, message, media, automaticDownload, sizeCalculation, layoutConstants) + let (unboundSize, initialWidth, continueLayout) = imageLayout(account, theme, strings, message, media, automaticDownload, automaticPlayback, sizeCalculation, layoutConstants) return (unboundSize, initialWidth, { constrainedSize, corners in let (finalWidth, finalLayout) = continueLayout(constrainedSize, corners) @@ -594,4 +636,12 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { func setOverlayColor(_ color: UIColor?, animated: Bool) { self.imageNode.setOverlayColor(color, animated: animated) } + + func isReadyForInteractivePreview() -> Bool { + if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { + return true + } else { + return false + } + } } diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index ea4e471f88..67569dcb15 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -280,7 +280,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } if !hasActionMedia && !isBroadcastChannel { if let effectiveAuthor = effectiveAuthor { - accessoryItem = ChatMessageAvatarAccessoryItem(account: account, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageTimestamp: content.index.timestamp) + accessoryItem = ChatMessageAvatarAccessoryItem(account: account, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), messageTimestamp: content.index.timestamp) } } } diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index a74865f610..fdeb421322 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -78,7 +78,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { sizeCalculation = .unconstrained } - let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.account, item.presentationData.theme, item.presentationData.strings, item.message, selectedMedia!, automaticDownload, sizeCalculation, layoutConstants) + let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.account, item.presentationData.theme, item.presentationData.strings, item.message, selectedMedia!, automaticDownload, item.controllerInteraction.automaticMediaDownloadSettings.autoplayGifs, sizeCalculation, layoutConstants) var forceFullCorners = false if let media = selectedMedia as? TelegramMediaFile, media.isAnimated { @@ -237,7 +237,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { if let message = self.item?.message, let currentMedia = self.media, !message.containsSecretMedia { - if self.interactiveImageNode.frame.contains(point) { + if self.interactiveImageNode.frame.contains(point), self.interactiveImageNode.isReadyForInteractivePreview() { return (message, .media(currentMedia)) } } diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index 8f3ca5e8a9..70ff66a864 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -102,7 +102,11 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.contentNode) self.contentNode.openMedia = { [weak self] in if let strongSelf = self, let item = strongSelf.item { - item.controllerInteraction.openMessage(item.message) + if let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content, content.instantPage != nil { + item.controllerInteraction.openInstantPage(item.message) + } else { + let _ = item.controllerInteraction.openMessage(item.message) + } } } self.contentNode.activateAction = { [weak self] in @@ -178,19 +182,19 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { switch type { case .instagram, .twitter: - mainMedia = webpage.image + mainMedia = webpage.image ?? webpage.file default: mainMedia = webpage.file ?? webpage.image } if let file = mainMedia as? TelegramMediaFile { - if let image = webpage.image, let embedUrl = webpage.embedUrl, !embedUrl.isEmpty { - mediaAndFlags = (image, [.preferMediaBeforeText]) + if let embedUrl = webpage.embedUrl, !embedUrl.isEmpty { + mediaAndFlags = (webpage.image ?? file, [.preferMediaBeforeText]) } else { mediaAndFlags = (file, []) } } else if let image = mainMedia as? TelegramMediaImage { - if let type = webpage.type, ["photo", "video", "embed", "article"].contains(type) { + if let type = webpage.type, ["photo", "video", "embed", "article", "gif"].contains(type) { var flags = ChatMessageAttachedContentNodeMediaFlags() if webpage.instantPage != nil, let largest = largestImageRepresentation(image.representations) { if largest.dimensions.width >= 256.0 { diff --git a/TelegramUI/ChatRecentActionsController.swift b/TelegramUI/ChatRecentActionsController.swift index c29c8d4c1a..3396b1c338 100644 --- a/TelegramUI/ChatRecentActionsController.swift +++ b/TelegramUI/ChatRecentActionsController.swift @@ -89,7 +89,7 @@ final class ChatRecentActionsController: ViewController { self.navigationItem.titleView = self.titleView - let rightButton = ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) + let rightButton = ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) self.navigationItem.setRightBarButton(rightButton.buttonItem, animated: false) self.titleView.title = self.presentationData.strings.Channel_AdminLog_TitleAllEvents diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index 1cd38a16f2..435a676c28 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -698,7 +698,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { switch result { case let .externalUrl(url): if let navigationController = strongSelf.getNavigationController() { - openExternalUrl(account: strongSelf.account, url: url, presentationData: strongSelf.presentationData, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: navigationController) + openExternalUrl(account: strongSelf.account, url: url, presentationData: strongSelf.presentationData, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: navigationController, dismissInput: { + self?.view.endEditing(true) + }) } case let .peer(peerId): strongSelf.openPeer(peerId: peerId, peer: nil) @@ -728,6 +730,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } }, present: { c, a in self?.presentController(c, a) + }, dismissInput: { + self?.view.endEditing(true) }) } } diff --git a/TelegramUI/ChatRecentActionsHistoryTransition.swift b/TelegramUI/ChatRecentActionsHistoryTransition.swift index 6728547e53..9d1781c780 100644 --- a/TelegramUI/ChatRecentActionsHistoryTransition.swift +++ b/TelegramUI/ChatRecentActionsHistoryTransition.swift @@ -212,7 +212,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var photo: TelegramMediaImage? if !new.isEmpty { - photo = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: new, reference: nil) + photo = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: new, reference: nil, partialReference: nil) } let action = TelegramMediaActionType.photoUpdated(image: photo) @@ -437,6 +437,13 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } } + for media in message.media { + for peerId in media.peerIds { + if let peer = self.entry.peers[peerId] { + peers[peer.id] = peer + } + } + } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) } diff --git a/TelegramUI/ChatRecentActionsSearchNavigationContentNode.swift b/TelegramUI/ChatRecentActionsSearchNavigationContentNode.swift index 3768e2f18e..1f27918060 100644 --- a/TelegramUI/ChatRecentActionsSearchNavigationContentNode.swift +++ b/TelegramUI/ChatRecentActionsSearchNavigationContentNode.swift @@ -22,7 +22,7 @@ final class ChatRecentActionsSearchNavigationContentNode: NavigationBarContentNo self.cancel = cancel - self.searchBar = SearchBarNode(theme: theme, strings: strings) + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme), strings: strings) let placeholderText = strings.Common_Search self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputPlaceholderTextColor) diff --git a/TelegramUI/ChatSearchNavigationContentNode.swift b/TelegramUI/ChatSearchNavigationContentNode.swift index 9405173405..2c2745614f 100644 --- a/TelegramUI/ChatSearchNavigationContentNode.swift +++ b/TelegramUI/ChatSearchNavigationContentNode.swift @@ -20,7 +20,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode { self.chatLocation = chatLocation self.interaction = interaction - self.searchBar = SearchBarNode(theme: theme, strings: strings) + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme), strings: strings) let placeholderText: String switch chatLocation { case .peer: diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index fbce927f3a..9935cd025f 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -270,6 +270,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { super.init() + self.view.disablesInteractiveTransitionGestureRecognizer = true + self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), for: .touchUpInside) self.view.addSubview(self.attachmentButton) diff --git a/TelegramUI/CheckDeviceAccess.swift b/TelegramUI/CheckDeviceAccess.swift index 34bcbe4e93..b88b1fbb2f 100644 --- a/TelegramUI/CheckDeviceAccess.swift +++ b/TelegramUI/CheckDeviceAccess.swift @@ -5,30 +5,33 @@ import Display import SwiftSignalKit import Photos import CoreLocation +import Contacts +import AddressBook import LegacyComponents -enum DeviceAccessMicrophoneSubject { +public enum DeviceAccessMicrophoneSubject { case audio case video } -enum DeviceAccessMediaLibrarySubject { +public enum DeviceAccessMediaLibrarySubject { case send case save } -enum DeviceAccessLocationSubject { +public enum DeviceAccessLocationSubject { case send case live case tracking } -enum DeviceAccessSubject { +public enum DeviceAccessSubject { case camera case microphone(DeviceAccessMicrophoneSubject) case mediaLibrary(DeviceAccessMediaLibrarySubject) case location(DeviceAccessLocationSubject) + case contacts } private enum AccessType { @@ -39,120 +42,175 @@ private enum AccessType { private let cachedMediaLibraryAccessStatus = Atomic(value: nil) -func authorizeDeviceAccess(to subject: DeviceAccessSubject, presentationData: PresentationData, present: @escaping (ViewController, Any?) -> Void, openSettings: @escaping () -> Void, _ completion: @escaping (Bool) -> Void) { - switch subject { - case .camera: - let status = PGCamera.cameraAuthorizationStatus() - if status == PGCameraAuthorizationStatusNotDetermined { - completion(true) - } else if status == PGCameraAuthorizationStatusRestricted || status == PGCameraAuthorizationStatusDenied { - let text: String - if status == PGCameraAuthorizationStatusRestricted { - text = presentationData.strings.AccessDenied_CameraRestricted - } else { - text = presentationData.strings.AccessDenied_Camera - } - completion(false) - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { - openSettings() - })]), nil) - } else if status == PGCameraAuthorizationStatusAuthorized { - completion(true) - } else { - assertionFailure() - completion(true) - } - case let .microphone(microphoneSubject): - if AVAudioSession.sharedInstance().recordPermission() == .granted { - completion(true) - } else { - AVAudioSession.sharedInstance().requestRecordPermission({ granted in - if granted { +public final class DeviceAccess { + private static let contactsPromise = ValuePromise(nil, ignoreRepeated: true) + static var contacts: Signal { + return self.contactsPromise.get() + } + + public static func authorizeAccess(to subject: DeviceAccessSubject, presentationData: PresentationData, present: @escaping (ViewController, Any?) -> Void, openSettings: @escaping () -> Void, _ completion: @escaping (Bool) -> Void) { + switch subject { + case .camera: + let status = PGCamera.cameraAuthorizationStatus() + if status == PGCameraAuthorizationStatusNotDetermined { completion(true) - } else { - completion(false) + } else if status == PGCameraAuthorizationStatusRestricted || status == PGCameraAuthorizationStatusDenied { let text: String - switch microphoneSubject { - case .audio: - text = presentationData.strings.AccessDenied_VoiceMicrophone - case .video: - text = presentationData.strings.AccessDenied_VideoMicrophone + if status == PGCameraAuthorizationStatusRestricted { + text = presentationData.strings.AccessDenied_CameraRestricted + } else { + text = presentationData.strings.AccessDenied_Camera } - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { + completion(false) + present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) - } - }) - } - case let .mediaLibrary(mediaLibrarySubject): - let continueWithValue: (Bool) -> Void = { value in - Queue.mainQueue().async { - if value { + } else if status == PGCameraAuthorizationStatusAuthorized { completion(true) } else { - completion(false) - let text: String - switch mediaLibrarySubject { - case .send: - text = presentationData.strings.AccessDenied_PhotosAndVideos - case .save: - text = presentationData.strings.AccessDenied_SaveMedia - } - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { - openSettings() - })]), nil) + assertionFailure() + completion(true) } - } - } - if let value = cachedMediaLibraryAccessStatus.with({ $0 }) { - continueWithValue(value) - } else { - PHPhotoLibrary.requestAuthorization({ status in - let value: Bool + case let .microphone(microphoneSubject): + if AVAudioSession.sharedInstance().recordPermission() == .granted { + completion(true) + } else { + AVAudioSession.sharedInstance().requestRecordPermission({ granted in + if granted { + completion(true) + } else { + completion(false) + let text: String + switch microphoneSubject { + case .audio: + text = presentationData.strings.AccessDenied_VoiceMicrophone + case .video: + text = presentationData.strings.AccessDenied_VideoMicrophone + } + present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + openSettings() + })]), nil) + } + }) + } + case let .mediaLibrary(mediaLibrarySubject): + let continueWithValue: (Bool) -> Void = { value in + Queue.mainQueue().async { + if value { + completion(true) + } else { + completion(false) + let text: String + switch mediaLibrarySubject { + case .send: + text = presentationData.strings.AccessDenied_PhotosAndVideos + case .save: + text = presentationData.strings.AccessDenied_SaveMedia + } + present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + openSettings() + })]), nil) + } + } + } + if let value = cachedMediaLibraryAccessStatus.with({ $0 }) { + continueWithValue(value) + } else { + PHPhotoLibrary.requestAuthorization({ status in + let value: Bool + switch status { + case .restricted, .denied, .notDetermined: + value = false + case .authorized: + value = true + } + let _ = cachedMediaLibraryAccessStatus.swap(value) + continueWithValue(value) + }) + } + case let .location(locationSubject): + let status = CLLocationManager.authorizationStatus() switch status { - case .restricted, .denied, .notDetermined: - value = false - case .authorized: - value = true - } - let _ = cachedMediaLibraryAccessStatus.swap(value) - continueWithValue(value) - }) - } - case let .location(locationSubject): - let status = CLLocationManager.authorizationStatus() - switch status { - case .authorizedAlways: - completion(true) - case .authorizedWhenInUse: - switch locationSubject { - case .send, .tracking: + case .authorizedAlways: completion(true) - case .live: + case .authorizedWhenInUse: + switch locationSubject { + case .send, .tracking: + completion(true) + case .live: + completion(false) + let text = presentationData.strings.AccessDenied_LocationAlwaysDenied + present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + openSettings() + })]), nil) + } + case .denied, .restricted: completion(false) - let text = presentationData.strings.AccessDenied_LocationAlwaysDenied - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { + let text: String + if status == .denied { + switch locationSubject { + case .send, .live: + text = presentationData.strings.AccessDenied_LocationDenied + case .tracking: + text = presentationData.strings.AccessDenied_LocationTracking + } + } else { + text = presentationData.strings.AccessDenied_LocationDisabled + } + present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) + case .notDetermined: + completion(true) } - case .denied, .restricted: - completion(false) - let text: String - if status == .denied { - switch locationSubject { - case .send, .live: - text = presentationData.strings.AccessDenied_LocationDenied - case .tracking: - text = presentationData.strings.AccessDenied_LocationTracking + case .contacts: + let _ = (self.contactsPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { value in + if let value = value { + completion(value) + } else { + if #available(iOSApplicationExtension 9.0, *) { + switch CNContactStore.authorizationStatus(for: .contacts) { + case .notDetermined: + let store = CNContactStore() + store.requestAccess(for: .contacts, completionHandler: { authorized, _ in + self.contactsPromise.set(authorized) + completion(authorized) + }) + case .authorized: + self.contactsPromise.set(true) + completion(true) + default: + self.contactsPromise.set(false) + completion(false) + } + } else { + switch ABAddressBookGetAuthorizationStatus() { + case .notDetermined: + var error: Unmanaged? + let addressBook = ABAddressBookCreateWithOptions(nil, &error) + if let addressBook = addressBook?.takeUnretainedValue() { + ABAddressBookRequestAccessWithCompletion(addressBook, { authorized, _ in + Queue.mainQueue().async { + self.contactsPromise.set(authorized) + completion(authorized) + } + }) + } else { + self.contactsPromise.set(false) + completion(false) + } + case .authorized: + self.contactsPromise.set(true) + completion(true) + default: + self.contactsPromise.set(false) + completion(false) + } + } } - } else { - text = presentationData.strings.AccessDenied_LocationDisabled - } - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { - openSettings() - })]), nil) - case .notDetermined: - completion(true) + }) } } } diff --git a/TelegramUI/ComposeController.swift b/TelegramUI/ComposeController.swift index d8c2c0fd5d..13e0b5e897 100644 --- a/TelegramUI/ComposeController.swift +++ b/TelegramUI/ComposeController.swift @@ -93,7 +93,9 @@ public class ComposeController: ViewController { } self.contactsNode.contactListNode.openPeer = { [weak self] peer in - self?.openPeer(peerId: peer.id) + if case let .peer(peer, _) = peer { + self?.openPeer(peerId: peer.id) + } } self.contactsNode.openCreateNewGroup = { [weak self] in @@ -103,7 +105,13 @@ public class ComposeController: ViewController { strongSelf.createActionDisposable.set((controller.result |> deliverOnMainQueue).start(next: { [weak controller] peerIds in if let strongSelf = self, let controller = controller { - let createGroup = createGroupController(account: strongSelf.account, peerIds: peerIds) + let createGroup = createGroupController(account: strongSelf.account, peerIds: peerIds.compactMap({ peerId in + if case let .peer(peerId) = peerId { + return peerId + } else { + return nil + } + })) (controller.navigationController as? NavigationController)?.pushViewController(createGroup) } })) @@ -115,11 +123,11 @@ public class ComposeController: ViewController { let controller = ContactSelectionController(account: strongSelf.account, title: { $0.Compose_NewEncryptedChat }) strongSelf.createActionDisposable.set((controller.result |> take(1) - |> deliverOnMainQueue).start(next: { [weak controller] peerId in - if let strongSelf = self, let peerId = peerId { + |> deliverOnMainQueue).start(next: { [weak controller] peer in + if let strongSelf = self, let contactPeer = peer, case let .peer(peer, _) = contactPeer { controller?.dismissSearch() controller?.displayNavigationActivity = true - strongSelf.createActionDisposable.set((createSecretChat(account: strongSelf.account, peerId: peerId) |> deliverOnMainQueue).start(next: { peerId in + strongSelf.createActionDisposable.set((createSecretChat(account: strongSelf.account, peerId: peer.id) |> deliverOnMainQueue).start(next: { peerId in if let strongSelf = self, let controller = controller { controller.displayNavigationActivity = false (controller.navigationController as? NavigationController)?.replaceAllButRootController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: true) diff --git a/TelegramUI/ComposeControllerNode.swift b/TelegramUI/ComposeControllerNode.swift index 18b1c390af..53f357a189 100644 --- a/TelegramUI/ComposeControllerNode.swift +++ b/TelegramUI/ComposeControllerNode.swift @@ -98,7 +98,7 @@ final class ComposeControllerNode: ASDisplayNode { if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) if !searchDisplayController.isDeactivating { - insets.top += 20.0 + insets.top += layout.statusBarHeight ?? 0.0 } } @@ -124,9 +124,9 @@ final class ComposeControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: false, openPeer: { [weak self] peerId in - if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { - requestOpenPeerFromSearch(peerId) + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: false, categories: [.cloudContacts, .global], openPeer: { [weak self] peer in + if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch, case let .peer(peer, _) = peer { + requestOpenPeerFromSearch(peer.id) } }), cancel: { [weak self] in self?.requestDeactivateSearch?() diff --git a/TelegramUI/ContactInfoStrings.swift b/TelegramUI/ContactInfoStrings.swift new file mode 100644 index 0000000000..e794abfc8e --- /dev/null +++ b/TelegramUI/ContactInfoStrings.swift @@ -0,0 +1,33 @@ +import Foundation +import Contacts +import AddressBook + +func localizedPhoneNumberLabel(label: String, strings: PresentationStrings) -> String { + if #available(iOSApplicationExtension 9.0, *) { + return CNLabeledValue.localizedString(forLabel: label) + } else { + + } + if label == "_$!!$_" { + return "mobile" + } else if label == "_$!!$_" { + return "home" + } else { + return label + } +} + +func localizedGenericContactFieldLabel(label: String, strings: PresentationStrings) -> String { + if #available(iOSApplicationExtension 9.0, *) { + return CNLabeledValue.localizedString(forLabel: label) + } else { + + } + if label == "_$!!$_" { + return "mobile" + } else if label == "_$!!$_" { + return "home" + } else { + return label + } +} diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index 3089636458..373e58a9f6 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -9,6 +9,7 @@ private enum ContactListNodeEntryId: Hashable { case search case option(index: Int) case peerId(Int64) + case deviceContact(DeviceContactStableId) var hashValue: Int { switch self { @@ -18,6 +19,8 @@ private enum ContactListNodeEntryId: Hashable { return (index + 2).hashValue case let .peerId(peerId): return peerId.hashValue + case let .deviceContact(id): + return id.hashValue } } @@ -47,23 +50,69 @@ private enum ContactListNodeEntryId: Hashable { default: return false } + case let .deviceContact(id): + if case .deviceContact(id) = rhs { + return true + } else { + return false + } } } } private final class ContactListNodeInteraction { let activateSearch: () -> Void - let openPeer: (Peer) -> Void + let openPeer: (ContactListPeer) -> Void - init(activateSearch: @escaping () -> Void, openPeer: @escaping (Peer) -> Void) { + init(activateSearch: @escaping () -> Void, openPeer: @escaping (ContactListPeer) -> Void) { self.activateSearch = activateSearch self.openPeer = openPeer } } -private struct ContactListPeer { - let peer: Peer - let isGlobal: Bool +enum ContactListPeerId: Hashable { + case peer(PeerId) + case deviceContact(DeviceContactStableId) +} + +enum ContactListPeer: Equatable { + case peer(peer: Peer, isGlobal: Bool) + case deviceContact(DeviceContactStableId, DeviceContactBasicData) + + var id: ContactListPeerId { + switch self { + case let .peer(peer, _): + return .peer(peer.id) + case let .deviceContact(id, _): + return .deviceContact(id) + } + } + + var indexName: PeerIndexNameRepresentation { + switch self { + case let .peer(peer, _): + return peer.indexName + case let .deviceContact(_, contact): + return .personName(first: contact.firstName, last: contact.lastName, addressName: "", phoneNumber: "") + } + } + + static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool { + switch lhs { + case let .peer(lhsPeer, lhsIsGlobal): + if case let .peer(rhsPeer, rhsIsGlobal) = rhs, lhsPeer.isEqual(rhsPeer), lhsIsGlobal == rhsIsGlobal { + return true + } else { + return false + } + case let .deviceContact(id, contact): + if case .deviceContact(id, contact) = rhs { + return true + } else { + return false + } + } + } } private enum ContactListNodeEntry: Comparable, Identifiable { @@ -78,7 +127,12 @@ private enum ContactListNodeEntry: Comparable, Identifiable { case let .option(index, _, _, _): return .option(index: index) case let .peer(_, peer, _, _, _, _, _): - return .peerId(peer.peer.id.toInt64()) + switch peer { + case let .peer(peer, _): + return .peerId(peer.id.toInt64()) + case let .deviceContact(id, _): + return .deviceContact(id) + } } } @@ -92,15 +146,23 @@ private enum ContactListNodeEntry: Comparable, Identifiable { return ContactListActionItem(theme: theme, title: option.title, icon: option.icon, action: option.action) case let .peer(_, peer, presence, header, selection, theme, strings): let status: ContactsPeerItemStatus - if peer.isGlobal, let _ = peer.peer.addressName { - status = .addressName("") - } else if let presence = presence { - status = .presence(presence) - } else { - status = .none + let itemPeer: ContactsPeerItemPeer + switch peer { + case let .peer(peer, isGlobal): + if isGlobal, let _ = peer.addressName { + status = .addressName("") + } else if let presence = presence { + status = .presence(presence) + } else { + status = .none + } + itemPeer = .peer(peer: peer, chatPeer: peer) + case let .deviceContact(id, contact): + status = .none + itemPeer = .deviceContact(stableId: id, contact: contact) } - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer.peer, chatPeer: peer.peer, status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in - interaction.openPeer(peer.peer) + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: itemPeer, status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + interaction.openPeer(peer) }) } } @@ -125,10 +187,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { if lhsIndex != rhsIndex { return false } - if lhsPeer.peer.id != rhsPeer.peer.id { - return false - } - if lhsPeer.isGlobal != rhsPeer.isGlobal { + if lhsPeer != rhsPeer { return false } if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { @@ -221,35 +280,49 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer] var entries: [ContactListNodeEntry] = [] var orderedPeers: [ContactListPeer] - var headers: [PeerId: ContactListNameIndexHeader] = [:] + var headers: [ContactListPeerId: ContactListNameIndexHeader] = [:] switch presentation { case let .orderedByPresence(options): entries.append(.search(theme, strings)) orderedPeers = peers.sorted(by: { lhs, rhs in - let lhsPresence = presences[lhs.peer.id] - let rhsPresence = presences[rhs.peer.id] - if let lhsPresence = lhsPresence as? TelegramUserPresence, let rhsPresence = rhsPresence as? TelegramUserPresence { - if lhsPresence.status < rhsPresence.status { - return false - } else if lhsPresence.status > rhsPresence.status { + if case let .peer(lhsPeer, _) = lhs, case let .peer(rhsPeer, _) = rhs { + let lhsPresence = presences[lhsPeer.id] + let rhsPresence = presences[rhsPeer.id] + if let lhsPresence = lhsPresence as? TelegramUserPresence, let rhsPresence = rhsPresence as? TelegramUserPresence { + if lhsPresence.status < rhsPresence.status { + return false + } else if lhsPresence.status > rhsPresence.status { + return true + } + } else if let _ = lhsPresence { return true + } else if let _ = rhsPresence { + return false } - } else if let _ = lhsPresence { + return lhsPeer.id < rhsPeer.id + } else if case .peer = lhs { return true - } else if let _ = rhsPresence { + } else { return false } - return lhs.peer.id < rhs.peer.id }) for i in 0 ..< options.count { entries.append(.option(i, options[i], theme, strings)) } case let .natural(displaySearch, options): orderedPeers = peers.sorted(by: { lhs, rhs in - let result = lhs.peer.indexName.isLessThan(other: rhs.peer.indexName) + let result = lhs.indexName.isLessThan(other: rhs.indexName) if result == .orderedSame { - return lhs.peer.id < rhs.peer.id + if case let .peer(lhsPeer, _) = lhs, case let .peer(rhsPeer, _) = rhs { + return lhsPeer.id < rhsPeer.id + } else if case let .deviceContact(lhsId, _) = lhs, case let .deviceContact(rhsId, _) = rhs { + return lhsId < rhsId + } else if case .peer = lhs { + return true + } else { + return false + } } else { return result == .orderedAscending } @@ -257,7 +330,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer] var headerCache: [unichar: ContactListNameIndexHeader] = [:] for peer in orderedPeers { var indexHeader: unichar = 35 - switch peer.peer.indexName { + switch peer.indexName { case let .title(title, _): if let c = title.utf16.first { indexHeader = c @@ -276,7 +349,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer] header = ContactListNameIndexHeader(theme: theme, letter: indexHeader) headerCache[indexHeader] = header } - headers[peer.peer.id] = header + headers[peer.id] = header } if displaySearch { entries.append(.search(theme, strings)) @@ -290,7 +363,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer] var removeIndices: [Int] = [] for i in 0 ..< orderedPeers.count { - switch orderedPeers[i].peer.indexName { + switch orderedPeers[i].indexName { case let .title(title, _): if title.isEmpty { removeIndices.append(i) @@ -318,7 +391,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer] for i in 0 ..< orderedPeers.count { let selection: ContactsPeerItemSelection if let selectionState = selectionState { - selection = .selectable(selected: selectionState.selectedPeerIndices[orderedPeers[i].peer.id] != nil) + selection = .selectable(selected: selectionState.selectedPeerIndices[orderedPeers[i].id] != nil) } else { selection = .none } @@ -327,9 +400,13 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer] case .orderedByPresence: header = commonHeader default: - header = headers[orderedPeers[i].peer.id] + header = headers[orderedPeers[i].id] } - entries.append(.peer(i, orderedPeers[i], presences[orderedPeers[i].peer.id], header, selection, theme, strings)) + var presence: PeerPresence? + if case let .peer(peer, _) = orderedPeers[i] { + presence = presences[peer.id] + } + entries.append(.peer(i, orderedPeers[i], presence, header, selection, theme, strings)) } return entries } @@ -365,14 +442,14 @@ public struct ContactListAdditionalOption: Equatable { enum ContactListPresentation { case orderedByPresence(options: [ContactListAdditionalOption]) case natural(displaySearch: Bool, options: [ContactListAdditionalOption]) - case search(Signal) + case search(signal: Signal, searchDeviceContacts: Bool) } struct ContactListNodeGroupSelectionState: Equatable { - let selectedPeerIndices: [PeerId: Int] + let selectedPeerIndices: [ContactListPeerId: Int] let nextSelectionIndex: Int - private init(selectedPeerIndices: [PeerId: Int], nextSelectionIndex: Int) { + private init(selectedPeerIndices: [ContactListPeerId: Int], nextSelectionIndex: Int) { self.selectedPeerIndices = selectedPeerIndices self.nextSelectionIndex = nextSelectionIndex } @@ -382,7 +459,7 @@ struct ContactListNodeGroupSelectionState: Equatable { self.nextSelectionIndex = 0 } - func withToggledPeerId(_ peerId: PeerId) -> ContactListNodeGroupSelectionState { + func withToggledPeerId(_ peerId: ContactListPeerId) -> ContactListNodeGroupSelectionState { var updatedIndices = self.selectedPeerIndices if let _ = updatedIndices[peerId] { updatedIndices.removeValue(forKey: peerId) @@ -392,10 +469,6 @@ struct ContactListNodeGroupSelectionState: Equatable { return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, nextSelectionIndex: self.nextSelectionIndex + 1) } } - - static func ==(lhs: ContactListNodeGroupSelectionState, rhs: ContactListNodeGroupSelectionState) -> Bool { - return lhs.selectedPeerIndices == rhs.selectedPeerIndices && lhs.nextSelectionIndex == rhs.nextSelectionIndex - } } struct ContactListFilter: OptionSet { @@ -410,6 +483,7 @@ struct ContactListFilter: OptionSet { final class ContactListNode: ASDisplayNode { private let account: Account + private let presentation: ContactListPresentation private let filter: ContactListFilter let listNode: ListView @@ -451,7 +525,7 @@ final class ContactListNode: ASDisplayNode { } var activateSearch: (() -> Void)? - var openPeer: ((Peer) -> Void)? + var openPeer: ((ContactListPeer) -> Void)? private let previousEntries = Atomic<[ContactListNodeEntry]?>(value: nil) private let disposable = MetaDisposable() @@ -462,6 +536,7 @@ final class ContactListNode: ASDisplayNode { init(account: Account, presentation: ContactListPresentation, filter: ContactListFilter = [.excludeSelf], selectionState: ContactListNodeGroupSelectionState? = nil) { self.account = account + self.presentation = presentation self.filter = filter self.listNode = ListView() @@ -493,7 +568,7 @@ final class ContactListNode: ASDisplayNode { let selectionStateSignal = self.selectionStatePromise.get() let transition: Signal let themeAndStringsPromise = self.themeAndStringsPromise - if case let .search(query) = presentation { + if case let .search(query, searchDeviceContacts) = presentation { transition = query |> mapToSignal { query in let foundLocalContacts = account.postbox.searchContacts(query: query.lowercased()) @@ -503,11 +578,18 @@ final class ContactListNode: ASDisplayNode { |> map { ($0.0, $0.1) } |> delay(0.2, queue: Queue.concurrentDefaultQueue()) ) + let foundDeviceContacts: Signal<[DeviceContactStableId: DeviceContactBasicData], NoError> + if searchDeviceContacts { + foundDeviceContacts = account.telegramApplicationContext.contactDataManager.search(query: query) + } else { + foundDeviceContacts = .single([:]) + } - return combineLatest(foundLocalContacts, foundRemoteContacts, selectionStateSignal, themeAndStringsPromise.get()) - |> mapToQueue { localPeers, remotePeers, selectionState, themeAndStrings -> Signal in + return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, selectionStateSignal, themeAndStringsPromise.get()) + |> mapToQueue { localPeers, remotePeers, deviceContacts, selectionState, themeAndStrings -> Signal in let signal = deferred { () -> Signal in var existingPeerIds = Set() + var existingNormalizedPhoneNumbers = Set() if filter.contains(.excludeSelf) { existingPeerIds.insert(account.peerId) } @@ -515,14 +597,20 @@ final class ContactListNode: ASDisplayNode { for peer in localPeers { if !existingPeerIds.contains(peer.id) { existingPeerIds.insert(peer.id) - peers.append(ContactListPeer(peer: peer, isGlobal: false)) + peers.append(.peer(peer: peer, isGlobal: false)) + if searchDeviceContacts, let user = peer as? TelegramUser, let phone = user.phone { + existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) + } } } for peer in remotePeers.0 { if peer.peer is TelegramUser { if !existingPeerIds.contains(peer.peer.id) { existingPeerIds.insert(peer.peer.id) - peers.append(ContactListPeer(peer: peer.peer, isGlobal: true)) + peers.append(.peer(peer: peer.peer, isGlobal: true)) + if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { + existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) + } } } } @@ -530,11 +618,24 @@ final class ContactListNode: ASDisplayNode { if peer.peer is TelegramUser { if !existingPeerIds.contains(peer.peer.id) { existingPeerIds.insert(peer.peer.id) - peers.append(ContactListPeer(peer: peer.peer, isGlobal: true)) + peers.append(.peer(peer: peer.peer, isGlobal: true)) + if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { + existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) + } } } } + outer: for (stableId, contact) in deviceContacts { + inner: for phoneNumber in contact.phoneNumbers { + let normalizedNumber = DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phoneNumber.value)) + if existingNormalizedPhoneNumbers.contains(normalizedNumber) { + continue outer + } + } + peers.append(.deviceContact(stableId, contact)) + } + let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: [:], presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1) let previous = previousEntries.swap(entries) return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: false)) @@ -551,7 +652,7 @@ final class ContactListNode: ASDisplayNode { transition = (combineLatest(self.contactPeersViewPromise.get(), selectionStateSignal, themeAndStringsPromise.get()) |> mapToQueue { view, selectionState, themeAndStrings -> Signal in let signal = deferred { () -> Signal in - let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: view.peers.map({ ContactListPeer(peer: $0, isGlobal: false) }), presences: view.peerPresences, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1) + let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: view.peers.map({ ContactListPeer.peer(peer: $0, isGlobal: false) }), presences: view.peerPresences, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1) let previous = previousEntries.swap(entries) let animated: Bool if let previous = previous { @@ -589,6 +690,13 @@ final class ContactListNode: ASDisplayNode { } }) + self.listNode.didEndScrolling = { [weak self] in + guard let strongSelf = self else { + return + } + fixSearchableListNodeScrolling(strongSelf.listNode) + } + self.enableUpdates = true } @@ -662,7 +770,9 @@ final class ContactListNode: ASDisplayNode { options.insert(.Synchronous) options.insert(.LowLatency) } else if transition.animated { - options.insert(.AnimateCrossfade) + if case .orderedByPresence = self.presentation { + options.insert(.AnimateCrossfade) + } } self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self { diff --git a/TelegramUI/ContactMultiselectionController.swift b/TelegramUI/ContactMultiselectionController.swift index 748d2e7b4d..0f54844652 100644 --- a/TelegramUI/ContactMultiselectionController.swift +++ b/TelegramUI/ContactMultiselectionController.swift @@ -5,12 +5,12 @@ import Postbox import SwiftSignalKit import TelegramCore -public enum ContactMultiselectionControllerMode { +enum ContactMultiselectionControllerMode { case groupCreation case peerSelection } -public class ContactMultiselectionController: ViewController { +class ContactMultiselectionController: ViewController { private let account: Account private let mode: ContactMultiselectionControllerMode @@ -25,12 +25,12 @@ public class ContactMultiselectionController: ViewController { private var _ready = Promise() private var _limitsReady = Promise() private var _listReady = Promise() - override public var ready: Promise { + override var ready: Promise { return self._ready } - private let _result = Promise<[PeerId]>() - public var result: Signal<[PeerId], NoError> { + private let _result = Promise<[ContactListPeerId]>() + var result: Signal<[ContactListPeerId], NoError> { return self._result.get() } @@ -44,7 +44,7 @@ public class ContactMultiselectionController: ViewController { private var limitsConfiguration: LimitsConfiguration? private var limitsConfigurationDisposable: Disposable? - public init(account: Account, mode: ContactMultiselectionControllerMode) { + init(account: Account, mode: ContactMultiselectionControllerMode) { self.account = account self.mode = mode @@ -92,7 +92,7 @@ public class ContactMultiselectionController: ViewController { self._ready.set(combineLatest(self._listReady.get(), self._limitsReady.get()) |> map { $0 && $1 }) } - required public init(coder aDecoder: NSCoder) { + required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -127,7 +127,7 @@ public class ContactMultiselectionController: ViewController { } } - override public func loadDisplayNode() { + override func loadDisplayNode() { self.displayNode = ContactMultiselectionControllerNode(account: self.account) self._listReady.set(self.contactsNode.contactListNode.ready) @@ -136,7 +136,7 @@ public class ContactMultiselectionController: ViewController { } self.contactsNode.openPeer = { [weak self] peer in - if let strongSelf = self { + if let strongSelf = self, case let .peer(peer, _) = peer { var updatedCount: Int? var addedToken: EditableTokenListToken? var removedTokenId: AnyHashable? @@ -147,13 +147,13 @@ public class ContactMultiselectionController: ViewController { var selectionState: ContactListNodeGroupSelectionState? strongSelf.contactsNode.contactListNode.updateSelectionState { state in if let state = state { - var updatedState = state.withToggledPeerId(peer.id) - if updatedState.selectedPeerIndices[peer.id] == nil { + var updatedState = state.withToggledPeerId(.peer(peer.id)) + if updatedState.selectedPeerIndices[.peer(peer.id)] == nil { removedTokenId = peer.id } else { if updatedState.selectedPeerIndices.count >= maxRegularCount { displayCountAlert = true - updatedState = updatedState.withToggledPeerId(peer.id) + updatedState = updatedState.withToggledPeerId(.peer(peer.id)) } else { addedToken = EditableTokenListToken(id: peer.id, title: peer.displayTitle) } @@ -207,7 +207,9 @@ public class ContactMultiselectionController: ViewController { if let state = state { let updatedState = state.withToggledPeerId(peerId) if updatedState.selectedPeerIndices[peerId] == nil { - removedTokenId = peerId + if case let .peer(peerId) = peerId { + removedTokenId = peerId + } } updatedCount = updatedState.selectedPeerIndices.count selectionState = updatedState @@ -245,13 +247,13 @@ public class ContactMultiselectionController: ViewController { self.displayNodeDidLoad() } - override public func viewWillAppear(_ animated: Bool) { + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.contactsNode.contactListNode.enableUpdates = true } - override public func viewDidAppear(_ animated: Bool) { + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { @@ -266,13 +268,13 @@ public class ContactMultiselectionController: ViewController { self.contactsNode.animateOut(completion: completion) } - override public func viewDidDisappear(_ animated: Bool) { + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.contactsNode.contactListNode.enableUpdates = false } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) @@ -283,7 +285,7 @@ public class ContactMultiselectionController: ViewController { } @objc func rightNavigationButtonPressed() { - var peerIds: [PeerId] = [] + var peerIds: [ContactListPeerId] = [] self.contactsNode.contactListNode.updateSelectionState { state in if let state = state { peerIds = Array(state.selectedPeerIndices.keys) diff --git a/TelegramUI/ContactMultiselectionControllerNode.swift b/TelegramUI/ContactMultiselectionControllerNode.swift index b9bf828fbe..02a3ec8c9a 100644 --- a/TelegramUI/ContactMultiselectionControllerNode.swift +++ b/TelegramUI/ContactMultiselectionControllerNode.swift @@ -32,9 +32,9 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { private var containerLayout: (ContainerViewLayout, CGFloat)? var requestDeactivateSearch: (() -> Void)? - var requestOpenPeerFromSearch: ((PeerId) -> Void)? - var openPeer: ((Peer) -> Void)? - var removeSelectedPeer: ((PeerId) -> Void)? + var requestOpenPeerFromSearch: ((ContactListPeerId) -> Void)? + var openPeer: ((ContactListPeer) -> Void)? + var removeSelectedPeer: ((ContactListPeerId) -> Void)? var editableTokens: [EditableTokenListToken] = [] @@ -70,7 +70,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { let searchText = ValuePromise() self.tokenListNode.deleteToken = { [weak self] id in - self?.removeSelectedPeer?(id as! PeerId) + self?.removeSelectedPeer?(ContactListPeerId.peer(id as! PeerId)) } self.tokenListNode.textUpdated = { [weak self] text in @@ -88,7 +88,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { selectionState = state return state } - let searchResultsNode = ContactListNode(account: account, presentation: ContactListPresentation.search(searchText.get()), selectionState: selectionState) + let searchResultsNode = ContactListNode(account: account, presentation: .search(signal: searchText.get(), searchDeviceContacts: false), selectionState: selectionState) searchResultsNode.openPeer = { peer in self?.tokenListNode.setText("") self?.openPeer?(peer) diff --git a/TelegramUI/ContactSelectionController.swift b/TelegramUI/ContactSelectionController.swift index a8f3482598..14f43a1ae5 100644 --- a/TelegramUI/ContactSelectionController.swift +++ b/TelegramUI/ContactSelectionController.swift @@ -5,7 +5,7 @@ import Postbox import SwiftSignalKit import TelegramCore -public class ContactSelectionController: ViewController { +class ContactSelectionController: ViewController { private let account: Account private var contactsNode: ContactSelectionControllerNode { @@ -15,18 +15,19 @@ public class ContactSelectionController: ViewController { private let index: PeerNameIndex = .lastNameFirst private let titleProducer: (PresentationStrings) -> String private let options: [ContactListAdditionalOption] + private let displayDeviceContacts: Bool private var _ready = Promise() - override public var ready: Promise { + override var ready: Promise { return self._ready } - private let _result = Promise() - var result: Signal { + private let _result = Promise() + var result: Signal { return self._result.get() } - private let confirmation: (PeerId) -> Signal + private let confirmation: (ContactListPeer) -> Signal private let createActionDisposable = MetaDisposable() private let confirmationDisposable = MetaDisposable() @@ -34,7 +35,7 @@ public class ContactSelectionController: ViewController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - public var displayNavigationActivity: Bool = false { + var displayNavigationActivity: Bool = false { didSet { if self.displayNavigationActivity != oldValue { if self.displayNavigationActivity { @@ -46,10 +47,11 @@ public class ContactSelectionController: ViewController { } } - public init(account: Account, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], confirmation: @escaping (PeerId) -> Signal = { _ in .single(true) }) { + init(account: Account, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], displayDeviceContacts: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal = { _ in .single(true) }) { self.account = account self.titleProducer = title self.options = options + self.displayDeviceContacts = displayDeviceContacts self.confirmation = confirmation self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -83,7 +85,7 @@ public class ContactSelectionController: ViewController { }) } - required public init(coder aDecoder: NSCoder) { + required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -113,8 +115,8 @@ public class ContactSelectionController: ViewController { } } - override public func loadDisplayNode() { - self.displayNode = ContactSelectionControllerNode(account: self.account, options: self.options) + override func loadDisplayNode() { + self.displayNode = ContactSelectionControllerNode(account: self.account, options: self.options, displayDeviceContacts: self.displayDeviceContacts) self._ready.set(self.contactsNode.contactListNode.ready) self.contactsNode.navigationBar = self.navigationBar @@ -123,8 +125,8 @@ public class ContactSelectionController: ViewController { self?.deactivateSearch() } - self.contactsNode.requestOpenPeerFromSearch = { [weak self] peerId in - self?.openPeer(peerId: peerId) + self.contactsNode.requestOpenPeerFromSearch = { [weak self] peer in + self?.openPeer(peer: peer) } self.contactsNode.contactListNode.activateSearch = { [weak self] in @@ -132,7 +134,7 @@ public class ContactSelectionController: ViewController { } self.contactsNode.contactListNode.openPeer = { [weak self] peer in - self?.openPeer(peerId: peer.id) + self?.openPeer(peer: peer) } self.contactsNode.dismiss = { [weak self] in @@ -142,7 +144,7 @@ public class ContactSelectionController: ViewController { self.displayNodeDidLoad() } - override public func viewWillAppear(_ animated: Bool) { + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments { @@ -157,7 +159,7 @@ public class ContactSelectionController: ViewController { self.contactsNode.contactListNode.enableUpdates = true } - override public func viewDidAppear(_ animated: Bool) { + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments { @@ -170,13 +172,13 @@ public class ContactSelectionController: ViewController { } } - override public func viewDidDisappear(_ animated: Bool) { + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.contactsNode.contactListNode.enableUpdates = false } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) @@ -199,12 +201,12 @@ public class ContactSelectionController: ViewController { } } - private func openPeer(peerId: PeerId) { + private func openPeer(peer: ContactListPeer) { self.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) - self.confirmationDisposable.set((self.confirmation(peerId) |> deliverOnMainQueue).start(next: { [weak self] value in + self.confirmationDisposable.set((self.confirmation(peer) |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { if value { - strongSelf._result.set(.single(peerId)) + strongSelf._result.set(.single(peer)) strongSelf.dismiss() } } @@ -222,7 +224,7 @@ public class ContactSelectionController: ViewController { } } - public func dismissSearch() { + func dismissSearch() { self.deactivateSearch() } } diff --git a/TelegramUI/ContactSelectionControllerNode.swift b/TelegramUI/ContactSelectionControllerNode.swift index 38eddc6b56..d1944c264b 100644 --- a/TelegramUI/ContactSelectionControllerNode.swift +++ b/TelegramUI/ContactSelectionControllerNode.swift @@ -6,6 +6,8 @@ import TelegramCore import SwiftSignalKit final class ContactSelectionControllerNode: ASDisplayNode { + let displayDeviceContacts: Bool + let contactListNode: ContactListNode private let account: Account @@ -16,14 +18,15 @@ final class ContactSelectionControllerNode: ASDisplayNode { var navigationBar: NavigationBar? var requestDeactivateSearch: (() -> Void)? - var requestOpenPeerFromSearch: ((PeerId) -> Void)? + var requestOpenPeerFromSearch: ((ContactListPeer) -> Void)? var dismiss: (() -> Void)? var presentationData: PresentationData var presentationDataDisposable: Disposable? - init(account: Account, options: [ContactListAdditionalOption]) { + init(account: Account, options: [ContactListAdditionalOption], displayDeviceContacts: Bool) { self.account = account + self.displayDeviceContacts = displayDeviceContacts self.contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: true, options: options)) @@ -69,7 +72,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) if !searchDisplayController.isDeactivating { - insets.top += 20.0 + insets.top += layout.statusBarHeight ?? 0.0 } } @@ -99,10 +102,14 @@ final class ContactSelectionControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: false, openPeer: { [weak self] peerId in - if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { - requestOpenPeerFromSearch(peerId) - } + var categories: ContactsSearchCategories = [.cloudContacts] + if self.displayDeviceContacts { + categories.insert(.deviceContacts) + } else { + categories.insert(.global) + } + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: false, categories: categories, openPeer: { [weak self] peer in + self?.requestOpenPeerFromSearch?(peer) }), cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() diff --git a/TelegramUI/ContactsController.swift b/TelegramUI/ContactsController.swift index 2bdc349437..072d4896f0 100644 --- a/TelegramUI/ContactsController.swift +++ b/TelegramUI/ContactsController.swift @@ -21,6 +21,7 @@ public class ContactsController: ViewController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + private var contactsAccessDisposable: Disposable? public init(account: Account) { self.account = account @@ -66,6 +67,7 @@ public class ContactsController: ViewController { deinit { self.presentationDataDisposable?.dispose() + self.contactsAccessDisposable?.dispose() } private func updateThemeAndStrings() { @@ -74,10 +76,15 @@ public class ContactsController: ViewController { self.title = self.presentationData.strings.Contacts_Title self.tabBarItem.title = self.presentationData.strings.Contacts_Title self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + if self.navigationItem.rightBarButtonItem != nil { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationAddIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.addPressed)) + } } override public func loadDisplayNode() { - self.displayNode = ContactsControllerNode(account: self.account) + self.displayNode = ContactsControllerNode(account: self.account, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }) self._ready.set(self.contactsNode.contactListNode.ready) self.contactsNode.navigationBar = self.navigationBar @@ -86,10 +93,16 @@ public class ContactsController: ViewController { self?.deactivateSearch() } - self.contactsNode.requestOpenPeerFromSearch = { [weak self] peerId in - if let strongSelf = self { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId))) - } + self.contactsNode.requestOpenPeerFromSearch = { [weak self] peer in + self?.contactsNode.contactListNode.openPeer?(peer) + /*if let strongSelf = self { + switch peer { + case let .peer(peer, _): + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peer.id))) + case let .deviceContact(stableId, _): + break + } + }*/ } self.contactsNode.contactListNode.activateSearch = { [weak self] in @@ -99,7 +112,19 @@ public class ContactsController: ViewController { self.contactsNode.contactListNode.openPeer = { [weak self] peer in if let strongSelf = self { strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peer.id))) + switch peer { + case let .peer(peer, _): + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, chatLocation: .peer(peer.id))) + case let .deviceContact(id, _): + let _ = (strongSelf.account.telegramApplicationContext.contactDataManager.extendedData(stableId: id) + |> take(1) + |> deliverOnMainQueue).start(next: { value in + guard let strongSelf = self, let value = value else { + return + } + (strongSelf.navigationController as? NavigationController)?.pushViewController(deviceContactInfoController(account: strongSelf.account, subject: .vcard(nil, id, value))) + }) + } } } @@ -148,6 +173,21 @@ public class ContactsController: ViewController { } @objc func addPressed() { - (self.navigationController as? NavigationController)?.pushViewController(createContactController(account: self.account)) + let _ = (DeviceAccess.contacts + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + + if let value = value, value { + (strongSelf.navigationController as? NavigationController)?.pushViewController(createContactController(account: strongSelf.account)) + } else { + let presentationData = strongSelf.presentationData + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + self?.account.telegramApplicationContext.applicationBindings.openSettings() + })]), in: .window(.root)) + } + }) } } diff --git a/TelegramUI/ContactsControllerNode.swift b/TelegramUI/ContactsControllerNode.swift index ca27ecbea9..187586d523 100644 --- a/TelegramUI/ContactsControllerNode.swift +++ b/TelegramUI/ContactsControllerNode.swift @@ -16,13 +16,13 @@ final class ContactsControllerNode: ASDisplayNode { var navigationBar: NavigationBar? var requestDeactivateSearch: (() -> Void)? - var requestOpenPeerFromSearch: ((PeerId) -> Void)? + var requestOpenPeerFromSearch: ((ContactListPeer) -> Void)? var openInvite: (() -> Void)? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - init(account: Account) { + init(account: Account, present: @escaping (ViewController, Any?) -> Void) { self.account = account self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -57,7 +57,22 @@ final class ContactsControllerNode: ASDisplayNode { }) inviteImpl = { [weak self] in - self?.openInvite?() + let _ = (DeviceAccess.contacts + |> take(1) + |> deliverOnMainQueue).start(next: { value in + guard let strongSelf = self else { + return + } + + if let value = value, value { + strongSelf.openInvite?() + } else { + let presentationData = strongSelf.presentationData + present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + self?.account.telegramApplicationContext.applicationBindings.openSettings() + })]), nil) + } + }) } } @@ -80,7 +95,7 @@ final class ContactsControllerNode: ASDisplayNode { if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) if !searchDisplayController.isDeactivating { - insets.top += 20.0 + insets.top += layout.statusBarHeight ?? 0.0 } } @@ -106,9 +121,9 @@ final class ContactsControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: false, openPeer: { [weak self] peerId in + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: false, categories: [.cloudContacts, .global, .deviceContacts], openPeer: { [weak self] peer in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { - requestOpenPeerFromSearch(peerId) + requestOpenPeerFromSearch(peer) } }), cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index 62e2516355..3ef574e218 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -9,6 +9,7 @@ import TelegramCore private let titleFont = Font.regular(17.0) private let titleBoldFont = Font.medium(17.0) private let statusFont = Font.regular(13.0) +private let badgeFont = Font.regular(14.0) private let selectedImage = UIImage(bundleImageName: "Contact List/SelectionChecked")?.precomposed() private let selectableImage = UIImage(bundleImageName: "Contact List/SelectionUnchecked")?.precomposed() @@ -66,18 +67,56 @@ enum ContactsPeerItemPeerMode { case peer } +enum ContactsPeerItemBadgeType { + case active + case inactive +} + +struct ContactsPeerItemBadge { + let count: Int32 + let type: ContactsPeerItemBadgeType +} + +enum ContactsPeerItemPeer: Equatable { + case peer(peer: Peer?, chatPeer: Peer?) + case deviceContact(stableId: DeviceContactStableId, contact: DeviceContactBasicData) + + static func ==(lhs: ContactsPeerItemPeer, rhs: ContactsPeerItemPeer) -> Bool { + switch lhs { + case let .peer(lhsPeer, lhsChatPeer): + if case let .peer(rhsPeer, rhsChatPeer) = rhs { + if !arePeersEqual(lhsPeer, rhsPeer) { + return false + } + if !arePeersEqual(lhsChatPeer, rhsChatPeer) { + return false + } + return true + } else { + return false + } + case let .deviceContact(stableId, contact): + if case .deviceContact(stableId, contact) = rhs { + return true + } else { + return false + } + } + } +} + class ContactsPeerItem: ListViewItem { let theme: PresentationTheme let strings: PresentationStrings let account: Account let peerMode: ContactsPeerItemPeerMode - let peer: Peer? - let chatPeer: Peer? + let peer: ContactsPeerItemPeer let status: ContactsPeerItemStatus + let badge: ContactsPeerItemBadge? let enabled: Bool let selection: ContactsPeerItemSelection let editing: ContactsPeerItemEditing - let action: (Peer) -> Void + let action: (ContactsPeerItemPeer) -> Void let setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? let deletePeer: ((PeerId) -> Void)? @@ -87,14 +126,14 @@ class ContactsPeerItem: ListViewItem { let header: ListViewItemHeader? - init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peerMode: ContactsPeerItemPeerMode, peer: Peer?, chatPeer: Peer?, status: ContactsPeerItemStatus, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (Peer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil) { self.theme = theme self.strings = strings self.account = account self.peerMode = peerMode self.peer = peer - self.chatPeer = chatPeer self.status = status + self.badge = badge self.enabled = enabled self.selection = selection self.editing = editing @@ -107,29 +146,47 @@ class ContactsPeerItem: ListViewItem { if let index = index { var letter: String = "#" - if let user = peer as? TelegramUser { - switch index { - case .firstNameFirst: - if let firstName = user.firstName, !firstName.isEmpty { - letter = String(firstName.prefix(1)).uppercased() - } else if let lastName = user.lastName, !lastName.isEmpty { - letter = String(lastName.prefix(1)).uppercased() + switch peer { + case let .peer(peer, _): + if let user = peer as? TelegramUser { + switch index { + case .firstNameFirst: + if let firstName = user.firstName, !firstName.isEmpty { + letter = String(firstName.prefix(1)).uppercased() + } else if let lastName = user.lastName, !lastName.isEmpty { + letter = String(lastName.prefix(1)).uppercased() + } + case .lastNameFirst: + if let lastName = user.lastName, !lastName.isEmpty { + letter = String(lastName.prefix(1)).uppercased() + } else if let firstName = user.firstName, !firstName.isEmpty { + letter = String(firstName.prefix(1)).uppercased() + } } - case .lastNameFirst: - if let lastName = user.lastName, !lastName.isEmpty { - letter = String(lastName.prefix(1)).uppercased() - } else if let firstName = user.firstName, !firstName.isEmpty { - letter = String(firstName.prefix(1)).uppercased() + } else if let group = peer as? TelegramGroup { + if !group.title.isEmpty { + letter = String(group.title.prefix(1)).uppercased() } - } - } else if let group = peer as? TelegramGroup { - if !group.title.isEmpty { - letter = String(group.title.prefix(1)).uppercased() - } - } else if let channel = peer as? TelegramChannel { - if !channel.title.isEmpty { - letter = String(channel.title.prefix(1)).uppercased() - } + } else if let channel = peer as? TelegramChannel { + if !channel.title.isEmpty { + letter = String(channel.title.prefix(1)).uppercased() + } + } + case let .deviceContact(_, contact): + switch index { + case .firstNameFirst: + if !contact.firstName.isEmpty { + letter = String(contact.firstName.prefix(1)).uppercased() + } else if !contact.lastName.isEmpty { + letter = String(contact.lastName.prefix(1)).uppercased() + } + case .lastNameFirst: + if !contact.lastName.isEmpty { + letter = String(contact.lastName.prefix(1)).uppercased() + } else if !contact.firstName.isEmpty { + letter = String(contact.firstName.prefix(1)).uppercased() + } + } } self.headerAccessoryItem = ContactsSectionHeaderAccessoryItem(sectionHeader: .letter(letter), theme: theme) } else { @@ -174,9 +231,7 @@ class ContactsPeerItem: ListViewItem { func selected(listView: ListView) { listView.clearHighlightAnimated(true) - if let peer = self.peer { - self.action(peer) - } + self.action(self.peer) } static func mergeType(item: ContactsPeerItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) { @@ -223,14 +278,25 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private let titleNode: TextNode private var verificationIconNode: ASImageNode? private let statusNode: TextNode + private var badgeBackgroundNode: ASImageNode? + private var badgeTextNode: TextNode? private var selectionNode: CheckNode? private var avatarState: (Account, Peer?)? private var peerPresenceManager: PeerPresenceStatusManager? private var layoutParams: (ContactsPeerItem, ListViewItemLayoutParams, Bool, Bool, Bool)? - var peer: Peer? { - return self.layoutParams?.0.peer + var chatPeer: Peer? { + if let peer = self.layoutParams?.0.peer { + switch peer { + case let .peer(peer, chatPeer): + return chatPeer ?? peer + case .deviceContact: + return nil + } + } else { + return nil + } } private var item: ContactsPeerItem? { return self.layoutParams?.0 @@ -311,6 +377,8 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let currentSelectionNode = self.selectionNode + let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) + let currentItem = self.layoutParams?.0 return { [weak self] item, params, first, last, firstWithHeader in @@ -342,10 +410,15 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } var isVerified = false - if let peer = item.peer as? TelegramUser { - isVerified = peer.flags.contains(.isVerified) - } else if let peer = item.peer as? TelegramChannel { - isVerified = peer.flags.contains(.isVerified) + switch item.peer { + case let .peer(peer, _): + if let peer = peer as? TelegramUser { + isVerified = peer.flags.contains(.isVerified) + } else if let peer = peer as? TelegramChannel { + isVerified = peer.flags.contains(.isVerified) + } + case .deviceContact: + break } var verificationIconImage: UIImage? if isVerified { @@ -356,63 +429,110 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { var statusAttributedString: NSAttributedString? var userPresence: TelegramUserPresence? - if let peer = item.peer { - let textColor: UIColor - if let _ = item.chatPeer as? TelegramSecretChat { - textColor = item.theme.chatList.secretTitleColor - } else { - textColor = item.theme.list.itemPrimaryTextColor - } - if let user = peer as? TelegramUser { - if peer.id == item.account.peerId, case .generalSearch = item.peerMode { - titleAttributedString = NSAttributedString(string: item.strings.DialogList_SavedMessages, font: titleBoldFont, textColor: textColor) - } else if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { + switch item.peer { + case let .peer(peer, chatPeer): + if let peer = peer { + let textColor: UIColor + if let _ = chatPeer as? TelegramSecretChat { + textColor = item.theme.chatList.secretTitleColor + } else { + textColor = item.theme.list.itemPrimaryTextColor + } + if let user = peer as? TelegramUser { + if peer.id == item.account.peerId, case .generalSearch = item.peerMode { + titleAttributedString = NSAttributedString(string: item.strings.DialogList_SavedMessages, font: titleBoldFont, textColor: textColor) + } else if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { + let string = NSMutableAttributedString() + string.append(NSAttributedString(string: firstName, font: titleFont, textColor: textColor)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: textColor)) + string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: textColor)) + titleAttributedString = string + } else if let firstName = user.firstName, !firstName.isEmpty { + titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: textColor) + } else if let lastName = user.lastName, !lastName.isEmpty { + titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: textColor) + } else { + titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleBoldFont, textColor: textColor) + } + } else if let group = peer as? TelegramGroup { + titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) + } else if let channel = peer as? TelegramChannel { + titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) + } + + switch item.status { + case .none: + break + case let .presence(presence): + if let presence = presence as? TelegramUserPresence { + userPresence = presence + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, timeFormat: .regular, presence: presence, relativeTo: Int32(timestamp)) + statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor) + } + case let .addressName(suffix): + if let addressName = peer.addressName { + let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.theme.list.itemAccentColor) + if !suffix.isEmpty { + let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + let finalString = NSMutableAttributedString() + finalString.append(addressNameString) + finalString.append(suffixString) + statusAttributedString = finalString + } else { + statusAttributedString = addressNameString + } + } else if !suffix.isEmpty { + statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + } + case let .custom(text): + statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + } + } + case let .deviceContact(_, contact): + let textColor: UIColor = item.theme.list.itemPrimaryTextColor + + if !contact.firstName.isEmpty, !contact.lastName.isEmpty { let string = NSMutableAttributedString() - string.append(NSAttributedString(string: firstName, font: titleFont, textColor: textColor)) + string.append(NSAttributedString(string: contact.firstName, font: titleFont, textColor: textColor)) string.append(NSAttributedString(string: " ", font: titleFont, textColor: textColor)) - string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: textColor)) + string.append(NSAttributedString(string: contact.lastName, font: titleBoldFont, textColor: textColor)) titleAttributedString = string - } else if let firstName = user.firstName, !firstName.isEmpty { - titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: textColor) - } else if let lastName = user.lastName, !lastName.isEmpty { - titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: textColor) + } else if !contact.firstName.isEmpty { + titleAttributedString = NSAttributedString(string: contact.firstName, font: titleBoldFont, textColor: textColor) + } else if !contact.lastName.isEmpty { + titleAttributedString = NSAttributedString(string: contact.lastName, font: titleBoldFont, textColor: textColor) } else { titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleBoldFont, textColor: textColor) } - } else if let group = peer as? TelegramGroup { - titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) - } else if let channel = peer as? TelegramChannel { - titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) - } - - switch item.status { - case .none: - break - case let .presence(presence): - if let presence = presence as? TelegramUserPresence { - userPresence = presence - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, timeFormat: .regular, presence: presence, relativeTo: Int32(timestamp)) - statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor) - } - case let .addressName(suffix): - if let addressName = peer.addressName { - let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.theme.list.itemAccentColor) - if !suffix.isEmpty { - let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) - let finalString = NSMutableAttributedString() - finalString.append(addressNameString) - finalString.append(suffixString) - statusAttributedString = finalString - } else { - statusAttributedString = addressNameString - } - } else if !suffix.isEmpty { - statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) - } - case let .custom(text): - statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + + switch item.status { + case let .custom(text): + statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + default: + break + } + } + + var badgeTextLayoutAndApply: (TextNodeLayout, () -> TextNode)? + var currentBadgeBackgroundImage: UIImage? + if let badge = item.badge { + let badgeTextColor: UIColor + switch badge.type { + case .inactive: + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.theme) + badgeTextColor = item.theme.chatList.unreadBadgeInactiveTextColor + case .active: + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme) + badgeTextColor = item.theme.chatList.unreadBadgeActiveTextColor } + let badgeAttributedString = NSAttributedString(string: badge.count > 0 ? "\(badge.count)" : " ", font: badgeFont, textColor: badgeTextColor) + badgeTextLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: badgeAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 50.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + } + + var badgeSize: CGFloat = 0.0 + if let currentBadgeBackgroundImage = currentBadgeBackgroundImage, let (badgeTextLayout, _) = badgeTextLayoutAndApply { + badgeSize += max(currentBadgeBackgroundImage.size.width, badgeTextLayout.size.width + 10.0) + 5.0 } var additionalTitleInset: CGFloat = 0.0 @@ -420,9 +540,11 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { additionalTitleInset += 3.0 + verificationIconImage.size.width } + additionalTitleInset += badgeSize + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset - additionalTitleInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset - badgeSize), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 48.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) @@ -435,12 +557,27 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { return (nodeLayout, { [weak self] in if let strongSelf = self { - if let peer = item.peer { - var overrideImage: AvatarNodeImageOverride? - if peer.id == item.account.peerId, case .generalSearch = item.peerMode { - overrideImage = .savedMessagesIcon - } - strongSelf.avatarNode.setPeer(account: item.account, peer: peer, overrideImage: overrideImage) + switch item.peer { + case let .peer(peer, _): + if let peer = peer { + var overrideImage: AvatarNodeImageOverride? + if peer.id == item.account.peerId, case .generalSearch = item.peerMode { + overrideImage = .savedMessagesIcon + } + strongSelf.avatarNode.setPeer(account: item.account, peer: peer, overrideImage: overrideImage) + } + case let .deviceContact(_, contact): + let letters: [String] + if !contact.firstName.isEmpty && !contact.lastName.isEmpty { + letters = [contact.firstName.substring(to: contact.firstName.index(after: contact.firstName.startIndex)).uppercased(), contact.lastName.substring(to: contact.lastName.index(after: contact.lastName.startIndex)).uppercased()] + } else if !contact.firstName.isEmpty { + letters = [contact.firstName.substring(to: contact.firstName.index(after: contact.firstName.startIndex)).uppercased()] + } else if !contact.lastName.isEmpty { + letters = [contact.lastName.substring(to: contact.lastName.index(after: contact.lastName.startIndex)).uppercased()] + } else { + letters = [" "] + } + strongSelf.avatarNode.setCustomLetters(letters) } return (strongSelf.avatarNode.ready, { [weak strongSelf] animated in @@ -471,7 +608,11 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.statusNode.alpha = item.enabled ? 1.0 : 1.0 let _ = statusApply() - transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 25.0), size: statusLayout.size)) + let statusFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 25.0), size: statusLayout.size) + let previousStatusFrame = strongSelf.statusNode.frame + + strongSelf.statusNode.frame = statusFrame + transition.animatePositionAdditive(node: strongSelf.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) if let verificationIconImage = verificationIconImage { if strongSelf.verificationIconNode == nil { @@ -492,6 +633,50 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { verificationIconNode.removeFromSupernode() } + let badgeBackgroundWidth: CGFloat + if let currentBadgeBackgroundImage = currentBadgeBackgroundImage, let (badgeTextLayout, badgeTextApply) = badgeTextLayoutAndApply { + let badgeBackgroundNode: ASImageNode + let badgeTransition: ContainedViewLayoutTransition + if let current = strongSelf.badgeBackgroundNode { + badgeBackgroundNode = current + badgeTransition = transition + } else { + badgeBackgroundNode = ASImageNode() + badgeBackgroundNode.isLayerBacked = true + badgeBackgroundNode.displaysAsynchronously = false + badgeBackgroundNode.displayWithoutProcessing = true + strongSelf.addSubnode(badgeBackgroundNode) + strongSelf.badgeBackgroundNode = badgeBackgroundNode + badgeTransition = .immediate + } + + badgeBackgroundNode.image = currentBadgeBackgroundImage + + badgeBackgroundWidth = max(badgeTextLayout.size.width + 10.0, currentBadgeBackgroundImage.size.width) + let badgeBackgroundFrame = CGRect(x: revealOffset + params.width - params.rightInset - badgeBackgroundWidth - 6.0, y: floor((nodeLayout.contentSize.height - currentBadgeBackgroundImage.size.height) / 2.0), width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height) + let badgeTextFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.midX - badgeTextLayout.size.width / 2.0, y: badgeBackgroundFrame.minY + 2.0), size: badgeTextLayout.size) + + let badgeTextNode = badgeTextApply() + if badgeTextNode !== strongSelf.badgeTextNode { + strongSelf.badgeTextNode?.removeFromSupernode() + strongSelf.addSubnode(badgeTextNode) + strongSelf.badgeTextNode = badgeTextNode + } + + badgeTransition.updateFrame(node: badgeBackgroundNode, frame: badgeBackgroundFrame) + badgeTransition.updateFrame(node: badgeTextNode, frame: badgeTextFrame) + } else { + badgeBackgroundWidth = 0.0 + if let badgeBackgroundNode = strongSelf.badgeBackgroundNode { + badgeBackgroundNode.removeFromSupernode() + strongSelf.badgeBackgroundNode = nil + } + if let badgeTextNode = strongSelf.badgeTextNode { + badgeTextNode.removeFromSupernode() + strongSelf.badgeTextNode = badgeTextNode + } + } + if let updatedSelectionNode = updatedSelectionNode { if strongSelf.selectionNode !== updatedSelectionNode { strongSelf.selectionNode?.removeFromSupernode() @@ -538,7 +723,7 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) - if let item = self.item { + if let item = self.item, let params = self.layoutParams?.1 { var leftInset: CGFloat = 65.0 switch item.selection { @@ -557,32 +742,65 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: self.titleNode, frame: titleFrame) var statusFrame = self.statusNode.frame + let previousStatusFrame = statusFrame statusFrame.origin.x = leftInset + offset - transition.updateFrame(node: self.statusNode, frame: statusFrame) + self.statusNode.frame = statusFrame + transition.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) if let verificationIconNode = self.verificationIconNode { var iconFrame = verificationIconNode.frame iconFrame.origin.x = offset + titleFrame.maxX + 3.0 transition.updateFrame(node: verificationIconNode, frame: iconFrame) } + + if let badgeBackgroundNode = self.badgeBackgroundNode, let badgeTextNode = self.badgeTextNode { + var badgeBackgroundFrame = badgeBackgroundNode.frame + badgeBackgroundFrame.origin.x = offset + params.width - params.rightInset - badgeBackgroundFrame.width - 6.0 + var badgeTextFrame = badgeTextNode.frame + badgeTextFrame.origin.x = badgeBackgroundFrame.midX - badgeTextFrame.width / 2.0 + + transition.updateFrame(node: badgeBackgroundNode, frame: badgeBackgroundFrame) + transition.updateFrame(node: badgeTextNode, frame: badgeTextFrame) + } } } override func revealOptionsInteractivelyOpened() { - if let item = self.item, let peer = item.peer { - item.setPeerIdWithRevealedOptions?(peer.id, nil) + if let item = self.item { + switch item.peer { + case let .peer(peer, _): + if let peer = peer { + item.setPeerIdWithRevealedOptions?(peer.id, nil) + } + case .deviceContact: + break + } } } override func revealOptionsInteractivelyClosed() { - if let item = self.item, let peer = item.peer { - item.setPeerIdWithRevealedOptions?(nil, peer.id) + if let item = self.item { + switch item.peer { + case let .peer(peer, _): + if let peer = peer { + item.setPeerIdWithRevealedOptions?(nil, peer.id) + } + case .deviceContact: + break + } } } override func revealOptionSelected(_ option: ItemListRevealOption) { - if let item = self.item, let peer = item.peer { - item.deletePeer?(peer.id) + if let item = self.item { + switch item.peer { + case let .peer(peer, _): + if let peer = peer { + item.deletePeer?(peer.id) + } + case .deviceContact: + break + } } self.setRevealOptionsOpened(false, animated: true) diff --git a/TelegramUI/ContactsSearchContainerNode.swift b/TelegramUI/ContactsSearchContainerNode.swift index 34e0355e07..2f4dee82f1 100644 --- a/TelegramUI/ContactsSearchContainerNode.swift +++ b/TelegramUI/ContactsSearchContainerNode.swift @@ -5,12 +5,19 @@ import SwiftSignalKit import Postbox import TelegramCore +private enum ContactListSearchGroup { + case contacts + case global + case deviceContacts +} + private struct ContactListSearchEntry: Identifiable, Comparable { let index: Int - let peer: Peer + let peer: ContactListPeer + let group: ContactListSearchGroup let enabled: Bool - var stableId: PeerId { + var stableId: ContactListPeerId { return self.peer.id } @@ -18,7 +25,10 @@ private struct ContactListSearchEntry: Identifiable, Comparable { if lhs.index != rhs.index { return false } - if !arePeersEqual(lhs.peer, rhs.peer) { + if lhs.peer != rhs.peer { + return false + } + if lhs.group != rhs.group { return false } if lhs.enabled != rhs.enabled { @@ -31,8 +41,25 @@ private struct ContactListSearchEntry: Identifiable, Comparable { return lhs.index < rhs.index } - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, openPeer: @escaping (Peer) -> Void) -> ListViewItem { - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: .none, enabled: self.enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { peer in + func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, openPeer: @escaping (ContactListPeer) -> Void) -> ListViewItem { + let header: ListViewItemHeader + switch self.group { + case .contacts: + header = ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil) + case .global: + header = ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings, actionTitle: nil, action: nil) + case .deviceContacts: + header = ChatListSearchItemHeader(type: .deviceContacts, theme: theme, strings: strings, actionTitle: nil, action: nil) + } + let peer = self.peer + let peerItem: ContactsPeerItemPeer + switch peer { + case let .peer(peer, _): + peerItem = .peer(peer: peer, chatPeer: peer) + case let .deviceContact(stableId, contact): + peerItem = .deviceContact(stableId: stableId, contact: contact) + } + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peerItem, status: .none, enabled: self.enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in openPeer(peer) }) } @@ -45,7 +72,7 @@ struct ContactListSearchContainerTransition { let isSearching: Bool } -private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, openPeer: @escaping (Peer) -> Void) -> ContactListSearchContainerTransition { +private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, account: Account, theme: PresentationTheme, strings: PresentationStrings, openPeer: @escaping (ContactListPeer) -> Void) -> ContactListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } @@ -55,9 +82,21 @@ private func contactListSearchContainerPreparedRecentTransition(from fromEntries return ContactListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } +struct ContactsSearchCategories: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + static let cloudContacts = ContactsSearchCategories(rawValue: 1 << 0) + static let global = ContactsSearchCategories(rawValue: 1 << 1) + static let deviceContacts = ContactsSearchCategories(rawValue: 1 << 2) +} + final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { private let account: Account - private let openPeer: (PeerId) -> Void + private let openPeer: (ContactListPeer) -> Void private let dimNode: ASDisplayNode private let listNode: ListView @@ -71,7 +110,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { private var containerViewLayout: (ContainerViewLayout, CGFloat)? private var enqueuedTransitions: [ContactListSearchContainerTransition] = [] - init(account: Account, onlyWriteable: Bool, filter: ContactListFilter = [.excludeSelf], openPeer: @escaping (PeerId) -> Void) { + init(account: Account, onlyWriteable: Bool, categories: ContactsSearchCategories, filter: ContactListFilter = [.excludeSelf], openPeer: @escaping (ContactListPeer) -> Void) { self.account = account self.openPeer = openPeer @@ -98,51 +137,68 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { let themeAndStringsPromise = self.themeAndStringsPromise let searchItems = searchQuery.get() - |> mapToSignal { query -> Signal<[ContactListSearchEntry]?, NoError> in - if let query = query, !query.isEmpty { - let foundLocalContacts = account.postbox.searchContacts(query: query.lowercased()) - let foundRemoteContacts: Signal<([FoundPeer], [FoundPeer]), NoError> = - .single(([], [])) + |> mapToSignal { query -> Signal<[ContactListSearchEntry]?, NoError> in + if let query = query, !query.isEmpty { + let foundLocalContacts = account.postbox.searchContacts(query: query.lowercased()) + let foundRemoteContacts: Signal<([FoundPeer], [FoundPeer])?, NoError> + if categories.contains(.global) { + foundRemoteContacts = .single(nil) |> then( searchPeers(account: account, query: query) |> map { ($0.0, $0.1) } |> delay(0.2, queue: Queue.concurrentDefaultQueue()) ) - - return combineLatest(foundLocalContacts, foundRemoteContacts, themeAndStringsPromise.get()) - |> delay(0.1, queue: Queue.concurrentDefaultQueue()) - |> map { localPeers, remotePeers, themeAndStrings -> [ContactListSearchEntry] in - var entries: [ContactListSearchEntry] = [] - var existingPeerIds = Set() - if filter.contains(.excludeSelf) { - existingPeerIds.insert(account.peerId) + } else { + foundRemoteContacts = .single(([], [])) + } + let searchDeviceContacts = categories.contains(.deviceContacts) + let foundDeviceContacts: Signal<[DeviceContactStableId: DeviceContactBasicData]?, NoError> + if searchDeviceContacts { + foundDeviceContacts = account.telegramApplicationContext.contactDataManager.search(query: query) + |> map(Optional.init) + } else { + foundDeviceContacts = .single([:]) + } + + return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, themeAndStringsPromise.get()) + |> delay(0.1, queue: Queue.concurrentDefaultQueue()) + |> map { localPeers, remotePeers, deviceContacts, themeAndStrings -> [ContactListSearchEntry] in + var entries: [ContactListSearchEntry] = [] + var existingPeerIds = Set() + if filter.contains(.excludeSelf) { + existingPeerIds.insert(account.peerId) + } + var existingNormalizedPhoneNumbers = Set() + var index = 0 + for peer in localPeers { + if existingPeerIds.contains(peer.id) { + continue } - var index = 0 - for peer in localPeers { - if existingPeerIds.contains(peer.id) { - continue - } - existingPeerIds.insert(peer.id) + existingPeerIds.insert(peer.id) + var enabled = true + if onlyWriteable { + enabled = canSendMessagesToPeer(peer) + } + entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer, isGlobal: false), group: .contacts, enabled: enabled)) + if searchDeviceContacts, let user = peer as? TelegramUser, let phone = user.phone { + existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) + } + index += 1 + } + /*for peer in remotePeers.1 { + if !existingPeerIds.contains(peer.peer.id) { + existingPeerIds.insert(peer.peer.id) var enabled = true if onlyWriteable { - enabled = canSendMessagesToPeer(peer) + enabled = canSendMessagesToPeer(peer.peer) } - entries.append(ContactListSearchEntry(index: index, peer: peer, enabled: enabled)) + + entries.append(ContactListSearchEntry(index: index, peer: peer.peer, enabled: enabled)) index += 1 } + }*/ + if let remotePeers = remotePeers { for peer in remotePeers.1 { - if !existingPeerIds.contains(peer.peer.id) { - existingPeerIds.insert(peer.peer.id) - var enabled = true - if onlyWriteable { - enabled = canSendMessagesToPeer(peer.peer) - } - - entries.append(ContactListSearchEntry(index: index, peer: peer.peer, enabled: enabled)) - index += 1 - } - } - for peer in remotePeers.0 { if !existingPeerIds.contains(peer.peer.id) { existingPeerIds.insert(peer.peer.id) @@ -151,15 +207,31 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { enabled = canSendMessagesToPeer(peer.peer) } - entries.append(ContactListSearchEntry(index: index, peer: peer.peer, enabled: enabled)) + entries.append(ContactListSearchEntry(index: index, peer: .peer(peer: peer.peer, isGlobal: true), group: .global, enabled: enabled)) + if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { + existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) + } index += 1 } } - return entries } - } else { - return .single(nil) - } + if let _ = remotePeers, let deviceContacts = deviceContacts { + outer: for (stableId, contact) in deviceContacts { + inner: for phoneNumber in contact.phoneNumbers { + let normalizedNumber = DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phoneNumber.value)) + if existingNormalizedPhoneNumbers.contains(normalizedNumber) { + continue outer + } + } + entries.append(ContactListSearchEntry(index: index, peer: .deviceContact(stableId, contact), group: .deviceContacts, enabled: true)) + index += 1 + } + } + return entries + } + } else { + return .single(nil) + } } let previousSearchItems = Atomic<[ContactListSearchEntry]>(value: []) @@ -169,11 +241,8 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { if let strongSelf = self { let previousItems = previousSearchItems.swap(items ?? []) - let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, openPeer: { peer in - if let openPeer = self?.openPeer { - self?.listNode.clearHighlightAnimated(true) - openPeer(peer.id) - } + let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, openPeer: { peer in self?.listNode.clearHighlightAnimated(true) + self?.openPeer(peer) }) /*var listItems: [ListViewItem] = [] @@ -182,7 +251,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { case let .peer(peer, theme, strings): - listItems.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { [weak self] peer in + listItems.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { [weak self] _ in if let openPeer = self?.openPeer { self?.listNode.clearHighlightAnimated(true) openPeer(peer.id) @@ -228,7 +297,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { diff --git a/TelegramUI/CreateContactController.swift b/TelegramUI/CreateContactController.swift index 868bf056b7..2dd50de38e 100644 --- a/TelegramUI/CreateContactController.swift +++ b/TelegramUI/CreateContactController.swift @@ -162,7 +162,7 @@ private enum CreateContactEntry: ItemListNodeEntry { arguments.openLabelSelection(id, label) }, delete: { arguments.deletePhone(id) - }) + }, tag: nil) case let .addPhone(theme, title): return UserInfoEditingPhoneActionItem(theme: theme, title: title, sectionId: self.section, action: { arguments.addPhone() @@ -220,16 +220,6 @@ private struct CreateContactState: Equatable { } } -private func localizedPhoneNumberLabel(label: String, strings: PresentationStrings) -> String { - if label == "_$!!$_" { - return "mobile" - } else if label == "_$!!$_" { - return "home" - } else { - return label - } -} - private func createContactEntries(account: Account, presentationData: PresentationData, state: CreateContactState) -> [CreateContactEntry] { var entries: [CreateContactEntry] = [] diff --git a/TelegramUI/CurrencyFormat.swift b/TelegramUI/CurrencyFormat.swift index 18b6176dbd..b233f1bf4b 100644 --- a/TelegramUI/CurrencyFormat.swift +++ b/TelegramUI/CurrencyFormat.swift @@ -46,7 +46,7 @@ private func loadCurrencyFormatterEntries() -> [String: CurrencyFormatterEntry] private let currencyFormatterEntries = loadCurrencyFormatterEntries() public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String { - if let entry = currencyFormatterEntries[currency] { + if let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] { var result = "" if amount < 0 { result.append("-") diff --git a/TelegramUI/DataPrivacySettingsController.swift b/TelegramUI/DataPrivacySettingsController.swift new file mode 100644 index 0000000000..5438e4dd4f --- /dev/null +++ b/TelegramUI/DataPrivacySettingsController.swift @@ -0,0 +1,487 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class DataPrivacyControllerArguments { + let account: Account + let clearPaymentInfo: () -> Void + let updateSecretChatLinkPreviews: (Bool) -> Void + let deleteContacts: () -> Void + let updateSyncContacts: (Bool) -> Void + let updateSuggestFrequentContacts: (Bool) -> Void + let deleteCloudDrafts: () -> Void + + init(account: Account, clearPaymentInfo: @escaping () -> Void, updateSecretChatLinkPreviews: @escaping (Bool) -> Void, deleteContacts: @escaping () -> Void, updateSyncContacts: @escaping (Bool) -> Void, updateSuggestFrequentContacts: @escaping (Bool) -> Void, deleteCloudDrafts: @escaping () -> Void) { + self.account = account + self.clearPaymentInfo = clearPaymentInfo + self.updateSecretChatLinkPreviews = updateSecretChatLinkPreviews + self.deleteContacts = deleteContacts + self.updateSyncContacts = updateSyncContacts + self.updateSuggestFrequentContacts = updateSuggestFrequentContacts + self.deleteCloudDrafts = deleteCloudDrafts + } +} + +private enum PrivacyAndSecuritySection: Int32 { + case contacts + case frequentContacts + case chats + case payments + case secretChats +} + +private enum PrivacyAndSecurityEntry: ItemListNodeEntry { + case contactsHeader(PresentationTheme, String) + case deleteContacts(PresentationTheme, String, Bool) + case syncContacts(PresentationTheme, String, Bool) + case syncContactsInfo(PresentationTheme, String) + + case frequentContacts(PresentationTheme, String, Bool) + case frequentContactsInfo(PresentationTheme, String) + + case chatsHeader(PresentationTheme, String) + case deleteCloudDrafts(PresentationTheme, String, Bool) + + case paymentHeader(PresentationTheme, String) + case clearPaymentInfo(PresentationTheme, String, Bool) + case paymentInfo(PresentationTheme, String) + + case secretChatLinkPreviewsHeader(PresentationTheme, String) + case secretChatLinkPreviews(PresentationTheme, String, Bool) + case secretChatLinkPreviewsInfo(PresentationTheme, String) + + var section: ItemListSectionId { + switch self { + case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo: + return PrivacyAndSecuritySection.contacts.rawValue + case .frequentContacts, .frequentContactsInfo: + return PrivacyAndSecuritySection.frequentContacts.rawValue + case .chatsHeader, .deleteCloudDrafts: + return PrivacyAndSecuritySection.chats.rawValue + case .paymentHeader, .clearPaymentInfo, .paymentInfo: + return PrivacyAndSecuritySection.payments.rawValue + case .secretChatLinkPreviewsHeader, .secretChatLinkPreviews, .secretChatLinkPreviewsInfo: + return PrivacyAndSecuritySection.secretChats.rawValue + + } + } + + var stableId: Int32 { + switch self { + case .contactsHeader: + return 0 + case .deleteContacts: + return 1 + case .syncContacts: + return 2 + case .syncContactsInfo: + return 3 + + case .frequentContacts: + return 4 + case .frequentContactsInfo: + return 5 + + case .chatsHeader: + return 6 + case .deleteCloudDrafts: + return 7 + + case .paymentHeader: + return 8 + case .clearPaymentInfo: + return 9 + case .paymentInfo: + return 10 + + case .secretChatLinkPreviewsHeader: + return 11 + case .secretChatLinkPreviews: + return 12 + case .secretChatLinkPreviewsInfo: + return 13 + } + } + + static func ==(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool { + switch lhs { + case let .contactsHeader(lhsTheme, lhsText): + if case let .contactsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .deleteContacts(lhsTheme, lhsText, lhsEnabled): + if case let .deleteContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .syncContacts(lhsTheme, lhsText, lhsEnabled): + if case let .syncContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .syncContactsInfo(lhsTheme, lhsText): + if case let .syncContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .frequentContacts(lhsTheme, lhsText, lhsEnabled): + if case let .frequentContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .frequentContactsInfo(lhsTheme, lhsText): + if case let .frequentContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .chatsHeader(lhsTheme, lhsText): + if case let .chatsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .deleteCloudDrafts(lhsTheme, lhsText, lhsEnabled): + if case let .deleteCloudDrafts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .paymentHeader(lhsTheme, lhsText): + if case let .paymentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .clearPaymentInfo(lhsTheme, lhsText, lhsEnabled): + if case let .clearPaymentInfo(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .paymentInfo(lhsTheme, lhsText): + if case let .paymentInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .secretChatLinkPreviewsHeader(lhsTheme, lhsText): + if case let .secretChatLinkPreviewsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .secretChatLinkPreviews(lhsTheme, lhsText, lhsEnabled): + if case let .secretChatLinkPreviews(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .secretChatLinkPreviewsInfo(lhsTheme, lhsText): + if case let .secretChatLinkPreviewsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + } + } + + static func <(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: DataPrivacyControllerArguments) -> ListViewItem { + switch self { + case let .contactsHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .deleteContacts(theme, text, value): + return ItemListActionItem(theme: theme, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.deleteContacts() + }) + case let .syncContacts(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + arguments.updateSyncContacts(updatedValue) + }) + case let .syncContactsInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .frequentContacts(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: !value, sectionId: self.section, style: .blocks, updated: { updatedValue in + arguments.updateSuggestFrequentContacts(updatedValue) + }) + case let .frequentContactsInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .chatsHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .deleteCloudDrafts(theme, text, value): + return ItemListActionItem(theme: theme, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.deleteCloudDrafts() + }) + case let .paymentHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .clearPaymentInfo(theme, text, enabled): + return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.clearPaymentInfo() + }) + case let .paymentInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .secretChatLinkPreviewsHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .secretChatLinkPreviews(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + arguments.updateSecretChatLinkPreviews(updatedValue) + }) + case let .secretChatLinkPreviewsInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + } + } +} + +private struct DataPrivacyControllerState: Equatable { + var clearingPaymentInfo: Bool = false + var deletingContacts: Bool = false + var updatedSuggestFrequentContacts: Bool? = nil + var deletingCloudDrafts: Bool = false +} + +private func dataPrivacyControllerEntries(presentationData: PresentationData, state: DataPrivacyControllerState, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool) -> [PrivacyAndSecurityEntry] { + var entries: [PrivacyAndSecurityEntry] = [] + + entries.append(.contactsHeader(presentationData.theme, presentationData.strings.PrivacySettings_Contacts)) + entries.append(.deleteContacts(presentationData.theme, presentationData.strings.PrivacySettings_DeleteContacts, !state.deletingContacts)) + entries.append(.syncContacts(presentationData.theme, presentationData.strings.PrivacySettings_SyncContacts, synchronizeDeviceContacts)) + entries.append(.syncContactsInfo(presentationData.theme, presentationData.strings.PrivacySettings_SyncContactsInfo)) + + entries.append(.frequentContacts(presentationData.theme, presentationData.strings.PrivacySettings_SuggestFrequentContacts, frequentContacts)) + entries.append(.frequentContactsInfo(presentationData.theme, presentationData.strings.PrivacySettings_SuggestFrequentContactsInfo)) + + entries.append(.chatsHeader(presentationData.theme, presentationData.strings.Privacy_ChatsTitle)) + entries.append(.deleteCloudDrafts(presentationData.theme, presentationData.strings.Privacy_DeleteDrafts, !state.deletingCloudDrafts)) + entries.append(.paymentHeader(presentationData.theme, presentationData.strings.Privacy_PaymentsTitle)) + entries.append(.clearPaymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfo, !state.clearingPaymentInfo)) + entries.append(.paymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfoHelp)) + + entries.append(.secretChatLinkPreviewsHeader(presentationData.theme, presentationData.strings.PrivacySettings_SecretChats)) + entries.append(.secretChatLinkPreviews(presentationData.theme, presentationData.strings.PrivacySettings_LinkPreviews, secretChatLinkPreviews ?? true)) + entries.append(.secretChatLinkPreviewsInfo(presentationData.theme, presentationData.strings.PrivacySettings_LinkPreviewsInfo)) + + return entries +} + +public func dataPrivacyController(account: Account) -> ViewController { + let statePromise = ValuePromise(DataPrivacyControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: DataPrivacyControllerState()) + let updateState: ((DataPrivacyControllerState) -> DataPrivacyControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var pushControllerImpl: ((ViewController) -> Void)? + var pushControllerInstantImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController) -> Void)? + + let actionsDisposable = DisposableSet() + + let currentInfoDisposable = MetaDisposable() + actionsDisposable.add(currentInfoDisposable) + + let clearPaymentInfoDisposable = MetaDisposable() + actionsDisposable.add(clearPaymentInfoDisposable) + + let arguments = DataPrivacyControllerArguments(account: account, clearPaymentInfo: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(presentationTheme: presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Privacy_PaymentsClearInfo, color: .destructive, action: { + var clear = false + updateState { state in + var state = state + if !state.clearingPaymentInfo { + clear = true + state.clearingPaymentInfo = true + } + return state + } + if clear { + clearPaymentInfoDisposable.set((clearBotPaymentInfo(network: account.network) + |> deliverOnMainQueue).start(completed: { + updateState { state in + var state = state + state.clearingPaymentInfo = false + return state + } + presentControllerImpl?(OverlayStatusController(theme: account.telegramApplicationContext.currentPresentationData.with({ $0 }).theme, type: .success)) + })) + } + dismissAction() + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + presentControllerImpl?(controller) + }, updateSecretChatLinkPreviews: { value in + let _ = ApplicationSpecificNotice.setSecretChatLinkPreviews(postbox: account.postbox, value: value).start() + }, deleteContacts: { + var canBegin = false + updateState { state in + if !state.deletingContacts { + canBegin = true + } + return state + } + if canBegin { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "This will remove your contacts from the Telegram servers.", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + var begin = false + updateState { state in + var state = state + if !state.deletingContacts { + state.deletingContacts = true + begin = true + } + return state + } + + if !begin { + return + } + + let _ = updateContactSynchronizationSettingsInteractively(postbox: account.postbox, { settings in + var settings = settings + settings.synchronizeDeviceContacts = false + return settings + }) + + actionsDisposable.add((deleteAllContacts(postbox: account.postbox, network: account.network) + |> deliverOnMainQueue).start(completed: { + updateState { state in + var state = state + state.deletingContacts = false + return state + } + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.PrivacySettings_DeleteContactsSuccess, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) + })) + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})])) + } + }, updateSyncContacts: { value in + let _ = updateContactSynchronizationSettingsInteractively(postbox: account.postbox, { settings in + var settings = settings + settings.synchronizeDeviceContacts = value + return settings + }).start() + }, updateSuggestFrequentContacts: { value in + let apply: () -> Void = { + updateState { state in + var state = state + state.updatedSuggestFrequentContacts = value + return state + } + let _ = updateRecentPeersEnabled(postbox: account.postbox, network: account.network, enabled: value).start() + } + if !value { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.PrivacySettings_SuggestFrequentContactsDisableNotice, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + apply() + })])) + } else { + apply() + } + }, deleteCloudDrafts: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(presentationTheme: presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Privacy_DeleteDrafts, color: .destructive, action: { + var clear = false + updateState { state in + var state = state + if !state.deletingCloudDrafts { + clear = true + state.deletingCloudDrafts = true + } + return state + } + if clear { + clearPaymentInfoDisposable.set((clearCloudDraftsInteractively(postbox: account.postbox, network: account.network) + |> deliverOnMainQueue).start(completed: { + updateState { state in + var state = state + state.deletingCloudDrafts = false + return state + } + presentControllerImpl?(OverlayStatusController(theme: account.telegramApplicationContext.currentPresentationData.with({ $0 }).theme, type: .success)) + })) + } + dismissAction() + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + presentControllerImpl?(controller) + }) + + let previousState = Atomic(value: nil) + + let preferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.contactSynchronizationSettings])) + + actionsDisposable.add(managedUpdatedRecentPeers(postbox: account.postbox, network: account.network).start()) + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, account.postbox.combinedView(keys: [.noticeEntry(ApplicationSpecificNotice.secretChatLinkPreviewsKey()), preferencesKey]), recentPeers(account: account)) + |> map { presentationData, state, combined, recentPeers -> (ItemListControllerState, (ItemListNodeState, PrivacyAndSecurityEntry.ItemGenerationArguments)) in + let secretChatLinkPreviews = (combined.views[.noticeEntry(ApplicationSpecificNotice.secretChatLinkPreviewsKey())] as? NoticeEntryView)?.value.flatMap({ ApplicationSpecificNotice.getSecretChatLinkPreviews($0) }) + + let synchronizeDeviceContacts: Bool = ((combined.views[preferencesKey] as? PreferencesView)?.values[ApplicationSpecificPreferencesKeys.contactSynchronizationSettings] as? ContactSynchronizationSettings)?.synchronizeDeviceContacts ?? true + + let suggestRecentPeers: Bool + if let updatedSuggestFrequentContacts = state.updatedSuggestFrequentContacts { + suggestRecentPeers = updatedSuggestFrequentContacts + } else { + switch recentPeers { + case .peers: + suggestRecentPeers = true + case .disabled: + suggestRecentPeers = false + } + } + + let rightNavigationButton: ItemListNavigationButton? = nil + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PrivateDataSettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + + let previousStateValue = previousState.swap(state) + let animateChanges = false + + let listState = ItemListNodeState(entries: dataPrivacyControllerEntries(presentationData: presentationData, state: state, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts, frequentContacts: suggestRecentPeers), style: .blocks, animateChanges: animateChanges) + + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(account: account, state: signal) + pushControllerImpl = { [weak controller] c in + (controller?.navigationController as? NavigationController)?.pushViewController(c) + } + pushControllerInstantImpl = { [weak controller] c in + (controller?.navigationController as? NavigationController)?.pushViewController(c, animated: false) + } + presentControllerImpl = { [weak controller] c in + controller?.present(c, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + + return controller +} diff --git a/TelegramUI/DateFormat.swift b/TelegramUI/DateFormat.swift index 4c0875e313..542bf3bea8 100644 --- a/TelegramUI/DateFormat.swift +++ b/TelegramUI/DateFormat.swift @@ -80,6 +80,15 @@ func stringForDate(timestamp: Int32, strings: PresentationStrings) -> String { return formatter.string(from: Date(timeIntervalSince1970: Double(timestamp))) } +func stringForDateWithoutYear(date: Date, strings: PresentationStrings) -> String { + let formatter = DateFormatter() + formatter.timeStyle = .none + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.locale = localeWithStrings(strings) + formatter.setLocalizedDateFormatFromTemplate("MMMMd") + return formatter.string(from: date) +} + func roundDateToDays(_ timestamp: Int32) -> Int32 { let calendar = Calendar(identifier: .gregorian) var components = calendar.dateComponents(Set([.era, .year, .month, .day]), from: Date(timeIntervalSince1970: Double(timestamp))) diff --git a/TelegramUI/DebugController.swift b/TelegramUI/DebugController.swift index 396951f51d..ef921dfb05 100644 --- a/TelegramUI/DebugController.swift +++ b/TelegramUI/DebugController.swift @@ -3,14 +3,15 @@ import Display import SwiftSignalKit import Postbox import TelegramCore +import MtProtoKitDynamic private final class DebugControllerArguments { let account: Account let accountManager: AccountManager - let presentController: (ViewController, ViewControllerPresentationArguments) -> Void + let presentController: (ViewController, ViewControllerPresentationArguments?) -> Void let pushController: (ViewController) -> Void - init(account: Account, accountManager: AccountManager, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, pushController: @escaping (ViewController) -> Void) { + init(account: Account, accountManager: AccountManager, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping (ViewController) -> Void) { self.account = account self.accountManager = accountManager self.presentController = presentController @@ -34,6 +35,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case redactSensitiveData(PresentationTheme, Bool) case enableRaiseToSpeak(PresentationTheme, Bool) case keepChatNavigationStack(PresentationTheme, Bool) + case testHashing(PresentationTheme) var section: ItemListSectionId { switch self { @@ -47,6 +49,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack: return DebugControllerSection.experiments.rawValue + case .testHashing: + return DebugControllerSection.experiments.rawValue } } @@ -68,6 +72,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 6 case .keepChatNavigationStack: return 7 + case .testHashing: + return 8 } } @@ -121,6 +127,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { } else { return false } + case let .testHashing(lhsTheme): + if case let .testHashing(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } } } @@ -141,8 +153,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { let messages = logs.map { (name, path) -> EnqueueMessage in let id = arc4random64() - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), reference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) - return .message(text: "", attributes: [], media: file, replyToMessageId: nil, localGroupingKey: nil) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) + return .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil) } let _ = enqueueMessages(account: arguments.account, peerId: peerId, messages: messages).start() } @@ -190,6 +202,24 @@ private enum DebugControllerEntry: ItemListNodeEntry { return settings }).start() }) + case let .testHashing(theme): + return ItemListActionItem(theme: theme, title: "Test Hashing", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + let passwordData = "case let .testHashing(theme):".data(using: .utf8)! + let salt = Data(count: 16) + + var startTime = CFAbsoluteTimeGetCurrent() + let result1 = MTPBKDF2(passwordData, salt, 100000) + let duration1 = CFAbsoluteTimeGetCurrent() - startTime + + startTime = CFAbsoluteTimeGetCurrent() + let result2 = MTArgon2(passwordData, salt, 5) + let duration2 = CFAbsoluteTimeGetCurrent() - startTime + if result1 == nil || result2 == nil { + arguments.presentController(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: theme), title: nil, text: "Error", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), nil) + } else { + arguments.presentController(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: theme), title: nil, text: "PBKDF2 \(duration1 * 1000.0) ms\nArgon2 \(duration2 * 1000.0) ms", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), nil) + } + }) } } } @@ -207,6 +237,7 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS entries.append(.enableRaiseToSpeak(presentationData.theme, mediaInputSettings.enableRaiseToSpeak)) entries.append(.keepChatNavigationStack(presentationData.theme, experimentalSettings.keepChatNavigationStack)) + entries.append(.testHashing(presentationData.theme)) return entries } diff --git a/TelegramUI/DeviceContactData.swift b/TelegramUI/DeviceContactData.swift new file mode 100644 index 0000000000..5a430868f5 --- /dev/null +++ b/TelegramUI/DeviceContactData.swift @@ -0,0 +1,385 @@ + import Foundation +import Contacts + +public final class DeviceContactPhoneNumberData: Equatable { + public let label: String + public let value: String + + public init(label: String, value: String) { + self.label = label + self.value = value + } + + public static func == (lhs: DeviceContactPhoneNumberData, rhs: DeviceContactPhoneNumberData) -> Bool { + if lhs.label != rhs.label { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } +} + +public final class DeviceContactEmailAddressData: Equatable { + public let label: String + public let value: String + + public init(label: String, value: String) { + self.label = label + self.value = value + } + + public static func == (lhs: DeviceContactEmailAddressData, rhs: DeviceContactEmailAddressData) -> Bool { + if lhs.label != rhs.label { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } +} + +public final class DeviceContactUrlData: Equatable { + public let label: String + public let value: String + + public init(label: String, value: String) { + self.label = label + self.value = value + } + + public static func == (lhs: DeviceContactUrlData, rhs: DeviceContactUrlData) -> Bool { + if lhs.label != rhs.label { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } +} + +public final class DeviceContactAddressData: Equatable, Hashable { + public let label: String + public let street1: String + public let street2: String + public let state: String + public let city: String + public let country: String + public let postcode: String + + public init(label: String, street1: String, street2: String, state: String, city: String, country: String, postcode: String) { + self.label = label + self.street1 = street1 + self.street2 = street2 + self.state = state + self.city = city + self.country = country + self.postcode = postcode + } + + public static func == (lhs: DeviceContactAddressData, rhs: DeviceContactAddressData) -> Bool { + if lhs.label != rhs.label { + return false + } + if lhs.street1 != rhs.street1 { + return false + } + if lhs.street2 != rhs.street2 { + return false + } + if lhs.state != rhs.state { + return false + } + if lhs.city != rhs.city { + return false + } + if lhs.country != rhs.country { + return false + } + if lhs.postcode != rhs.postcode { + return false + } + return true + } + + public var hashValue: Int { + var result = 0 + result = result &* 31 &+ self.label.hashValue + result = result &* 31 &+ self.street1.hashValue + result = result &* 31 &+ self.street2.hashValue + result = result &* 31 &+ self.state.hashValue + result = result &* 31 &+ self.city.hashValue + result = result &* 31 &+ self.country.hashValue + result = result &* 31 &+ self.postcode.hashValue + return result + } +} + +public final class DeviceContactSocialProfileData: Equatable, Hashable { + public let label: String + public let service: String + public let username: String + + public init(label: String, service: String, username: String) { + self.label = label + self.service = service + self.username = username + } + + public static func == (lhs: DeviceContactSocialProfileData, rhs: DeviceContactSocialProfileData) -> Bool { + if lhs.label != rhs.label { + return false + } + if lhs.service != rhs.service { + return false + } + if lhs.username != rhs.username { + return false + } + return true + } + + public var hashValue: Int { + var result = 0 + result = result &* 31 &+ self.label.hashValue + result = result &* 31 &+ self.service.hashValue + result = result &* 31 &+ self.username.hashValue + return result + } +} + +public final class DeviceContactInstantMessagingProfileData: Equatable, Hashable { + public let label: String + public let service: String + public let username: String + + public init(label: String, service: String, username: String) { + self.label = label + self.service = service + self.username = username + } + + public static func == (lhs: DeviceContactInstantMessagingProfileData, rhs: DeviceContactInstantMessagingProfileData) -> Bool { + if lhs.label != rhs.label { + return false + } + if lhs.service != rhs.service { + return false + } + if lhs.username != rhs.username { + return false + } + return true + } + + public var hashValue: Int { + var result = 0 + result = result &* 31 &+ self.label.hashValue + result = result &* 31 &+ self.service.hashValue + result = result &* 31 &+ self.username.hashValue + return result + } +} + +public final class DeviceContactBasicData: Equatable { + public let firstName: String + public let lastName: String + public let phoneNumbers: [DeviceContactPhoneNumberData] + + public init(firstName: String, lastName: String, phoneNumbers: [DeviceContactPhoneNumberData]) { + self.firstName = firstName + self.lastName = lastName + self.phoneNumbers = phoneNumbers + } + + public static func ==(lhs: DeviceContactBasicData, rhs: DeviceContactBasicData) -> Bool { + if lhs.firstName != rhs.firstName { + return false + } + if lhs.lastName != rhs.lastName { + return false + } + if lhs.phoneNumbers != rhs.phoneNumbers { + return false + } + return true + } +} + +public final class DeviceContactExtendedData: Equatable { + public let basicData: DeviceContactBasicData + + public let middleName: String + public let prefix: String + public let suffix: String + public let organization: String + public let jobTitle: String + public let department: String + public let emailAddresses: [DeviceContactEmailAddressData] + public let urls: [DeviceContactUrlData] + public let addresses: [DeviceContactAddressData] + public let birthdayDate: Date? + public let socialProfiles: [DeviceContactSocialProfileData] + public let instantMessagingProfiles: [DeviceContactInstantMessagingProfileData] + + public init(basicData: DeviceContactBasicData, middleName: String, prefix: String, suffix: String, organization: String, jobTitle: String, department: String, emailAddresses: [DeviceContactEmailAddressData], urls: [DeviceContactUrlData], addresses: [DeviceContactAddressData], birthdayDate: Date?, socialProfiles: [DeviceContactSocialProfileData], instantMessagingProfiles: [DeviceContactInstantMessagingProfileData]) { + self.basicData = basicData + self.middleName = middleName + self.prefix = prefix + self.suffix = suffix + self.organization = organization + self.jobTitle = jobTitle + self.department = department + self.emailAddresses = emailAddresses + self.urls = urls + self.addresses = addresses + self.birthdayDate = birthdayDate + self.socialProfiles = socialProfiles + self.instantMessagingProfiles = instantMessagingProfiles + } + + public static func ==(lhs: DeviceContactExtendedData, rhs: DeviceContactExtendedData) -> Bool { + if lhs.basicData != rhs.basicData { + return false + } + if lhs.middleName != rhs.middleName { + return false + } + if lhs.prefix != rhs.prefix { + return false + } + if lhs.suffix != rhs.suffix { + return false + } + if lhs.organization != rhs.organization { + return false + } + if lhs.jobTitle != rhs.jobTitle { + return false + } + if lhs.department != rhs.department { + return false + } + if lhs.emailAddresses != rhs.emailAddresses { + return false + } + if lhs.urls != rhs.urls { + return false + } + if lhs.addresses != rhs.addresses { + return false + } + if lhs.birthdayDate != rhs.birthdayDate { + return false + } + if lhs.socialProfiles != rhs.socialProfiles { + return false + } + if lhs.instantMessagingProfiles != rhs.instantMessagingProfiles { + return false + } + return true + } +} + +extension DeviceContactExtendedData { + convenience init?(vcard: Data) { + if #available(iOSApplicationExtension 9.0, *) { + guard let contact = (try? CNContactVCardSerialization.contacts(with: vcard))?.first else { + return nil + } + self.init(contact: contact) + } else { + return nil + } + } + + @available(iOSApplicationExtension 9.0, *) + func asMutableCNContact() -> CNMutableContact { + let contact = CNMutableContact() + contact.givenName = self.basicData.firstName + contact.familyName = self.basicData.lastName + contact.namePrefix = self.prefix + contact.nameSuffix = self.suffix + contact.middleName = self.middleName + contact.phoneNumbers = self.basicData.phoneNumbers.map { phoneNumber -> CNLabeledValue in + return CNLabeledValue(label: phoneNumber.label, value: CNPhoneNumber(stringValue: phoneNumber.value)) + } + contact.emailAddresses = self.emailAddresses.map { email -> CNLabeledValue in + CNLabeledValue(label: email.label, value: email.value as NSString) + } + contact.urlAddresses = self.urls.map { url -> CNLabeledValue in + CNLabeledValue(label: url.label, value: url.value as NSString) + } + contact.socialProfiles = self.socialProfiles.map({ profile -> CNLabeledValue in + return CNLabeledValue(label: profile.label, value: CNSocialProfile(urlString: nil, username: profile.username, userIdentifier: nil, service: profile.service)) + }) + contact.instantMessageAddresses = self.instantMessagingProfiles.map({ profile -> CNLabeledValue in + return CNLabeledValue(label: profile.label, value: CNInstantMessageAddress(username: profile.username, service: profile.service)) + }) + contact.postalAddresses = self.addresses.map({ address -> CNLabeledValue in + let value = CNMutablePostalAddress() + value.street = address.street1 + "\n" + address.street2 + value.state = address.state + value.city = address.city + value.country = address.country + value.postalCode = address.postcode + return CNLabeledValue(label: address.label, value: value) + }) + return contact + } + + func serializedVCard() -> String? { + if #available(iOSApplicationExtension 9.0, *) { + guard let data = try? CNContactVCardSerialization.data(with: [self.asMutableCNContact()]) else { + return nil + } + return String(data: data, encoding: .utf8) + } + return nil + } + + @available(iOSApplicationExtension 9.0, *) + convenience init(contact: CNContact) { + var phoneNumbers: [DeviceContactPhoneNumberData] = [] + for number in contact.phoneNumbers { + phoneNumbers.append(DeviceContactPhoneNumberData(label: number.label ?? "", value: number.value.stringValue)) + } + var emailAddresses: [DeviceContactEmailAddressData] = [] + for email in contact.emailAddresses { + emailAddresses.append(DeviceContactEmailAddressData(label: email.label ?? "", value: email.value as String)) + } + + var urls: [DeviceContactUrlData] = [] + for url in contact.urlAddresses { + urls.append(DeviceContactUrlData(label: url.label ?? "", value: url.value as String)) + } + + var addresses: [DeviceContactAddressData] = [] + for address in contact.postalAddresses { + addresses.append(DeviceContactAddressData(label: address.label ?? "", street1: address.value.street, street2: "", state: address.value.state, city: address.value.city, country: address.value.country, postcode: address.value.postalCode)) + } + + var birthdayDate: Date? + if let birthday = contact.birthday { + if let date = birthday.date { + birthdayDate = date + } + } + var socialProfiles: [DeviceContactSocialProfileData] = [] + for profile in contact.socialProfiles { + socialProfiles.append(DeviceContactSocialProfileData(label: profile.label ?? "", service: profile.value.service, username: profile.value.username)) + } + + var instantMessagingProfiles: [DeviceContactInstantMessagingProfileData] = [] + for profile in contact.instantMessageAddresses { + instantMessagingProfiles.append(DeviceContactInstantMessagingProfileData(label: profile.label ?? "", service: profile.value.service, username: profile.value.username)) + } + + let basicData = DeviceContactBasicData(firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers) + self.init(basicData: basicData, middleName: contact.middleName, prefix: contact.namePrefix, suffix: contact.nameSuffix, organization: contact.organizationName, jobTitle: contact.jobTitle, department: contact.departmentName, emailAddresses: emailAddresses, urls: urls, addresses: addresses, birthdayDate: birthdayDate, socialProfiles: socialProfiles, instantMessagingProfiles: instantMessagingProfiles) + } +} diff --git a/TelegramUI/DeviceContactDataManager.swift b/TelegramUI/DeviceContactDataManager.swift new file mode 100644 index 0000000000..3cf5831d46 --- /dev/null +++ b/TelegramUI/DeviceContactDataManager.swift @@ -0,0 +1,452 @@ +import Foundation +import SwiftSignalKit +import TelegramCore +import Contacts +import AddressBook + +public typealias DeviceContactStableId = String + +private protocol DeviceContactDataContext { + func getExtendedContactData(stableId: DeviceContactStableId) -> DeviceContactExtendedData? + func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData? + func createContactWithData(_ contactData: DeviceContactExtendedData) -> (DeviceContactStableId, DeviceContactExtendedData)? +} + +@available(iOSApplicationExtension 9.0, *) +private final class DeviceContactDataModernContext: DeviceContactDataContext { + let store = CNContactStore() + var updateHandle: NSObjectProtocol? + var currentContacts: [DeviceContactStableId: DeviceContactBasicData] = [:] + + init(queue: Queue, updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void) { + self.currentContacts = self.retrieveContacts() + updated(self.currentContacts) + let handle = NotificationCenter.default.addObserver(forName: NSNotification.Name.CNContactStoreDidChange, object: nil, queue: nil, using: { [weak self] _ in + queue.async { + guard let strongSelf = self else { + return + } + let contacts = strongSelf.retrieveContacts() + if strongSelf.currentContacts != contacts { + strongSelf.currentContacts = contacts + updated(strongSelf.currentContacts) + } + } + }) + self.updateHandle = handle + } + + deinit { + if let updateHandle = updateHandle { + NotificationCenter.default.removeObserver(updateHandle) + } + } + + private func retrieveContacts() -> [DeviceContactStableId: DeviceContactBasicData] { + let keysToFetch: [CNKeyDescriptor] = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor] + + let request = CNContactFetchRequest(keysToFetch: keysToFetch) + request.unifyResults = true + + var result: [DeviceContactStableId: DeviceContactBasicData] = [:] + let _ = try? self.store.enumerateContacts(with: request, usingBlock: { contact, _ in + let stableIdAndContact = DeviceContactDataModernContext.parseContact(contact) + result[stableIdAndContact.0] = stableIdAndContact.1 + }) + return result + } + + private static func parseContact(_ contact: CNContact) -> (DeviceContactStableId, DeviceContactBasicData) { + var phoneNumbers: [DeviceContactPhoneNumberData] = [] + for number in contact.phoneNumbers { + phoneNumbers.append(DeviceContactPhoneNumberData(label: number.label ?? "", value: number.value.stringValue)) + } + return (contact.identifier, DeviceContactBasicData(firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers)) + } + + func getExtendedContactData(stableId: DeviceContactStableId) -> DeviceContactExtendedData? { + let keysToFetch: [CNKeyDescriptor] = [ + CNContactFormatter.descriptorForRequiredKeys(for: .fullName), + CNContactPhoneNumbersKey as CNKeyDescriptor, + CNContactEmailAddressesKey as CNKeyDescriptor, + CNContactBirthdayKey as CNKeyDescriptor, + CNContactSocialProfilesKey as CNKeyDescriptor, + CNContactInstantMessageAddressesKey as CNKeyDescriptor, + CNContactPostalAddressesKey as CNKeyDescriptor, + CNContactUrlAddressesKey as CNKeyDescriptor, + CNContactOrganizationNameKey as CNKeyDescriptor, + CNContactJobTitleKey as CNKeyDescriptor, + CNContactDepartmentNameKey as CNKeyDescriptor + ] + + guard let contact = try? self.store.unifiedContact(withIdentifier: stableId, keysToFetch: keysToFetch) else { + return nil + } + + return DeviceContactExtendedData(contact: contact) + } + + func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData? { + let keysToFetch: [CNKeyDescriptor] = [ + CNContactFormatter.descriptorForRequiredKeys(for: .fullName), + CNContactPhoneNumbersKey as CNKeyDescriptor, + CNContactEmailAddressesKey as CNKeyDescriptor, + CNContactBirthdayKey as CNKeyDescriptor, + CNContactSocialProfilesKey as CNKeyDescriptor, + CNContactInstantMessageAddressesKey as CNKeyDescriptor, + CNContactPostalAddressesKey as CNKeyDescriptor, + CNContactUrlAddressesKey as CNKeyDescriptor, + CNContactOrganizationNameKey as CNKeyDescriptor, + CNContactJobTitleKey as CNKeyDescriptor, + CNContactDepartmentNameKey as CNKeyDescriptor + ] + + guard let current = try? self.store.unifiedContact(withIdentifier: stableId, keysToFetch: keysToFetch) else { + return nil + } + + let contact = contactData.asMutableCNContact() + + let mutableContact = current.mutableCopy() as! CNMutableContact + mutableContact.givenName = contact.givenName + mutableContact.familyName = contact.familyName + + var phoneNumbers = mutableContact.phoneNumbers + for phoneNumber in contact.phoneNumbers.reversed() { + var found = false + inner: for n in phoneNumbers { + if n.value.stringValue == phoneNumber.value.stringValue { + found = true + break inner + } + } + if !found { + phoneNumbers.insert(phoneNumber, at: 0) + } + } + mutableContact.phoneNumbers = phoneNumbers + + var urlAddresses = mutableContact.urlAddresses + for urlAddress in contact.urlAddresses.reversed() { + var found = false + inner: for n in urlAddresses { + if n.value.isEqual(urlAddress.value) { + found = true + break inner + } + } + if !found { + urlAddresses.insert(urlAddress, at: 0) + } + } + mutableContact.urlAddresses = urlAddresses + + var emailAddresses = mutableContact.emailAddresses + for emailAddress in contact.emailAddresses.reversed() { + var found = false + inner: for n in emailAddresses { + if n.value.isEqual(emailAddress.value) { + found = true + break inner + } + } + if !found { + emailAddresses.insert(emailAddress, at: 0) + } + } + mutableContact.emailAddresses = emailAddresses + + var postalAddresses = mutableContact.postalAddresses + for postalAddress in contact.postalAddresses.reversed() { + var found = false + inner: for n in postalAddresses { + if n.value.isEqual(postalAddress.value) { + found = true + break inner + } + } + if !found { + postalAddresses.insert(postalAddress, at: 0) + } + } + mutableContact.postalAddresses = postalAddresses + + if contact.birthday != nil { + mutableContact.birthday = contact.birthday + } + + var socialProfiles = mutableContact.socialProfiles + for socialProfile in contact.socialProfiles.reversed() { + var found = false + inner: for n in socialProfiles { + if n.value.username.lowercased() == socialProfile.value.username.lowercased() && n.value.service.lowercased() == socialProfile.value.service.lowercased() { + found = true + break inner + } + } + if !found { + socialProfiles.insert(socialProfile, at: 0) + } + } + mutableContact.socialProfiles = socialProfiles + + var instantMessageAddresses = mutableContact.instantMessageAddresses + for instantMessageAddress in contact.instantMessageAddresses.reversed() { + var found = false + inner: for n in instantMessageAddresses { + if n.value.isEqual(instantMessageAddress.value) { + found = true + break inner + } + } + if !found { + instantMessageAddresses.insert(instantMessageAddress, at: 0) + } + } + mutableContact.instantMessageAddresses = instantMessageAddresses + + let saveRequest = CNSaveRequest() + saveRequest.update(mutableContact) + let _ = try? self.store.execute(saveRequest) + + return DeviceContactExtendedData(contact: mutableContact) + } + + func createContactWithData(_ contactData: DeviceContactExtendedData) -> (DeviceContactStableId, DeviceContactExtendedData)? { + let saveRequest = CNSaveRequest() + let mutableContact = contactData.asMutableCNContact() + saveRequest.add(mutableContact, toContainerWithIdentifier: nil) + let _ = try? self.store.execute(saveRequest) + + return (mutableContact.identifier, contactData) + } +} + +private final class ExtendedContactDataContext { + var value: DeviceContactExtendedData? + let subscribers = Bag<(DeviceContactExtendedData) -> Void>() +} + +private final class DeviceContactDataManagerImpl { + private let queue: Queue + + private var dataContext: DeviceContactDataContext? + private var extendedContexts: [DeviceContactStableId: ExtendedContactDataContext] = [:] + + private var stableIdToBasicContactData: [DeviceContactStableId: DeviceContactBasicData] = [:] + private var normalizedPhoneNumberToStableId: [DeviceContactNormalizedPhoneNumber: [DeviceContactStableId]] = [:] + + private var importableContacts = Set() + + private var accessDisposable: Disposable? + private let dataDisposable = MetaDisposable() + + private let basicDataSubscribers = Bag<([DeviceContactStableId: DeviceContactBasicData]) -> Void>() + private let importableContactsSubscribers = Bag<(Set) -> Void>() + + init(queue: Queue) { + self.queue = queue + self.accessDisposable = (DeviceAccess.contacts + |> deliverOn(self.queue)).start(next: { [weak self] authorizationStatus in + guard let strongSelf = self else { + return + } + if let authorizationStatus = authorizationStatus, authorizationStatus { + if #available(iOSApplicationExtension 9.0, *) { + strongSelf.dataContext = DeviceContactDataModernContext(queue: strongSelf.queue, updated: { stableIdToBasicContactData in + guard let strongSelf = self else { + return + } + strongSelf.updateAll(stableIdToBasicContactData) + }) + } else { + + } + } else { + strongSelf.updateAll([:]) + } + }) + } + + deinit { + self.accessDisposable?.dispose() + } + + private func updateAll(_ stableIdToBasicContactData: [DeviceContactStableId: DeviceContactBasicData]) { + self.stableIdToBasicContactData = stableIdToBasicContactData + var normalizedPhoneNumberToStableId: [DeviceContactNormalizedPhoneNumber: [DeviceContactStableId]] = [:] + for (stableId, basicData) in self.stableIdToBasicContactData { + for phoneNumber in basicData.phoneNumbers { + let normalizedPhoneNumber = DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phoneNumber.value)) + if normalizedPhoneNumberToStableId[normalizedPhoneNumber] == nil { + normalizedPhoneNumberToStableId[normalizedPhoneNumber] = [] + } + normalizedPhoneNumberToStableId[normalizedPhoneNumber]!.append(stableId) + } + } + self.normalizedPhoneNumberToStableId = normalizedPhoneNumberToStableId + for f in self.basicDataSubscribers.copyItems() { + f(self.stableIdToBasicContactData) + } + } + + func basicData(updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void) -> Disposable { + let queue = self.queue + + let index = self.basicDataSubscribers.add({ data in + updated(data) + }) + + updated(self.stableIdToBasicContactData) + + return ActionDisposable { [weak self] in + queue.async { + guard let strongSelf = self else { + return + } + strongSelf.basicDataSubscribers.remove(index) + } + } + } + + func extendedData(stableId: String, updated: @escaping (DeviceContactExtendedData?) -> Void) -> Disposable { + let queue = self.queue + + let current = self.dataContext?.getExtendedContactData(stableId: stableId) + updated(current) + + return ActionDisposable { [weak self] in + queue.async { + + } + } + } + + func importable(updated: @escaping (Set) -> Void) -> Disposable { + let queue = self.queue + + let index = self.importableContactsSubscribers.add({ data in + updated(data) + }) + + updated(self.importableContacts) + + return ActionDisposable { [weak self] in + queue.async { + guard let strongSelf = self else { + return + } + strongSelf.importableContactsSubscribers.remove(index) + } + } + } + + func search(query: String, updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void) -> Disposable { + let normalizedQuery = query.lowercased() + var result: [DeviceContactStableId: DeviceContactBasicData] = [:] + for (stableId, basicData) in self.stableIdToBasicContactData { + if basicData.firstName.lowercased().hasPrefix(normalizedQuery) || basicData.lastName.lowercased().hasPrefix(normalizedQuery) { + result[stableId] = basicData + } + } + updated(result) + return EmptyDisposable + } + + func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId, completion: @escaping (DeviceContactExtendedData?) -> Void) { + let result = self.dataContext?.appendContactData(contactData, to: stableId) + completion(result) + } + + func createContactWithData(_ contactData: DeviceContactExtendedData, completion: @escaping ((DeviceContactStableId, DeviceContactExtendedData)?) -> Void) { + let result = self.dataContext?.createContactWithData(contactData) + completion(result) + } +} + +public final class DeviceContactDataManager { + private let queue = Queue() + private let impl: QueueLocalObject + + init() { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return DeviceContactDataManagerImpl(queue: queue) + }) + } + + public func basicData() -> Signal<[DeviceContactStableId: DeviceContactBasicData], NoError> { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with({ impl in + disposable.set(impl.basicData(updated: { value in + subscriber.putNext(value) + })) + }) + return disposable + } + } + + public func extendedData(stableId: DeviceContactStableId) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with({ impl in + disposable.set(impl.extendedData(stableId: stableId, updated: { value in + subscriber.putNext(value) + })) + }) + return disposable + } + } + + public func importable() -> Signal, NoError> { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with({ impl in + disposable.set(impl.importable(updated: { value in + subscriber.putNext(value) + })) + }) + return disposable + } + } + + public func search(query: String) -> Signal<[DeviceContactStableId: DeviceContactBasicData], NoError> { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with({ impl in + disposable.set(impl.search(query: query, updated: { value in + subscriber.putNext(value) + subscriber.putCompletion() + })) + }) + return disposable + } + } + + func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with({ impl in + impl.appendContactData(contactData, to: stableId, completion: { next in + subscriber.putNext(next) + subscriber.putCompletion() + }) + }) + return disposable + } + } + + func createContactWithData(_ contactData: DeviceContactExtendedData) -> Signal<(DeviceContactStableId, DeviceContactExtendedData)?, NoError> { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with({ impl in + impl.createContactWithData(contactData, completion: { next in + subscriber.putNext(next) + subscriber.putCompletion() + }) + }) + return disposable + } + } +} diff --git a/TelegramUI/DeviceContactInfoController.swift b/TelegramUI/DeviceContactInfoController.swift new file mode 100644 index 0000000000..35191188e4 --- /dev/null +++ b/TelegramUI/DeviceContactInfoController.swift @@ -0,0 +1,1072 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import MessageUI + +private enum DeviceContactInfoAction { + case sendMessage + case createContact + case addToExisting + case invite +} + +private final class DeviceContactInfoControllerArguments { + let account: Account + let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void + let updatePhone: (Int64, String) -> Void + let updatePhoneLabel: (Int64, String) -> Void + let deletePhone: (Int64) -> Void + let setPhoneIdWithRevealedOptions: (Int64?, Int64?) -> Void + let addPhoneNumber: () -> Void + let performAction: (DeviceContactInfoAction) -> Void + let toggleSelection: (DeviceContactInfoDataId) -> Void + let callPhone: (String) -> Void + let openUrl: (String) -> Void + let openAddress: (DeviceContactAddressData) -> Void + + init(account: Account, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updatePhone: @escaping (Int64, String) -> Void, updatePhoneLabel: @escaping (Int64, String) -> Void, deletePhone: @escaping (Int64) -> Void, setPhoneIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, addPhoneNumber: @escaping () -> Void, performAction: @escaping (DeviceContactInfoAction) -> Void, toggleSelection: @escaping (DeviceContactInfoDataId) -> Void, callPhone: @escaping (String) -> Void, openUrl: @escaping (String) -> Void, openAddress: @escaping (DeviceContactAddressData) -> Void) { + self.account = account + self.updateEditingName = updateEditingName + self.updatePhone = updatePhone + self.updatePhoneLabel = updatePhoneLabel + self.deletePhone = deletePhone + self.setPhoneIdWithRevealedOptions = setPhoneIdWithRevealedOptions + self.addPhoneNumber = addPhoneNumber + self.performAction = performAction + self.toggleSelection = toggleSelection + self.callPhone = callPhone + self.openUrl = openUrl + self.openAddress = openAddress + } +} + +private enum DeviceContactInfoSection: ItemListSectionId { + case info + case editing + case data +} + +private enum DeviceContactInfoEntryTag: Equatable, ItemListItemTag { + case editingPhone(Int64) + case company + + func isEqual(to other: ItemListItemTag) -> Bool { + return self == (other as? DeviceContactInfoEntryTag) + } +} + +private enum DeviceContactInfoDataId: Hashable { + case job + case phoneNumber(String, String) + case email(String, String) + case url(String, String) + case address(DeviceContactAddressData) + case birthday + case socialProfile(DeviceContactSocialProfileData) + case instantMessenger(DeviceContactInstantMessagingProfileData) +} + +private enum DeviceContactInfoConstantEntryId: Hashable { + case info + case invite + case sendMessage + case createContact + case addToExisting + case company + case birthday + case addPhoneNumber +} + +private enum DeviceContactInfoEntryId: Hashable { + case constant(DeviceContactInfoConstantEntryId) + case phoneNumber(Int) + case email(Int) + case url(Int) + case address(Int) + case socialProfile(Int) + case instantMessenger(Int) + case editingPhoneNumber(Int64) +} + +private enum DeviceContactInfoEntry: ItemListNodeEntry { + case info(Int, PresentationTheme, PresentationStrings, peer: Peer, state: ItemListAvatarAndNameInfoItemState, job: String?) + + case invite(Int, PresentationTheme, String) + case sendMessage(Int, PresentationTheme, String) + case createContact(Int, PresentationTheme, String) + case addToExisting(Int, PresentationTheme, String) + + case company(Int, PresentationTheme, String, String, Bool?) + case phoneNumber(Int, Int, PresentationTheme, String, String, String, Bool?) + case editingPhoneNumber(Int, PresentationTheme, PresentationStrings, Int64, String, String, String, Bool) + case addPhoneNumber(Int, PresentationTheme, String) + case email(Int, Int, PresentationTheme, String, String, String, Bool?) + case url(Int, Int, PresentationTheme, String, String, String, Bool?) + case address(Int, Int, PresentationTheme, String, DeviceContactAddressData, Bool?) + case birthday(Int, PresentationTheme, String, Date, String, Bool?) + case socialProfile(Int, Int, PresentationTheme, String, DeviceContactSocialProfileData, String, Bool?) + case instantMessenger(Int, Int, PresentationTheme, String, DeviceContactInstantMessagingProfileData, String, Bool?) + + var section: ItemListSectionId { + switch self { + case .info: + return DeviceContactInfoSection.info.rawValue + case .editingPhoneNumber, .addPhoneNumber: + return DeviceContactInfoSection.editing.rawValue + case .invite, .sendMessage, .createContact, .addToExisting: + return DeviceContactInfoSection.info.rawValue + default: + return DeviceContactInfoSection.data.rawValue + } + } + + var stableId: DeviceContactInfoEntryId { + switch self { + case .info: + return .constant(.info) + case .sendMessage: + return .constant(.sendMessage) + case .invite: + return .constant(.invite) + case .createContact: + return .constant(.createContact) + case .addToExisting: + return .constant(.addToExisting) + case .company: + return .constant(.company) + case let .phoneNumber(_, catIndex, _, _, _, _, _): + return .phoneNumber(catIndex) + case let .editingPhoneNumber(_, _, _, id, _, _, _, _): + return .editingPhoneNumber(id) + case .addPhoneNumber: + return .constant(.addPhoneNumber) + case let .email(_, catIndex, _, _, _, _, _): + return .email(catIndex) + case let .url(_, catIndex, _, _, _, _, _): + return .url(catIndex) + case let .address(_, catIndex, _, _, _, _): + return .address(catIndex) + case .birthday: + return .constant(.birthday) + case let .socialProfile(_, catIndex, _, _, _, _, _): + return .socialProfile(catIndex) + case let .instantMessenger(_, catIndex, _, _, _, _, _): + return .instantMessenger(catIndex) + } + } + + static func ==(lhs: DeviceContactInfoEntry, rhs: DeviceContactInfoEntry) -> Bool { + switch lhs { + case let .info(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsState, lhsJobSummary): + if case let .info(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsState, rhsJobSummary) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if !arePeersEqual(lhsPeer, rhsPeer) { + return false + } + if lhsState != rhsState { + return false + } + if lhsJobSummary != rhsJobSummary { + return false + } + return true + } else { + return false + } + case let .sendMessage(lhsIndex, lhsTheme, lhsTitle): + if case let .sendMessage(rhsIndex, rhsTheme, rhsTitle) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle { + return true + } else { + return false + } + case let .invite(lhsIndex, lhsTheme, lhsTitle): + if case let .invite(rhsIndex, rhsTheme, rhsTitle) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle { + return true + } else { + return false + } + case let .createContact(lhsIndex, lhsTheme, lhsTitle): + if case let .createContact(rhsIndex, rhsTheme, rhsTitle) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle { + return true + } else { + return false + } + case let .addToExisting(lhsIndex, lhsTheme, lhsTitle): + if case let .addToExisting(rhsIndex, rhsTheme, rhsTitle) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle { + return true + } else { + return false + } + case let .company(lhsIndex, lhsTheme, lhsTitle, lhsValue, lhsSelected): + if case let .company(rhsIndex, rhsTheme, rhsTitle, rhsValue, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .phoneNumber(lhsIndex, lhsCatIndex, lhsTheme, lhsTitle, lhsLabel, lhsValue, lhsSelected): + if case let .phoneNumber(rhsIndex, rhsCatIndex, rhsTheme, rhsTitle, rhsLabel, rhsValue, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCatIndex == rhsCatIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsLabel == rhsLabel, lhsValue == rhsValue, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .editingPhoneNumber(lhsIndex, lhsTheme, lhsStrings, lhsId, lhsTitle, lhsLabel, lhsValue, lhsSelected): + if case let .editingPhoneNumber(rhsIndex, rhsTheme, rhsStrings, rhsId, rhsTitle, rhsLabel, rhsValue, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsId == rhsId, lhsTitle == rhsTitle, lhsLabel == rhsLabel, lhsValue == rhsValue, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .addPhoneNumber(lhsIndex, lhsTheme, lhsTitle): + if case let .addPhoneNumber(rhsIndex, rhsTheme, rhsTitle) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle { + return true + } else { + return false + } + case let .email(lhsIndex, lhsCatIndex, lhsTheme, lhsTitle, lhsLabel, lhsValue, lhsSelected): + if case let .email(rhsIndex, rhsCatIndex, rhsTheme, rhsTitle, rhsLabel, rhsValue, rhsSelected) = rhs, lhsCatIndex == rhsCatIndex, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsLabel == rhsLabel, lhsValue == rhsValue, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .url(lhsIndex, lhsCatIndex, lhsTheme, lhsTitle, lhsLabel, lhsValue, lhsSelected): + if case let .url(rhsIndex, rhsCatIndex, rhsTheme, rhsTitle, rhsLabel, rhsValue, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCatIndex == rhsCatIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsLabel == rhsLabel, lhsValue == rhsValue, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .address(lhsIndex, lhsCatIndex, lhsTheme, lhsTitle, lhsValue, lhsSelected): + if case let .address(rhsIndex, rhsCatIndex, rhsTheme, rhsTitle, rhsValue, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCatIndex == rhsCatIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .birthday(lhsIndex, lhsTheme, lhsTitle, lhsValue, lhsText, lhsSelected): + if case let .birthday(rhsIndex, rhsTheme, rhsTitle, rhsValue, rhsText, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsText == rhsText, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .socialProfile(lhsIndex, lhsCatIndex, lhsTheme, lhsTitle, lhsValue, lhsText, lhsSelected): + if case let .socialProfile(rhsIndex, rhsCatIndex, rhsTheme, rhsTitle, rhsValue, rhsText, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCatIndex == rhsCatIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsText == rhsText, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .instantMessenger(lhsIndex, lhsCatIndex, lhsTheme, lhsTitle, lhsValue, lhsText, lhsSelected): + if case let .instantMessenger(rhsIndex, rhsCatIndex, rhsTheme, rhsTitle, rhsValue, rhsText, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCatIndex == rhsCatIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsText == rhsText, lhsSelected == rhsSelected { + return true + } else { + return false + } + } + } + + private var sortIndex: Int { + switch self { + case let .info(index, _, _, _, _, _): + return index + case let .sendMessage(index, _, _): + return index + case let .invite(index, _, _): + return index + case let .createContact(index, _, _): + return index + case let .addToExisting(index, _, _): + return index + case let .company(index, _, _, _, _): + return index + case let .phoneNumber(index, _, _, _, _, _, _): + return index + case let .editingPhoneNumber(index, _, _, _, _, _, _, _): + return index + case let .addPhoneNumber(index, _, _): + return index + case let .email(index, _, _, _, _, _, _): + return index + case let .url(index, _, _, _, _, _, _): + return index + case let .address(index, _, _, _, _, _): + return index + case let .birthday(index, _, _, _, _, _): + return index + case let .socialProfile(index, _, _, _, _, _, _): + return index + case let .instantMessenger(index, _, _, _, _, _, _): + return index + } + } + + static func <(lhs: DeviceContactInfoEntry, rhs: DeviceContactInfoEntry) -> Bool { + return lhs.sortIndex < rhs.sortIndex + } + + func item(_ arguments: DeviceContactInfoControllerArguments) -> ListViewItem { + switch self { + case let .info(_, theme, strings, peer, state, jobSummary): + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: peer, presence: nil, label: jobSummary, cachedData: nil, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in + arguments.updateEditingName(editingName) + }, avatarTapped: { + }, context: nil, call: nil) + case let .sendMessage(_, theme, title): + return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + arguments.performAction(.sendMessage) + }) + case let .invite(_, theme, title): + return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + arguments.performAction(.invite) + }) + case let .createContact(_, theme, title): + return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + arguments.performAction(.createContact) + }) + case let .addToExisting(_, theme, title): + return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + arguments.performAction(.addToExisting) + }) + case let .company(_, theme, title, value, selected): + return ItemListTextWithLabelItem(theme: theme, label: title, text: value, enabledEntitiyTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { + }, tag: nil) + case let .phoneNumber(_, _, theme, title, label, value, selected): + return ItemListTextWithLabelItem(theme: theme, label: title, text: value, textColor: .accent, enabledEntitiyTypes: [], multiline: false, selected: selected, sectionId: self.section, action: { + if selected != nil { + arguments.toggleSelection(.phoneNumber(label, value)) + } else { + arguments.callPhone(value) + } + }, tag: nil) + case let .editingPhoneNumber(_, theme, strings, id, title, label, value, hasActiveRevealControls): + return UserInfoEditingPhoneItem(theme: theme, strings: strings, id: id, label: title, value: value, editing: UserInfoEditingPhoneItemEditing(editable: true, hasActiveRevealControls: hasActiveRevealControls), sectionId: self.section, setPhoneIdWithRevealedOptions: { lhs, rhs in + arguments.setPhoneIdWithRevealedOptions(lhs, rhs) + }, updated: { value in + arguments.updatePhone(id, value) + }, selectLabel: { + arguments.updatePhoneLabel(id, label) + }, delete: { + arguments.deletePhone(id) + }, tag: DeviceContactInfoEntryTag.editingPhone(id)) + case let .addPhoneNumber(_, theme, title): + return UserInfoEditingPhoneActionItem(theme: theme, title: title, sectionId: self.section, action: { + arguments.addPhoneNumber() + }) + case let .email(_, _, theme, title, label, value, selected): + return ItemListTextWithLabelItem(theme: theme, label: title, text: value, textColor: .accent, enabledEntitiyTypes: [], multiline: false, selected: selected, sectionId: self.section, action: { + if selected != nil { + arguments.toggleSelection(.email(label, value)) + } else { + arguments.openUrl("mailto:\(value)") + } + }, tag: nil) + case let .url(_, _, theme, title, label, value, selected): + return ItemListTextWithLabelItem(theme: theme, label: title, text: value, textColor: .accent, enabledEntitiyTypes: [], multiline: false, selected: selected, sectionId: self.section, action: { + if selected != nil { + arguments.toggleSelection(.url(label, value)) + } else { + arguments.openUrl(value) + } + }, tag: nil) + case let .address(_, _, theme, title, value, selected): + var string = "" + func combineComponent(string: inout String, component: String) { + if !component.isEmpty { + if !string.isEmpty { + string.append("\n") + } + string.append(component) + } + } + combineComponent(string: &string, component: value.street1) + combineComponent(string: &string, component: value.street2) + combineComponent(string: &string, component: value.state) + combineComponent(string: &string, component: value.city) + combineComponent(string: &string, component: value.country) + combineComponent(string: &string, component: value.postcode) + return ItemListTextWithLabelItem(theme: theme, label: title, text: string, textColor: .primary, enabledEntitiyTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { + if selected != nil { + arguments.toggleSelection(.address(value)) + } else { + arguments.openAddress(value) + } + }, tag: nil) + case let .birthday(_, theme, title, value, text, selected): + return ItemListTextWithLabelItem(theme: theme, label: title, text: text, textColor: .accent, enabledEntitiyTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { + if selected != nil { + arguments.toggleSelection(.birthday) + } else { + //arguments.openUrl("mailto:\(value)") + } + }, tag: nil) + case let .socialProfile(_, _, theme, title, value, text, selected): + return ItemListTextWithLabelItem(theme: theme, label: title, text: text, textColor: .accent, enabledEntitiyTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { + if selected != nil { + arguments.toggleSelection(.socialProfile(value)) + } else { + //arguments.openUrl("mailto:\(value)") + } + }, tag: nil) + case let .instantMessenger(_, _, theme, title, value, text, selected): + return ItemListTextWithLabelItem(theme: theme, label: title, text: text, textColor: .accent, enabledEntitiyTypes: [], multiline: true, selected: selected, sectionId: self.section, action: { + if selected != nil { + arguments.toggleSelection(.instantMessenger(value)) + } else { + //arguments.openUrl("mailto:\(value)") + } + }, tag: nil) + } + } +} + +private struct DeviceContactInfoEditingState: Equatable { + let editingName: ItemListAvatarAndNameInfoItemName? + + static func ==(lhs: DeviceContactInfoEditingState, rhs: DeviceContactInfoEditingState) -> Bool { + if lhs.editingName != rhs.editingName { + return false + } + return true + } +} + +private struct EditingPhoneNumber: Equatable { + var id: Int64 + var label: String + var value: String +} + +private struct DeviceContactInfoState: Equatable { + var savingData: Bool = false + var editingState: DeviceContactInfoEditingState? = nil + var excludedComponents = Set() + var phoneNumbers: [EditingPhoneNumber] = [] + var nextPhoneNumber: Int64 = 1 + var phoneIdWithRevealedOptions: Int64? +} + +private func filteredContactData(contactData: DeviceContactExtendedData, excludedComponents: Set) -> DeviceContactExtendedData { + let phoneNumbers = contactData.basicData.phoneNumbers.filter({ phoneNumber in + return !excludedComponents.contains(.phoneNumber(phoneNumber.label, formatPhoneNumber(phoneNumber.value))) + }) + let emailAddresses = contactData.emailAddresses.filter({ email in + return !excludedComponents.contains(.email(email.label, email.value)) + }) + let urls = contactData.urls.filter({ url in + return !excludedComponents.contains(.url(url.label, url.value)) + }) + let addresses = contactData.addresses.filter({ address in + return !excludedComponents.contains(.address(address)) + }) + let socialProfiles = contactData.socialProfiles.filter({ socialProfile in + return !excludedComponents.contains(.socialProfile(socialProfile)) + }) + let instantMessagingProfiles = contactData.instantMessagingProfiles.filter({ instantMessenger in + return !excludedComponents.contains(.instantMessenger(instantMessenger)) + }) + let includeJob = !excludedComponents.contains(.job) + let includeBirthday = !excludedComponents.contains(.birthday) + + return DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumbers: phoneNumbers), middleName: contactData.middleName, prefix: contactData.prefix, suffix: contactData.suffix, organization: includeJob ? contactData.organization : "", jobTitle: includeJob ? contactData.jobTitle : "", department: includeJob ? contactData.department : "", emailAddresses: emailAddresses, urls: urls, addresses: addresses, birthdayDate: includeBirthday ? contactData.birthdayDate : nil, socialProfiles: socialProfiles, instantMessagingProfiles: instantMessagingProfiles) +} + +private func deviceContactInfoEntries(account: Account, presentationData: PresentationData, peer: Peer?, contactData: DeviceContactExtendedData, isContact: Bool, state: DeviceContactInfoState, selecting: Bool, editingPhoneNumbers: Bool) -> [DeviceContactInfoEntry] { + var entries: [DeviceContactInfoEntry] = [] + + var editingName: ItemListAvatarAndNameInfoItemName? + + var isEditing = false + if let editingState = state.editingState { + isEditing = true + editingName = editingState.editingName + } + + var jobSummary: String? + if !contactData.organization.isEmpty { + jobSummary = contactData.organization + } else if !contactData.department.isEmpty { + jobSummary = contactData.department + } else if !contactData.jobTitle.isEmpty { + jobSummary = contactData.jobTitle + } + + entries.append(.info(entries.count, presentationData.theme, presentationData.strings, peer: peer ?? TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []), state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), job: jobSummary)) + + if !selecting { + if let _ = peer { + entries.append(.sendMessage(entries.count, presentationData.theme, presentationData.strings.UserInfo_SendMessage)) + } else { + entries.append(.invite(entries.count, presentationData.theme, presentationData.strings.Contacts_InviteToTelegram)) + } + + if !isContact { + entries.append(.createContact(entries.count, presentationData.theme, presentationData.strings.UserInfo_CreateNewContact)) + entries.append(.addToExisting(entries.count, presentationData.theme, presentationData.strings.UserInfo_AddToExisting)) + } + } + + if editingPhoneNumbers { + for number in state.phoneNumbers { + entries.append(.editingPhoneNumber(entries.count, presentationData.theme, presentationData.strings, number.id, localizedPhoneNumberLabel(label: number.label, strings: presentationData.strings), number.label, number.value, state.phoneIdWithRevealedOptions == number.id)) + } + entries.append(.addPhoneNumber(entries.count, presentationData.theme, presentationData.strings.UserInfo_AddPhone)) + } else { + var numberIndex = 0 + for number in contactData.basicData.phoneNumbers { + let formattedNumber = formatPhoneNumber(number.value) + entries.append(.phoneNumber(entries.count, numberIndex, presentationData.theme, localizedPhoneNumberLabel(label: number.label, strings: presentationData.strings), number.label, formattedNumber, selecting ? !state.excludedComponents.contains(.phoneNumber(number.label, formattedNumber)) : nil)) + numberIndex += 1 + } + } + + var emailIndex = 0 + for email in contactData.emailAddresses { + entries.append(.email(entries.count, emailIndex, presentationData.theme, localizedGenericContactFieldLabel(label: email.label, strings: presentationData.strings), email.label, email.value, selecting ? !state.excludedComponents.contains(.email(email.label, email.value)) : nil)) + emailIndex += 1 + } + + var urlIndex = 0 + for url in contactData.urls { + entries.append(.url(entries.count, urlIndex, presentationData.theme, localizedGenericContactFieldLabel(label: url.label, strings: presentationData.strings), url.label, url.value, selecting ? !state.excludedComponents.contains(.url(url.label, url.value)) : nil)) + urlIndex += 1 + } + + var addressIndex = 0 + for address in contactData.addresses { + entries.append(.address(entries.count, addressIndex, presentationData.theme, localizedGenericContactFieldLabel(label: address.label, strings: presentationData.strings), address, selecting ? !state.excludedComponents.contains(.address(address)) : nil)) + addressIndex += 1 + } + + if let birthday = contactData.birthdayDate { + let dateText: String + let calendar = Calendar(identifier: .gregorian) + let components = calendar.dateComponents(Set([.era, .year, .month, .day]), from: birthday) + if let year = components.year, year > 1 { + dateText = stringForDate(timestamp: Int32(birthday.timeIntervalSince1970), strings: presentationData.strings) + } else { + dateText = stringForDateWithoutYear(date: birthday, strings: presentationData.strings) + } + entries.append(.birthday(entries.count, presentationData.theme, "birthday", birthday, dateText, selecting ? !state.excludedComponents.contains(.birthday) : nil)) + } + + var socialProfileIndex = 0 + for profile in contactData.socialProfiles { + var label = localizedGenericContactFieldLabel(label: profile.label, strings: presentationData.strings) + var text = profile.username + switch profile.service.lowercased() { + case "twitter": + label = "Twitter" + text = "@\(profile.username)" + case "facebook": + label = "Facebook" + default: + if !profile.service.isEmpty { + label = profile.service + } + } + entries.append(.socialProfile(entries.count, socialProfileIndex, presentationData.theme, label, profile, text, selecting ? !state.excludedComponents.contains(.socialProfile(profile)) : nil)) + socialProfileIndex += 1 + } + + var instantMessagingProfileIndex = 0 + for profile in contactData.instantMessagingProfiles { + var label = localizedGenericContactFieldLabel(label: profile.label, strings: presentationData.strings) + if !profile.service.isEmpty { + label = profile.service + } + entries.append(.instantMessenger(entries.count, instantMessagingProfileIndex, presentationData.theme, label, profile, profile.username, selecting ? !state.excludedComponents.contains(.instantMessenger(profile)) : nil)) + instantMessagingProfileIndex += 1 + } + + return entries +} + +enum DeviceContactInfoSubject { + case vcard(Peer?, DeviceContactStableId?, DeviceContactExtendedData) + case filter(peer: Peer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (Peer?, DeviceContactExtendedData) -> Void) + case create(peer: Peer?, contactData: DeviceContactExtendedData, completion: (Peer?, DeviceContactExtendedData) -> Void) + + var peer: Peer? { + switch self { + case let .vcard(peer, _, _): + return peer + case let .filter(peer, _, _, _): + return peer + case let .create(peer, _, _): + return nil + } + } + + var contactData: DeviceContactExtendedData { + switch self { + case let .vcard(_, _, data): + return data + case let .filter(_, _, data, _): + return data + case let .create(_, data, _): + return data + } + } +} + +private final class DeviceContactInfoController: ItemListController, MFMessageComposeViewControllerDelegate, UINavigationControllerDelegate { + private var composer: MFMessageComposeViewController? + func inviteContact(presentationData: PresentationData, numbers: [String]) { + if MFMessageComposeViewController.canSendText() { + let composer = MFMessageComposeViewController() + composer.messageComposeDelegate = self + composer.recipients = Array(Set(numbers)) + let url = presentationData.strings.InviteText_URL + let body = presentationData.strings.InviteText_SingleContact(url).0 + composer.body = body + self.composer = composer + if let window = self.view.window { + window.rootViewController?.present(composer, animated: true) + } + } + } + + @objc func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) { + self.composer = nil + + controller.dismiss(animated: true, completion: nil) + + guard case .sent = result else { + return + } + } +} + +func deviceContactInfoController(account: Account, subject: DeviceContactInfoSubject) -> ViewController { + var initialState = DeviceContactInfoState() + if case let .create(peer, contactData, _) = subject { + var peerPhoneNumber: String? + var firstName = contactData.basicData.firstName + var lastName = contactData.basicData.lastName + if let peer = peer as? TelegramUser { + firstName = peer.firstName ?? "" + lastName = peer.lastName ?? "" + if let phone = peer.phone { + let formattedPhone = formatPhoneNumber(phone) + peerPhoneNumber = formattedPhone + initialState.phoneNumbers.append(EditingPhoneNumber(id: initialState.nextPhoneNumber, label: "_$!!$_", value: formattedPhone)) + initialState.nextPhoneNumber += 1 + } + } + for phoneNumber in contactData.basicData.phoneNumbers { + if peerPhoneNumber != formatPhoneNumber(phoneNumber.value) { + initialState.phoneNumbers.append(EditingPhoneNumber(id: initialState.nextPhoneNumber, label: phoneNumber.label, value: phoneNumber.value)) + initialState.nextPhoneNumber += 1 + } + } + initialState.editingState = DeviceContactInfoEditingState(editingName: .personName(firstName: firstName, lastName: lastName)) + } + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((DeviceContactInfoState) -> DeviceContactInfoState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var addToExistingImpl: (() -> Void)? + var openChatImpl: ((PeerId) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? + var replaceControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? + var openUrlImpl: ((String) -> Void)? + var openAddressImpl: ((DeviceContactAddressData) -> Void)? + var inviteImpl: (([String]) -> Void)? + var dismissImpl: ((Bool) -> Void)? + + let actionsDisposable = DisposableSet() + + var displayCopyContextMenuImpl: ((DeviceContactInfoEntryTag, String) -> Void)? + + let requestCallImpl: (String) -> Void = { number in + let _ = (account.postbox.transaction { transaction -> TelegramUser? in + if let peer = subject.peer { + return transaction.getPeer(peer.id) as? TelegramUser + } + return nil + } + |> deliverOnMainQueue).start(next: { user in + if let user = user, let phone = user.phone, formatPhoneNumber(phone) == formatPhoneNumber(number) { + let callResult = account.telegramApplicationContext.callManager?.requestCall(peerId: user.id, endCurrentIfAny: false) + if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { + if currentPeerId == user.id { + account.telegramApplicationContext.navigateToCurrentCall?() + } else { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let _ = (account.postbox.transaction { transaction -> (Peer?, Peer?) in + return (transaction.getPeer(user.id), transaction.getPeer(currentPeerId)) + } |> deliverOnMainQueue).start(next: { peer, current in + if let peer = peer, let current = current { + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peer.id, endCurrentIfAny: true) + })]), nil) + } + }) + } + } + } + }) + } + + let arguments = DeviceContactInfoControllerArguments(account: account, updateEditingName: { editingName in + updateState { state in + var state = state + if let _ = state.editingState { + state.editingState = DeviceContactInfoEditingState(editingName: editingName) + } + return state + } + }, updatePhone: { id, value in + updateState { state in + var state = state + for i in 0 ..< state.phoneNumbers.count { + if state.phoneNumbers[i].id == id { + state.phoneNumbers[i].value = value + break + } + } + return state + } + }, updatePhoneLabel: { id, currentLabel in + + }, deletePhone: { id in + updateState { state in + var state = state + for i in 0 ..< state.phoneNumbers.count { + if state.phoneNumbers[i].id == id { + state.phoneNumbers.remove(at: i) + break + } + } + return state + } + }, setPhoneIdWithRevealedOptions: { id, fromId in + updateState { state in + var state = state + if (id == nil && fromId == state.phoneIdWithRevealedOptions) || (id != nil && fromId == nil) { + state.phoneIdWithRevealedOptions = id + } + return state + } + }, addPhoneNumber: { + updateState { state in + var state = state + let id = state.nextPhoneNumber + state.nextPhoneNumber += 1 + state.phoneNumbers.append(EditingPhoneNumber(id: id, label: "_$!!$_", value: "+")) + return state + } + }, performAction: { action in + switch action { + case .invite: + let inviteAction: (String) -> Void = { number in + inviteImpl?([number]) + } + if subject.contactData.basicData.phoneNumbers.count == 1 { + inviteAction(subject.contactData.basicData.phoneNumbers[0].value) + } else { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(presentationTheme: presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + var items: [ActionSheetItem] = [] + for phoneNumber in subject.contactData.basicData.phoneNumbers { + items.append(ActionSheetButtonItem(title: formatPhoneNumber(phoneNumber.value), action: { + dismissAction() + inviteAction(phoneNumber.value) + })) + } + + controller.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + case .createContact: + presentControllerImpl?(deviceContactInfoController(account: account, subject: .create(peer: subject.peer, contactData: subject.contactData, completion: { peer, contactData in + dismissImpl?(false) + if let peer = peer { + + } else { + + } + })), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + case .addToExisting: + addToExistingImpl?() + case .sendMessage: + if let peer = subject.peer { + openChatImpl?(peer.id) + } + } + }, toggleSelection: { dataId in + updateState { state in + var state = state + if state.excludedComponents.contains(dataId) { + state.excludedComponents.remove(dataId) + } else { + state.excludedComponents.insert(dataId) + } + return state + } + }, callPhone: { phoneNumer in + + }, openUrl: { url in + openUrlImpl?(url) + }, openAddress: { address in + openAddressImpl?(address) + }) + + let contactData: Signal<(Peer?, DeviceContactStableId?, DeviceContactExtendedData), NoError> + switch subject { + case let .vcard(peer, id, data): + contactData = .single((peer, id, data)) + case let .filter(peer, id, data, _): + contactData = .single((peer, id, data)) + case let .create(peer, data, _): + contactData = .single((peer, nil, data)) + } + + let previousEditingPhoneIds = Atomic?>(value: nil) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), contactData) + |> map { presentationData, state, peerAndContactData -> (ItemListControllerState, (ItemListNodeState, DeviceContactInfoEntry.ItemGenerationArguments)) in + var leftNavigationButton: ItemListNavigationButton? + switch subject { + case .vcard: + break + case .filter, .create: + leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?(true) + }) + } + + var rightNavigationButton: ItemListNavigationButton? + if case let .filter(_, _, _, completion) = subject { + let filteredData = filteredContactData(contactData: peerAndContactData.2, excludedComponents: state.excludedComponents) + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.ShareMenu_Send), style: .bold, enabled: !filteredData.basicData.phoneNumbers.isEmpty, action: { + completion(peerAndContactData.0, filteredData) + dismissImpl?(true) + }) + } else if case let .create(_, _, completion) = subject { + let filteredData = filteredContactData(contactData: peerAndContactData.2, excludedComponents: state.excludedComponents) + var filteredPhoneNumbers: [DeviceContactPhoneNumberData] = [] + for phoneNumber in state.phoneNumbers { + if !phoneNumber.value.isEmpty && phoneNumber.value != "+" { + filteredPhoneNumbers.append(DeviceContactPhoneNumberData(label: phoneNumber.label, value: phoneNumber.value)) + } + } + var composedContactData: DeviceContactExtendedData? + if let editingName = state.editingState?.editingName, case let .personName(firstName, lastName) = editingName, (!firstName.isEmpty || !lastName.isEmpty) { + composedContactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: filteredPhoneNumbers), middleName: filteredData.middleName, prefix: filteredData.prefix, suffix: filteredData.suffix, organization: filteredData.organization, jobTitle: filteredData.jobTitle, department: filteredData.department, emailAddresses: filteredData.emailAddresses, urls: filteredData.urls, addresses: filteredData.addresses, birthdayDate: filteredData.birthdayDate, socialProfiles: filteredData.socialProfiles, instantMessagingProfiles: filteredData.instantMessagingProfiles) + } + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Compose_Create), style: .bold, enabled: !filteredPhoneNumbers.isEmpty && composedContactData != nil, action: { + if let composedContactData = composedContactData { + let _ = (account.telegramApplicationContext.contactDataManager.createContactWithData(composedContactData) + |> deliverOnMainQueue).start(next: { contactIdAndData in + dismissImpl?(true) + }) + } + }) + } + + var editingPhones = false + var selecting = false + switch subject { + case .vcard: + break + case .filter: + selecting = true + case .create: + selecting = true + editingPhones = true + } + if case .filter = subject { + selecting = true + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.UserInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) + + let editingPhoneIds = Set(state.phoneNumbers.map({ $0.id })) + let previousPhoneIds = previousEditingPhoneIds.swap(editingPhoneIds) + let insertedPhoneIds = editingPhoneIds.subtracting(previousPhoneIds ?? Set()) + var insertedPhoneId: Int64? + if insertedPhoneIds.count == 1, let id = insertedPhoneIds.first { + for phoneNumber in state.phoneNumbers { + if phoneNumber.id == id { + if phoneNumber.value.isEmpty || phoneNumber.value == "+" { + insertedPhoneId = id + } + break + } + } + } + + var focusItemTag: ItemListItemTag? + if let insertedPhoneId = insertedPhoneId { + focusItemTag = DeviceContactInfoEntryTag.editingPhone(insertedPhoneId) + } + + let listState = ItemListNodeState(entries: deviceContactInfoEntries(account: account, presentationData: presentationData, peer: peerAndContactData.0, contactData: peerAndContactData.2, isContact: peerAndContactData.1 != nil, state: state, selecting: selecting, editingPhoneNumbers: editingPhones), style: .plain, focusItemTag: focusItemTag) + + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = DeviceContactInfoController(account: account, state: signal) + addToExistingImpl = { [weak controller] in + guard let controller = controller else { + return + } + addContactToExisting(account: account, parentController: controller, contactData: subject.contactData, completion: { peer, contactId, contactData in + replaceControllerImpl?(deviceContactInfoController(account: account, subject: .vcard(peer, contactId, contactData))) + }) + } + openChatImpl = { [weak controller] peerId in + if let navigationController = (controller?.navigationController as? NavigationController) { + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) + } + } + pushControllerImpl = { [weak controller] value in + (controller?.navigationController as? NavigationController)?.pushViewController(value) + } + replaceControllerImpl = { [weak controller] value in + (controller?.navigationController as? NavigationController)?.replaceTopController(value, animated: true) + } + presentControllerImpl = { [weak controller] value, presentationArguments in + controller?.present(value, in: .window(.root), with: presentationArguments) + } + dismissImpl = { [weak controller] animated in + if let navigationController = controller?.navigationController as? NavigationController { + let _ = navigationController.popViewController(animated: animated) + } else { + controller?.dismiss() + } + } + inviteImpl = { [weak controller] numbers in + controller?.inviteContact(presentationData: account.telegramApplicationContext.currentPresentationData.with { $0 }, numbers: numbers) + } + openAddressImpl = { [weak controller] address in + guard let _ = controller else { + return + } + + } + openUrlImpl = { [weak controller] url in + guard let controller = controller else { + return + } + openExternalUrl(account: account, url: url, presentationData: account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: account.telegramApplicationContext, navigationController: controller.navigationController as? NavigationController, dismissInput: { [weak controller] in + controller?.view.endEditing(true) + }) + } + + displayCopyContextMenuImpl = { [weak controller] tag, value in + if let strongController = controller { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + var resultItemNode: ListViewItemNode? + let _ = strongController.frameForItemNode({ itemNode in + if let itemNode = itemNode as? ItemListTextWithLabelItemNode { + if let itemTag = itemNode.tag as? DeviceContactInfoEntryTag { + if itemTag == tag { + resultItemNode = itemNode + return true + } + } + } + return false + }) + if let resultItemNode = resultItemNode { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = value + })]) + strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in + if let strongController = controller, let resultItemNode = resultItemNode { + return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds) + } else { + return nil + } + })) + + } + } + } + + return controller +} + +private func addContactToExisting(account: Account, parentController: ViewController, contactData: DeviceContactExtendedData, completion: @escaping (Peer?, DeviceContactStableId, DeviceContactExtendedData) -> Void) { + let contactsController = ContactSelectionController(account: account, title: { $0.Contacts_Title }, displayDeviceContacts: true) + parentController.present(contactsController, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let _ = (contactsController.result + |> deliverOnMainQueue).start(next: { peer in + if let peer = peer { + let dataSignal: Signal<(Peer?, DeviceContactStableId?), NoError> + switch peer { + case let .peer(contact, _): + guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + return + } + dataSignal = account.telegramApplicationContext.contactDataManager.basicData() + |> take(1) + |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactStableId?), NoError> in + var stableId: String? + let queryPhoneNumber = formatPhoneNumber(phoneNumber) + outer: for (id, data) in basicData { + for phoneNumber in data.phoneNumbers { + if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber { + stableId = id + break outer + } + } + } + return .single((contact, stableId)) + } + case let .deviceContact(id, _): + dataSignal = .single((nil, id)) + } + let _ = (dataSignal + |> deliverOnMainQueue).start(next: { peer, stableId in + guard let stableId = stableId else { + parentController.present(deviceContactInfoController(account: account, subject: .create(peer: peer, contactData: contactData, completion: { peer, contactData in + + })), in: .window(.root)) + return + } + let _ = (account.telegramApplicationContext.contactDataManager.appendContactData(contactData, to: stableId) + |> deliverOnMainQueue).start(next: { contactData in + guard let contactData = contactData else { + return + } + let _ = (account.postbox.contactPeersView(accountPeerId: nil, includePresences: false) + |> take(1) + |> deliverOnMainQueue).start(next: { view in + let phones = Set(contactData.basicData.phoneNumbers.map { + return formatPhoneNumber($0.value) + }) + var foundPeer: Peer? + for peer in view.peers { + if let user = peer as? TelegramUser, let phone = user.phone { + let phone = formatPhoneNumber(phone) + if phones.contains(phone) { + foundPeer = peer + break + } + } + } + completion(foundPeer, stableId, contactData) + }) + }) + }) + } + }) +} diff --git a/TelegramUI/EditAccessoryPanelNode.swift b/TelegramUI/EditAccessoryPanelNode.swift index b350a2fbe7..326c38299a 100644 --- a/TelegramUI/EditAccessoryPanelNode.swift +++ b/TelegramUI/EditAccessoryPanelNode.swift @@ -265,18 +265,18 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { } override func updateState(size: CGSize, interfaceState: ChatPresentationInterfaceState) { - let editMedia = interfaceState.editMessageState?.media + let editMediaReference = interfaceState.editMessageState?.mediaReference var updatedEditMedia = false - if let currentEditMediaReference = self.currentEditMediaReference, let editMedia = editMedia { - if !currentEditMediaReference.media.isEqual(editMedia) { + if let currentEditMediaReference = self.currentEditMediaReference, let editMediaReference = editMediaReference { + if !currentEditMediaReference.media.isEqual(editMediaReference.media) { updatedEditMedia = true } - } else if (editMedia != nil) != (self.currentEditMediaReference != nil) { + } else if (editMediaReference != nil) != (self.currentEditMediaReference != nil) { updatedEditMedia = true } if updatedEditMedia { - if let editMedia = editMedia { - self.currentEditMediaReference = .standalone(media: editMedia) + if let editMediaReference = editMediaReference { + self.currentEditMediaReference = editMediaReference } else { self.currentEditMediaReference = nil } diff --git a/TelegramUI/EditableTokenListNode.swift b/TelegramUI/EditableTokenListNode.swift index 22e5361b1f..df90ef76a9 100644 --- a/TelegramUI/EditableTokenListNode.swift +++ b/TelegramUI/EditableTokenListNode.swift @@ -94,6 +94,7 @@ private final class CaretIndicatorNode: ASImageNode { final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { private let theme: EditableTokenListNodeTheme + private let scrollNode: ASScrollNode private let placeholderNode: ASTextNode private var tokenNodes: [TokenNode] = [] private let separatorNode: ASDisplayNode @@ -107,6 +108,8 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { init(theme: EditableTokenListNodeTheme, placeholder: String) { self.theme = theme + self.scrollNode = ASScrollNode() + self.placeholderNode = ASTextNode() self.placeholderNode.isLayerBacked = true self.placeholderNode.maximumNumberOfLines = 1 @@ -149,12 +152,14 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { if let strongSelf = self { if let selectedTokenId = strongSelf.selectedTokenId { strongSelf.deleteToken?(selectedTokenId) + strongSelf.updateSelectedTokenId(nil) } else if let tokenNode = strongSelf.tokenNodes.last { - strongSelf.selectedTokenId = tokenNode.token.id - tokenNode.isSelected = true + strongSelf.updateSelectedTokenId(tokenNode.token.id) } } } + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } func updateLayout(tokens: [EditableTokenListToken], width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { @@ -283,6 +288,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { @objc func textFieldChanged(_ textField: UITextField) { self.placeholderNode.isHidden = textField.text != nil && !textField.text!.isEmpty + self.updateSelectedTokenId(nil) self.textUpdated?(textField.text ?? "") } @@ -302,4 +308,26 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.textFieldNode.textField.text = text self.textFieldChanged(self.textFieldNode.textField) } + + private func updateSelectedTokenId(_ id: AnyHashable?) { + self.selectedTokenId = id + for tokenNode in self.tokenNodes { + tokenNode.isSelected = id == tokenNode.token.id + } + if id != nil { + self.textFieldNode.textField.becomeFirstResponder() + } + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let point = recognizer.location(in: self.view) + for tokenNode in self.tokenNodes { + if tokenNode.frame.contains(point) { + self.updateSelectedTokenId(tokenNode.token.id) + break + } + } + } + } } diff --git a/TelegramUI/FFMpegMediaVideoFrameDecoder.swift b/TelegramUI/FFMpegMediaVideoFrameDecoder.swift index 9d3adebd04..2d965ff3fa 100644 --- a/TelegramUI/FFMpegMediaVideoFrameDecoder.swift +++ b/TelegramUI/FFMpegMediaVideoFrameDecoder.swift @@ -108,9 +108,9 @@ final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { ioSurfaceProperties["IOSurfaceIsGlobal"] = true as NSNumber var options: [String: Any] = [kCVPixelBufferBytesPerRowAlignmentKey as String: frame.pointee.linesize.0 as NSNumber] - if #available(iOSApplicationExtension 9.0, *) { + /*if #available(iOSApplicationExtension 9.0, *) { options[kCVPixelBufferOpenGLESTextureCacheCompatibilityKey as String] = true as NSNumber - } + }*/ options[kCVPixelBufferIOSurfacePropertiesKey as String] = ioSurfaceProperties CVPixelBufferCreate(kCFAllocatorDefault, @@ -138,17 +138,42 @@ final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { dstPlane[2 * i + 1] = frame.pointee.data.2![i] } - CVPixelBufferLockBaseAddress(pixelBuffer, []) + let status = CVPixelBufferLockBaseAddress(pixelBuffer, []) + if status != kCVReturnSuccess { + return nil + } let bytePerRowY = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0) let bytesPerRowUV = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1) - var base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) - memcpy(base, frame.pointee.data.0!, bytePerRowY * Int(frame.pointee.height)) + var base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0)! + if bytePerRowY == frame.pointee.linesize.0 { + memcpy(base, frame.pointee.data.0!, bytePerRowY * Int(frame.pointee.height)) + } else { + var dest = base + var src = frame.pointee.data.0! + let linesize = Int(frame.pointee.linesize.0) + for _ in 0 ..< Int(frame.pointee.height) { + memcpy(dest, src, linesize) + dest = dest.advanced(by: bytePerRowY) + src = src.advanced(by: linesize) + } + } - base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) - memcpy(base, dstPlane, bytesPerRowUV * Int(frame.pointee.height) / 2) + base = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1)! + if bytesPerRowUV == frame.pointee.linesize.1 * 2 { + memcpy(base, dstPlane, bytesPerRowUV * Int(frame.pointee.height) / 2) + } else { + var dest = base + var src = dstPlane + let linesize = Int(frame.pointee.linesize.1) * 2 + for _ in 0 ..< Int(frame.pointee.height) / 2 { + memcpy(dest, src, linesize) + dest = dest.advanced(by: bytesPerRowUV) + src = src.advanced(by: linesize) + } + } CVPixelBufferUnlockBaseAddress(pixelBuffer, []) diff --git a/TelegramUI/FeedGroupingController.swift b/TelegramUI/FeedGroupingController.swift index 8690f25b43..cde6f0b0be 100644 --- a/TelegramUI/FeedGroupingController.swift +++ b/TelegramUI/FeedGroupingController.swift @@ -25,7 +25,7 @@ final class FeedGroupingController: ViewController { self.title = "Grouping" - /*let rightButton = ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) + /*let rightButton = ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) self.navigationItem.setRightBarButton(rightButton.buttonItem, animated: false)*/ } diff --git a/TelegramUI/FetchMediaUtils.swift b/TelegramUI/FetchMediaUtils.swift index 0231a59475..2834f365c5 100644 --- a/TelegramUI/FetchMediaUtils.swift +++ b/TelegramUI/FetchMediaUtils.swift @@ -19,6 +19,14 @@ func messageMediaFileCancelInteractiveFetch(account: Account, messageId: Message account.telegramApplicationContext.fetchManager.cancelInteractiveFetches(category: .file, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: file.resource) } +func messageMediaImageInteractiveFetched(account: Account, message: Message, image: TelegramMediaImage, resource: MediaResource) -> Signal { + return account.telegramApplicationContext.fetchManager.interactivelyFetched(category: .image, location: .chat(message.id.peerId), locationKey: .messageId(message.id), resourceReference: AnyMediaReference.message(message: MessageReference(message), media: image).resourceReference(resource), statsCategory: .image, elevatedPriority: false, userInitiated: true) +} + +func messageMediaImageCancelInteractiveFetch(account: Account, messageId: MessageId, image: TelegramMediaImage, resource: MediaResource) { + account.telegramApplicationContext.fetchManager.cancelInteractiveFetches(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: resource) +} + func messageMediaFileStatus(account: Account, messageId: MessageId, file: TelegramMediaFile) -> Signal { return account.telegramApplicationContext.fetchManager.fetchStatus(category: .file, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: file.resource) } diff --git a/TelegramUI/FixSearchableListNodeScrolling.swift b/TelegramUI/FixSearchableListNodeScrolling.swift new file mode 100644 index 0000000000..ddfca1b98c --- /dev/null +++ b/TelegramUI/FixSearchableListNodeScrolling.swift @@ -0,0 +1,29 @@ +import Foundation +import AsyncDisplayKit +import Display + +func fixSearchableListNodeScrolling(_ listNode: ListView) { + var searchItemNode: ListViewItemNode? + var nextItemNode: ListViewItemNode? + + listNode.forEachItemNode({ itemNode in + if let itemNode = itemNode as? ChatListSearchItemNode { + searchItemNode = itemNode + } else if searchItemNode != nil && nextItemNode == nil { + nextItemNode = itemNode as? ListViewItemNode + } + }) + + if let searchItemNode = searchItemNode { + let itemFrame = searchItemNode.apparentFrame + if itemFrame.contains(CGPoint(x: 0.0, y: listNode.insets.top)) { + if itemFrame.minY + itemFrame.height * 0.6 < listNode.insets.top { + if let nextItemNode = nextItemNode { + listNode.ensureItemNodeVisibleAtTopInset(nextItemNode) + } + } else { + listNode.ensureItemNodeVisibleAtTopInset(searchItemNode) + } + } + } +} diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 46c0f717a2..525ec7aac0 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -18,7 +18,7 @@ private final class GroupInfoArguments { let presentController: (ViewController, ViewControllerPresentationArguments) -> Void let changeNotificationMuteSettings: () -> Void let changeNotificationSoundSettings: () -> Void - let togglePreHistory: (Bool) -> Void + let openPreHistory: () -> Void let openSharedMedia: () -> Void let openAdminManagement: () -> Void let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void @@ -35,7 +35,7 @@ private final class GroupInfoArguments { let aboutLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void let openStickerPackSetup: () -> Void - init(account: Account, peerId: PeerId, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, togglePreHistory: @escaping (Bool) -> Void, openSharedMedia: @escaping () -> Void, openAdminManagement: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, promotePeer: @escaping (RenderedChannelParticipant) -> Void, restrictPeer: @escaping (RenderedChannelParticipant) -> Void, removePeer: @escaping (PeerId) -> Void, convertToSupergroup: @escaping () -> Void, leave: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, openStickerPackSetup: @escaping () -> Void) { + init(account: Account, peerId: PeerId, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openPreHistory: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdminManagement: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, promotePeer: @escaping (RenderedChannelParticipant) -> Void, restrictPeer: @escaping (RenderedChannelParticipant) -> Void, removePeer: @escaping (PeerId) -> Void, convertToSupergroup: @escaping () -> Void, leave: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, openStickerPackSetup: @escaping () -> Void) { self.account = account self.peerId = peerId self.avatarAndNameInfoContext = avatarAndNameInfoContext @@ -45,7 +45,7 @@ private final class GroupInfoArguments { self.presentController = presentController self.changeNotificationMuteSettings = changeNotificationMuteSettings self.changeNotificationSoundSettings = changeNotificationSoundSettings - self.togglePreHistory = togglePreHistory + self.openPreHistory = openPreHistory self.openSharedMedia = openSharedMedia self.openAdminManagement = openAdminManagement self.updateEditingName = updateEditingName @@ -138,7 +138,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { case stickerPack(PresentationTheme, String, String) case adminManagement(PresentationTheme, String) case groupTypeSetup(PresentationTheme, String, String) - case preHistory(PresentationTheme, String, Bool) + case preHistory(PresentationTheme, String, String) case groupDescriptionSetup(PresentationTheme, String, String) case groupManagementInfoLabel(PresentationTheme, String, String) case membersAdmins(PresentationTheme, String, String) @@ -460,8 +460,8 @@ private enum GroupInfoEntry: ItemListNodeEntry { arguments.openStickerPackSetup() }) case let .preHistory(theme, title, value): - return ItemListSwitchItem(theme: theme, title: title, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in - arguments.togglePreHistory(value) + return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { + arguments.openPreHistory() }) case let .sharedMedia(theme, title): return ItemListDisclosureItem(theme: theme, title: title, label: "", sectionId: self.section, style: .blocks, action: { @@ -749,7 +749,7 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa if isCreator { entries.append(GroupInfoEntry.groupTypeSetup(presentationData.theme, presentationData.strings.GroupInfo_GroupType, isPublic ? presentationData.strings.Channel_Setup_TypePublic : presentationData.strings.Channel_Setup_TypePrivate)) if !isPublic, let cachedData = view.cachedData as? CachedChannelData { - entries.append(GroupInfoEntry.preHistory(presentationData.theme, "Group History For New Members", cachedData.flags.contains(.preHistoryEnabled))) + entries.append(GroupInfoEntry.preHistory(presentationData.theme, presentationData.strings.GroupInfo_GroupHistory, cachedData.flags.contains(.preHistoryEnabled) ? presentationData.strings.GroupInfo_GroupHistoryVisible : presentationData.strings.GroupInfo_GroupHistoryHidden)) } } if canEditGroupInfo { @@ -1101,9 +1101,6 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl actionsDisposable.add(updateAvatarDisposable) let currentAvatarMixin = Atomic(value: nil) - let updatePreHistoryDisposable = MetaDisposable() - actionsDisposable.add(updatePreHistoryDisposable) - let navigateDisposable = MetaDisposable() actionsDisposable.add(navigateDisposable) @@ -1276,8 +1273,8 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl }) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) - }, togglePreHistory: { value in - updatePreHistoryDisposable.set(updateChannelHistoryAvailabilitySettingsInteractively(postbox: account.postbox, network: account.network, peerId: peerId, historyAvailableForNewMembers: value).start()) + }, openPreHistory: { + presentControllerImpl?(groupPreHistorySetupController(account: account, peerId: peerId), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, openSharedMedia: { if let controller = peerSharedMediaController(account: account, peerId: peerId) { pushControllerImpl?(controller) @@ -1318,9 +1315,9 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl inviteByLinkImpl?() })) - let contactsController = ContactSelectionController(account: account, title: { $0.GroupInfo_AddParticipantTitle }, options: options, confirmation: { peerId in - if let confirmationImpl = confirmationImpl { - return confirmationImpl(peerId) + let contactsController = ContactSelectionController(account: account, title: { $0.GroupInfo_AddParticipantTitle }, options: options, confirmation: { peer in + if let confirmationImpl = confirmationImpl, case let .peer(peer, _) = peer { + return confirmationImpl(peer.id) } else { return .single(false) } @@ -1349,8 +1346,9 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl let addMember = contactsController.result |> deliverOnMainQueue - |> mapToSignal { memberId -> Signal in - if let memberId = memberId { + |> mapToSignal { memberPeer -> Signal in + if let memberPeer = memberPeer, case let .peer(selectedPeer, _) = memberPeer { + let memberId = selectedPeer.id if peerId.namespace == Namespaces.Peer.CloudChannel { return account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.addMember(account: account, peerId: peerId, memberId: memberId) } @@ -1651,6 +1649,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl (controller?.navigationController as? NavigationController)?.pushViewController(value) } presentControllerImpl = { [weak controller] value, presentationArguments in + controller?.view.endEditing(true) controller?.present(value, in: .window(.root), with: presentationArguments) } popToRootImpl = { [weak controller] in diff --git a/TelegramUI/GroupInfoSearchNavigationContentNode.swift b/TelegramUI/GroupInfoSearchNavigationContentNode.swift index 15ef2a0048..060ec77aee 100644 --- a/TelegramUI/GroupInfoSearchNavigationContentNode.swift +++ b/TelegramUI/GroupInfoSearchNavigationContentNode.swift @@ -22,7 +22,7 @@ final class GroupInfoSearchNavigationContentNode: NavigationBarContentNode, Item self.cancel = cancel - self.searchBar = SearchBarNode(theme: theme, strings: strings) + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme), strings: strings) let placeholderText = strings.Conversation_SearchByName_Placeholder self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputPlaceholderTextColor) diff --git a/TelegramUI/GroupPreHistorySetupController.swift b/TelegramUI/GroupPreHistorySetupController.swift new file mode 100644 index 0000000000..2d777e7b6e --- /dev/null +++ b/TelegramUI/GroupPreHistorySetupController.swift @@ -0,0 +1,176 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class GroupPreHistorySetupArguments { + let toggle: (Bool) -> Void + + init(toggle: @escaping (Bool) -> Void) { + self.toggle = toggle + } +} + +private enum GroupPreHistorySetupSection: Int32 { + case info +} + +private enum GroupPreHistorySetupEntry: ItemListNodeEntry { + case header(PresentationTheme, String) + case visible(PresentationTheme, String, Bool) + case hidden(PresentationTheme, String, Bool) + case info(PresentationTheme, String) + + var section: ItemListSectionId { + return GroupPreHistorySetupSection.info.rawValue + } + + var stableId: Int32 { + switch self { + case .header: + return 0 + case .visible: + return 1 + case .hidden: + return 2 + case .info: + return 3 + } + } + + static func ==(lhs: GroupPreHistorySetupEntry, rhs: GroupPreHistorySetupEntry) -> Bool { + switch lhs { + case let .header(lhsTheme, lhsText): + if case let .header(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .visible(lhsTheme, lhsText, lhsValue): + if case let .visible(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .hidden(lhsTheme, lhsText, lhsValue): + if case let .hidden(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .info(lhsTheme, lhsText): + if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + } + } + + static func <(lhs: GroupPreHistorySetupEntry, rhs: GroupPreHistorySetupEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: GroupPreHistorySetupArguments) -> ListViewItem { + switch self { + case let .header(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .visible(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.toggle(true) + }) + case let .hidden(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.toggle(false) + }) + case let .info(theme, text): + return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + } + } +} + +private struct GroupPreHistorySetupState: Equatable { + var changedValue: Bool? + var applyingSetting: Bool = false +} + +private func groupPreHistorySetupEntries(presentationData: PresentationData, defaultValue: Bool, state: GroupPreHistorySetupState) -> [GroupPreHistorySetupEntry] { + var entries: [GroupPreHistorySetupEntry] = [] + let value = state.changedValue ?? defaultValue + entries.append(.header(presentationData.theme, presentationData.strings.Group_Setup_HistoryHeader)) + entries.append(.visible(presentationData.theme, presentationData.strings.Group_Setup_HistoryVisible, value)) + entries.append(.hidden(presentationData.theme, presentationData.strings.Group_Setup_HistoryHidden, !value)) + entries.append(.info(presentationData.theme, value ? presentationData.strings.Group_Setup_HistoryVisibleHelp : presentationData.strings.Group_Setup_HistoryHiddenHelp)) + + return entries +} + +public func groupPreHistorySetupController(account: Account, peerId: PeerId) -> ViewController { + let statePromise = ValuePromise(GroupPreHistorySetupState(), ignoreRepeated: true) + let stateValue = Atomic(value: GroupPreHistorySetupState()) + let updateState: ((GroupPreHistorySetupState) -> GroupPreHistorySetupState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var dismissImpl: (() -> Void)? + + let actionsDisposable = DisposableSet() + + let applyDisposable = MetaDisposable() + actionsDisposable.add(applyDisposable) + + let arguments = GroupPreHistorySetupArguments(toggle: { value in + updateState { state in + var state = state + state.changedValue = value + return state + } + }) + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId)) + |> deliverOnMainQueue + |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, GroupPreHistorySetupEntry.ItemGenerationArguments)) in + let defaultValue: Bool = (view.cachedData as? CachedChannelData)?.flags.contains(.preHistoryEnabled) ?? false + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }) + var rightNavigationButton: ItemListNavigationButton? + if state.applyingSetting { + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) + } else { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + var value: Bool? + updateState { state in + var state = state + state.applyingSetting = true + value = state.changedValue + return state + } + if let value = value, value != defaultValue { + applyDisposable.set((updateChannelHistoryAvailabilitySettingsInteractively(postbox: account.postbox, network: account.network, peerId: peerId, historyAvailableForNewMembers: value) + |> deliverOnMainQueue).start(completed: { + dismissImpl?() + })) + } else { + dismissImpl?() + } + }) + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Group_Setup_HistoryTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: groupPreHistorySetupEntries(presentationData: presentationData, defaultValue: defaultValue, state: state), style: .blocks) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(account: account, state: signal) + dismissImpl = { [weak controller] in + controller?.view.endEditing(true) + controller?.dismiss() + } + return controller +} diff --git a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift index 1317a59833..89cbd709f2 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -3,6 +3,7 @@ import AsyncDisplayKit import Postbox import TelegramCore import Display +import SwiftSignalKit private struct ChatContextResultStableId: Hashable { let result: ChatContextResult @@ -41,16 +42,28 @@ private struct HorizontalListContextResultsChatInputContextPanelTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] + let entryCount: Int + let hasMore: Bool } -private func preparedTransition(from fromEntries: [HorizontalListContextResultsChatInputContextPanelEntry], to toEntries: [HorizontalListContextResultsChatInputContextPanelEntry], account: Account, resultSelected: @escaping (ChatContextResult) -> Void) -> HorizontalListContextResultsChatInputContextPanelTransition { +private final class HorizontalListContextResultsOpaqueState { + let entryCount: Int + let hasMore: Bool + + init(entryCount: Int, hasMore: Bool) { + self.entryCount = entryCount + self.hasMore = hasMore + } +} + +private func preparedTransition(from fromEntries: [HorizontalListContextResultsChatInputContextPanelEntry], to toEntries: [HorizontalListContextResultsChatInputContextPanelEntry], hasMore: Bool, account: Account, resultSelected: @escaping (ChatContextResult) -> Void) -> HorizontalListContextResultsChatInputContextPanelTransition { 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, resultSelected: resultSelected), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, resultSelected: resultSelected), directionHint: nil) } - return HorizontalListContextResultsChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) + return HorizontalListContextResultsChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates, entryCount: toEntries.count, hasMore: hasMore) } final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputContextPanelNode { @@ -59,8 +72,11 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont private let listView: ListView private let separatorNode: ASDisplayNode - private var currentResults: ChatContextResultCollection? + private var currentExternalResults: ChatContextResultCollection? + private var currentProcessedResults: ChatContextResultCollection? private var currentEntries: [HorizontalListContextResultsChatInputContextPanelEntry]? + private var isLoadingMore = false + private let loadMoreDisposable = MetaDisposable() private var enqueuedTransitions: [(HorizontalListContextResultsChatInputContextPanelTransition, Bool)] = [] private var hasValidLayout = false @@ -87,6 +103,20 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont self.addSubnode(self.listView) self.addSubnode(self.separatorNode) + + self.listView.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in + if let strongSelf = self, let state = opaqueTransactionState as? HorizontalListContextResultsOpaqueState { + if let visible = displayedRange.visibleRange { + if state.hasMore && visible.lastIndex <= state.entryCount - 10 { + strongSelf.loadMore() + } + } + } + } + } + + deinit { + self.loadMoreDisposable.dispose() } override func didLoad() { @@ -104,11 +134,19 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont var selectedItemNodeAndContent: (ASDisplayNode, PeekControllerContent)? strongSelf.listView.forEachItemNode { itemNode in if itemNode.frame.contains(convertedPoint), let itemNode = itemNode as? HorizontalListContextResultsChatInputPanelItemNode, let item = itemNode.item { - selectedItemNodeAndContent = (itemNode, ChatContextResultPeekContent(account: item.account, contextResult: item.result, menu: [ - PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, action: { - item.resultSelected(item.result) - }) - ])) + if case let .internalReference(internalReference) = item.result, let file = internalReference.file, file.isSticker { + selectedItemNodeAndContent = (itemNode, StickerPreviewPeekContent(account: item.account, item: .found(FoundStickerItem(file: file, stringRepresentations: [])), menu: [ + PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, action: { + item.resultSelected(item.result) + }) + ])) + } else { + selectedItemNodeAndContent = (itemNode, ChatContextResultPeekContent(account: item.account, contextResult: item.result, menu: [ + PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, action: { + item.resultSelected(item.result) + }) + ])) + } } } return .single(selectedItemNodeAndContent) @@ -127,7 +165,42 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont } func updateResults(_ results: ChatContextResultCollection) { - self.currentResults = results + if self.currentExternalResults == results { + return + } + self.currentExternalResults = results + self.currentProcessedResults = results + + self.isLoadingMore = false + self.loadMoreDisposable.set(nil) + self.updateInternalResults(results) + } + + private func loadMore() { + guard !self.isLoadingMore, let currentProcessedResults = self.currentProcessedResults, let nextOffset = currentProcessedResults.nextOffset else { + return + } + self.isLoadingMore = true + self.loadMoreDisposable.set((requestChatContextResults(account: self.account, botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, location: .single(currentProcessedResults.geoPoint), offset: nextOffset) + |> deliverOnMainQueue).start(next: { [weak self] nextResults in + guard let strongSelf = self, let nextResults = nextResults else { + return + } + strongSelf.isLoadingMore = false + var results: [ChatContextResult] = [] + for result in currentProcessedResults.results { + results.append(result) + } + for result in nextResults.results { + results.append(result) + } + let mergedResults = ChatContextResultCollection(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, geoPoint: currentProcessedResults.geoPoint, queryId: nextResults.queryId, nextOffset: nextResults.nextOffset, presentation: currentProcessedResults.presentation, switchPeer: currentProcessedResults.switchPeer, results: results, cacheTimeout: currentProcessedResults.cacheTimeout) + strongSelf.currentProcessedResults = mergedResults + strongSelf.updateInternalResults(mergedResults) + })) + } + + private func updateInternalResults(_ results: ChatContextResultCollection) { var entries: [HorizontalListContextResultsChatInputContextPanelEntry] = [] var index = 0 var resultIds = Set() @@ -143,7 +216,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont } let firstTime = self.currentEntries == nil - let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, account: self.account, resultSelected: { [weak self] result in + let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, hasMore: results.nextOffset != nil, account: self.account, resultSelected: { [weak self] result in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.sendContextResult(results, result) } @@ -175,7 +248,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont //options.insert(.AnimateCrossfade) } - self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: HorizontalListContextResultsOpaqueState(entryCount: transition.entryCount, hasMore: transition.hasMore), completion: { [weak self] _ in if let strongSelf = self, firstTime { let position = strongSelf.listView.position strongSelf.listView.isHidden = false diff --git a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift index fc2bec2016..a0c168b384 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift @@ -195,7 +195,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode var videoFile: TelegramMediaFile? var imageDimensions: CGSize? switch item.result { - case let .externalReference(_, type, title, _, url, content, thumbnail, _): + case let .externalReference(_, _, type, title, _, url, content, thumbnail, _): if let content = content { imageResource = content.resource } else if let thumbnail = thumbnail { @@ -203,10 +203,10 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode } imageDimensions = content?.dimensions if type == "gif", let thumbnailResource = imageResource, let content = content, let dimensions = content.dimensions { - videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), reference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) + videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) imageResource = nil } - case let .internalReference(_, _, title, _, image, file, _): + case let .internalReference(_, _, _, title, _, image, file, _): if let image = image { if let largestRepresentation = largestImageRepresentation(image.representations) { imageDimensions = largestRepresentation.dimensions @@ -274,7 +274,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode updateImageSignal = chatMessageSticker(account: item.account, file: stickerFile, small: false) } else { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0), resource: imageResource) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil, partialReference: nil) updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photoReference: .standalone(media: tmpImage)) } } else { diff --git a/TelegramUI/HorizontalStickerGridItem.swift b/TelegramUI/HorizontalStickerGridItem.swift index 0dc65e61ce..22f9a9e1f7 100644 --- a/TelegramUI/HorizontalStickerGridItem.swift +++ b/TelegramUI/HorizontalStickerGridItem.swift @@ -96,7 +96,7 @@ final class HorizontalStickerGridItemNode: GridItemNode { if let (_, _, mediaDimensions) = self.currentState { let imageSize = mediaDimensions.aspectFitted(boundingSize) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() let imageFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: CGSize(width: imageSize.width, height: imageSize.height)) self.imageNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: imageSize.width, height: imageSize.height)) self.imageNode.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) diff --git a/TelegramUI/InstantImageGalleryItem.swift b/TelegramUI/InstantImageGalleryItem.swift index 8318cdcf40..bb58304593 100644 --- a/TelegramUI/InstantImageGalleryItem.swift +++ b/TelegramUI/InstantImageGalleryItem.swift @@ -127,6 +127,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { } } self.accountAndMedia = (account, imageReference.abstract) + self.footerContentNode.setShareMedia(imageReference.abstract) } func setFile(account: Account, fileReference: FileMediaReference) { @@ -141,6 +142,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { } } self.accountAndMedia = (account, fileReference.abstract) + self.footerContentNode.setShareMedia(fileReference.abstract) } override func animateIn(from node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void) { diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index e3b2b06051..a3b40f17ff 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -13,6 +13,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private var strings: PresentationStrings private var timeFormat: PresentationTimeFormat private var theme: InstantPageTheme? + private var manualThemeOverride: InstantPageThemeType? private let getNavigationController: () -> NavigationController? private let present: (ViewController, Any?) -> Void private let pushController: (ViewController) -> Void @@ -48,13 +49,19 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private let resolveUrlDisposable = MetaDisposable() private let loadWebpageDisposable = MetaDisposable() + private var themeReferenceDate: Date? + init(account: Account, settings: InstantPagePresentationSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, timeFormat: PresentationTimeFormat, statusBar: StatusBar, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) { self.account = account self.presentationTheme = presentationTheme self.timeFormat = timeFormat self.strings = strings self.settings = settings - self.theme = settings.flatMap { return instantPageThemeForSettingsAndTime(presentationTheme: presentationTheme, settings: $0, time: Date()) } + let themeReferenceDate = Date() + self.themeReferenceDate = themeReferenceDate + self.theme = settings.flatMap { settings in + return instantPageThemeForType(instantPageThemeTypeForSettingsAndTime(presentationTheme: presentationTheme, settings: settings, time: themeReferenceDate), settings: settings) + } self.statusBar = statusBar self.getNavigationController = getNavigationController @@ -112,11 +119,20 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { let previousSettings = self.settings var updateLayout = previousSettings == nil + if let previousSettings = previousSettings { + if previousSettings.themeType != settings.themeType { + self.themeReferenceDate = nil + } + } + self.settings = settings - let theme = instantPageThemeForSettingsAndTime(presentationTheme: self.presentationTheme, settings: settings, time: Date()) + let themeType = instantPageThemeTypeForSettingsAndTime(presentationTheme: self.presentationTheme, settings: settings, time: self.themeReferenceDate) + let theme = instantPageThemeForType(themeType, settings: settings) self.theme = theme self.strings = strings + self.settingsNode?.updateSettingsAndCurrentThemeType(settings: settings, type: themeType) + var animated = false if let previousSettings = previousSettings { if previousSettings.themeType != settings.themeType || previousSettings.autoNightMode != settings.autoNightMode { @@ -714,7 +730,9 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } })) } else { - openExternalUrl(account: strongSelf.account, url: externalUrl, presentationData: strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: strongSelf.getNavigationController()) + openExternalUrl(account: strongSelf.account, url: externalUrl, presentationData: strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: strongSelf.getNavigationController(), dismissInput: { + self?.view.endEditing(true) + }) } default: openResolvedUrl(result, account: strongSelf.account, navigationController: strongSelf.getNavigationController(), openPeer: { peerId, navigation in @@ -737,6 +755,8 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } }, present: { c, a in self?.present(c, a) + }, dismissInput: { + self?.view.endEditing(true) }) } } @@ -816,7 +836,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { return } if self.settingsNode == nil { - let settingsNode = InstantPageSettingsNode(strings: self.strings, settings: settings, applySettings: { [weak self] settings in + let settingsNode = InstantPageSettingsNode(strings: self.strings, settings: settings, currentThemeType: instantPageThemeTypeForSettingsAndTime(presentationTheme: self.presentationTheme, settings: settings, time: self.themeReferenceDate), applySettings: { [weak self] settings in if let strongSelf = self { strongSelf.update(settings: settings, strings: strongSelf.strings) let _ = updateInstantPagePresentationSettingsInteractively(postbox: strongSelf.account.postbox, { _ in diff --git a/TelegramUI/InstantPageGalleryFooterContentNode.swift b/TelegramUI/InstantPageGalleryFooterContentNode.swift index 35e2a18a4b..1ce1dc1197 100644 --- a/TelegramUI/InstantPageGalleryFooterContentNode.swift +++ b/TelegramUI/InstantPageGalleryFooterContentNode.swift @@ -14,6 +14,7 @@ final class InstantPageGalleryFooterContentNode: GalleryFooterContentNode { private let account: Account private var theme: PresentationTheme private var strings: PresentationStrings + private var shareMedia: AnyMediaReference? private let actionButton: UIButton private let textNode: ASTextNode @@ -55,8 +56,13 @@ final class InstantPageGalleryFooterContentNode: GalleryFooterContentNode { } } + func setShareMedia(_ shareMedia: AnyMediaReference?) { + self.shareMedia = shareMedia + self.actionButton.isHidden = shareMedia == nil + } + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - var panelHeight: CGFloat = 44.0 + bottomInset + var panelHeight: CGFloat = 44.0 + bottomInset + contentInset if !self.textNode.isHidden { let sideInset: CGFloat = leftInset + 8.0 let topInset: CGFloat = 8.0 @@ -72,5 +78,8 @@ final class InstantPageGalleryFooterContentNode: GalleryFooterContentNode { } @objc func actionButtonPressed() { + if let shareMedia = self.shareMedia { + self.controllerInteraction?.presentController(ShareController(account: self.account, subject: .media(shareMedia), saveToCameraRoll: true, showInChat: nil, externalShare: true, immediateExternalShare: false), nil) + } } } diff --git a/TelegramUI/InstantPageSettingsItemTheme.swift b/TelegramUI/InstantPageSettingsItemTheme.swift index 7aec03baf8..633c413031 100644 --- a/TelegramUI/InstantPageSettingsItemTheme.swift +++ b/TelegramUI/InstantPageSettingsItemTheme.swift @@ -46,8 +46,8 @@ final class InstantPageSettingsItemTheme: Equatable { return true } - static func themeFor(_ settings: InstantPagePresentationSettings) -> InstantPageSettingsItemTheme { - switch settings.themeType { + static func themeFor(_ type: InstantPageThemeType) -> InstantPageSettingsItemTheme { + switch type { case .light: return lightTheme case .sepia: diff --git a/TelegramUI/InstantPageSettingsNode.swift b/TelegramUI/InstantPageSettingsNode.swift index 02959113a9..7c3058341e 100644 --- a/TelegramUI/InstantPageSettingsNode.swift +++ b/TelegramUI/InstantPageSettingsNode.swift @@ -19,6 +19,7 @@ private func generateArrowImage(color: UIColor) -> UIImage? { final class InstantPageSettingsNode: ASDisplayNode { private var settings: InstantPagePresentationSettings + private var currentThemeType: InstantPageThemeType private var theme: InstantPageSettingsItemTheme private let applySettings: (InstantPagePresentationSettings) -> Void @@ -32,9 +33,10 @@ final class InstantPageSettingsNode: ASDisplayNode { private let arrowNode: ASImageNode private let itemContainerNode: ASDisplayNode - init(strings: PresentationStrings, settings: InstantPagePresentationSettings, applySettings: @escaping (InstantPagePresentationSettings) -> Void) { + init(strings: PresentationStrings, settings: InstantPagePresentationSettings, currentThemeType: InstantPageThemeType, applySettings: @escaping (InstantPagePresentationSettings) -> Void) { self.settings = settings - self.theme = InstantPageSettingsItemTheme.themeFor(settings) + self.currentThemeType = currentThemeType + self.theme = InstantPageSettingsItemTheme.themeFor(currentThemeType) self.applySettings = applySettings @@ -205,34 +207,36 @@ final class InstantPageSettingsNode: ASDisplayNode { if updated != self.settings { self.settings = updated - self.sansFamilyNode.checked = !self.settings.forceSerif - self.serifFamilyNode.checked = self.settings.forceSerif - self.themeItemNode.themeType = self.settings.themeType - self.autoNightItemNode.isEnabled = self.settings.themeType != .dark + self.applySettings(settings) + } + } + + func updateSettingsAndCurrentThemeType(settings: InstantPagePresentationSettings, type: InstantPageThemeType) { + self.currentThemeType = type + + self.sansFamilyNode.checked = !self.settings.forceSerif + self.serifFamilyNode.checked = self.settings.forceSerif + self.themeItemNode.themeType = self.settings.themeType + self.autoNightItemNode.isEnabled = self.settings.themeType != .dark + + let theme = InstantPageSettingsItemTheme.themeFor(self.currentThemeType) + if theme != self.theme { + self.theme = theme - let theme = InstantPageSettingsItemTheme.themeFor(self.settings) - if theme != self.theme { - self.theme = theme - - if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) { - self.view.addSubview(snapshotView) - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - - self.arrowNode.image = generateArrowImage(color: self.theme.itemBackgroundColor) - self.itemContainerNode.backgroundColor = self.theme.listBackgroundColor - for section in self.sections { - for item in section { - item.updateTheme(self.theme) - } - } - - + if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) { + self.view.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) } - self.applySettings(settings) + self.arrowNode.image = generateArrowImage(color: self.theme.itemBackgroundColor) + self.itemContainerNode.backgroundColor = self.theme.listBackgroundColor + for section in self.sections { + for item in section { + item.updateTheme(self.theme) + } + } } } diff --git a/TelegramUI/InstantPageSlideshowItemNode.swift b/TelegramUI/InstantPageSlideshowItemNode.swift index 58ebd3027f..4e22a04c61 100644 --- a/TelegramUI/InstantPageSlideshowItemNode.swift +++ b/TelegramUI/InstantPageSlideshowItemNode.swift @@ -112,6 +112,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe self.scrollView.clipsToBounds = false self.scrollView.scrollsToTop = false self.view.addSubview(self.scrollView) + self.view.disablesInteractiveTransitionGestureRecognizer = true } func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { diff --git a/TelegramUI/InstantPageTheme.swift b/TelegramUI/InstantPageTheme.swift index 957dec02b5..79cc8a40cf 100644 --- a/TelegramUI/InstantPageTheme.swift +++ b/TelegramUI/InstantPageTheme.swift @@ -184,12 +184,12 @@ private func fontSizeMultiplierForVariant(_ variant: InstantPagePresentationFont } } -func instantPageThemeForSettingsAndTime(presentationTheme: PresentationTheme, settings: InstantPagePresentationSettings, time: Date) -> InstantPageTheme { +func instantPageThemeTypeForSettingsAndTime(presentationTheme: PresentationTheme, settings: InstantPagePresentationSettings, time: Date?) -> InstantPageThemeType { if settings.autoNightMode { switch settings.themeType { case .light, .sepia, .gray: var useDarkTheme = false - switch presentationTheme.name { + /*switch presentationTheme.name { case let .builtin(name): switch name { case .nightAccent, .nightGrayscale: @@ -199,21 +199,27 @@ func instantPageThemeForSettingsAndTime(presentationTheme: PresentationTheme, se } default: break - } - let calendar = Calendar.current - let hour = calendar.component(.hour, from: time) - if hour <= 8 || hour >= 22 { - useDarkTheme = true + }*/ + if let time = time { + let calendar = Calendar.current + let hour = calendar.component(.hour, from: time) + if hour <= 8 || hour >= 22 { + useDarkTheme = true + } } if useDarkTheme { - return darkTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) + return .dark } case .dark: break } } - switch settings.themeType { + return settings.themeType +} + +func instantPageThemeForType(_ type: InstantPageThemeType, settings: InstantPagePresentationSettings) -> InstantPageTheme { + switch type { case .light: return lightTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif) case .sepia: diff --git a/TelegramUI/InviteContactsControllerNode.swift b/TelegramUI/InviteContactsControllerNode.swift index b210ba08da..c53be1761a 100644 --- a/TelegramUI/InviteContactsControllerNode.swift +++ b/TelegramUI/InviteContactsControllerNode.swift @@ -95,7 +95,7 @@ private enum InviteContactsEntry: Comparable, Identifiable { status = .none } let peer = TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: contact.firstName, lastName: contact.lastName, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) - return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: peer, chatPeer: peer, status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: status, enabled: true, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { _ in interaction.toggleContact(contact.id) }) } @@ -454,6 +454,13 @@ final class InviteContactsControllerNode: ASDisplayNode { } } } + + self.listNode.didEndScrolling = { [weak self] in + guard let strongSelf = self else { + return + } + fixSearchableListNodeScrolling(strongSelf.listNode) + } } deinit { @@ -477,7 +484,7 @@ final class InviteContactsControllerNode: ASDisplayNode { if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) if !searchDisplayController.isDeactivating { - insets.top += 20.0 + insets.top += layout.statusBarHeight ?? 0.0 } } @@ -543,7 +550,7 @@ final class InviteContactsControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: false, openPeer: { [weak self] peerId in + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: false, categories: [.deviceContacts], openPeer: { [weak self] peerId in }), cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index f5c7f01f6a..35aa977d83 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -153,6 +153,7 @@ class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { let mode: ItemListAvatarAndNameInfoItemMode let peer: Peer? let presence: PeerPresence? + let label: String? let cachedData: CachedPeerData? let state: ItemListAvatarAndNameInfoItemState let sectionId: ItemListSectionId @@ -167,13 +168,14 @@ class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { let selectable: Bool - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, mode: ItemListAvatarAndNameInfoItemMode, peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, sectionId: ItemListSectionId, style: ItemListAvatarAndNameInfoItemStyle, editingNameUpdated: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, avatarTapped: @escaping () -> Void, context: ItemListAvatarAndNameInfoItemContext? = nil, updatingImage: ItemListAvatarAndNameInfoItemUpdatingAvatar? = nil, call: (() -> Void)? = nil, action: (() -> Void)? = nil, tag: ItemListItemTag? = nil) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, mode: ItemListAvatarAndNameInfoItemMode, peer: Peer?, presence: PeerPresence?, label: String? = nil, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, sectionId: ItemListSectionId, style: ItemListAvatarAndNameInfoItemStyle, editingNameUpdated: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, avatarTapped: @escaping () -> Void, context: ItemListAvatarAndNameInfoItemContext? = nil, updatingImage: ItemListAvatarAndNameInfoItemUpdatingAvatar? = nil, call: (() -> Void)? = nil, action: (() -> Void)? = nil, tag: ItemListItemTag? = nil) { self.account = account self.theme = theme self.strings = strings self.mode = mode self.peer = peer self.presence = presence + self.label = label self.cachedData = cachedData self.state = state self.sectionId = sectionId @@ -390,7 +392,10 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite } statusColor = item.theme.list.itemSecondaryTextColor case .generic: - if let _ = peer.botInfo { + if let label = item.label { + statusText = label + statusColor = item.theme.list.itemSecondaryTextColor + } else if let _ = peer.botInfo { statusText = item.strings.Bot_GenericBotStatus statusColor = item.theme.list.itemSecondaryTextColor } else if let presence = item.presence as? TelegramUserPresence { diff --git a/TelegramUI/ItemListController.swift b/TelegramUI/ItemListController.swift index 3d15bcb64a..19d7cade16 100644 --- a/TelegramUI/ItemListController.swift +++ b/TelegramUI/ItemListController.swift @@ -126,7 +126,7 @@ struct ItemListControllerState { } } -final class ItemListController: ViewController { +class ItemListController: ViewController { private let state: Signal<(ItemListControllerState, (ItemListNodeState, Entry.ItemGenerationArguments)), NoError> private var leftNavigationButtonTitleAndStyle: (ItemListNavigationButtonContent, ItemListNavigationButtonStyle)? @@ -264,7 +264,7 @@ final class ItemListController: ViewController { var image: UIImage? switch icon { case .search: - image = PresentationResourcesRootController.navigationSearchIcon(controllerState.theme) + image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.theme) } item = UIBarButtonItem(image: image, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) } @@ -317,7 +317,7 @@ final class ItemListController: ViewController { var image: UIImage? switch icon { case .search: - image = PresentationResourcesRootController.navigationSearchIcon(controllerState.theme) + image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.theme) } item = UIBarButtonItem(image: image, style: style.barButtonItemStyle, target: strongSelf, action: action) } diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index 0255757387..fd89e3ec67 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -44,6 +44,7 @@ private struct ItemListNodeTransition { let firstTime: Bool let animated: Bool let animateAlpha: Bool + let crossfade: Bool let mergedEntries: [Entry] } @@ -53,13 +54,15 @@ struct ItemListNodeState { let emptyStateItem: ItemListControllerEmptyStateItem? let searchItem: ItemListControllerSearch? let animateChanges: Bool + let crossfadeState: Bool let focusItemTag: ItemListItemTag? - init(entries: [Entry], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, searchItem: ItemListControllerSearch? = nil, animateChanges: Bool = true) { + init(entries: [Entry], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, searchItem: ItemListControllerSearch? = nil, crossfadeState: Bool = false, animateChanges: Bool = true) { self.entries = entries self.style = style self.emptyStateItem = emptyStateItem self.searchItem = searchItem + self.crossfadeState = crossfadeState self.animateChanges = animateChanges self.focusItemTag = focusItemTag } @@ -179,7 +182,7 @@ class ItemListControllerNode: ViewControllerTracingNod if previous?.style != state.style { updatedStyle = state.style } - return ItemListNodeTransition(theme: theme, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, searchItem: state.searchItem, focusItemTag: state.focusItemTag, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, mergedEntries: state.entries) + return ItemListNodeTransition(theme: theme, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, searchItem: state.searchItem, focusItemTag: state.focusItemTag, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, crossfade: state.crossfadeState, mergedEntries: state.entries) }) |> deliverOnMainQueue).start(next: { [weak self] transition in if let strongSelf = self { strongSelf.enqueueTransition(transition) @@ -307,6 +310,8 @@ class ItemListControllerNode: ViewControllerTracingNod options.insert(.PreferSynchronousResourceLoading) options.insert(.PreferSynchronousDrawing) options.insert(.AnimateAlpha) + } else if transition.crossfade { + options.insert(.AnimateCrossfade) } else { options.insert(.Synchronous) options.insert(.PreferSynchronousDrawing) @@ -329,9 +334,13 @@ class ItemListControllerNode: ViewControllerTracingNod strongSelf.appliedFocusItemTag = focusItemTag if let focusItemTag = focusItemTag { strongSelf.listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ItemListItemNode, let itemTag = itemNode.tag, itemTag.isEqual(to: focusItemTag) { - if let focusableNode = itemNode as? ItemListItemFocusableNode { - focusableNode.focus() + if let itemNode = itemNode as? ItemListItemNode { + if let itemTag = itemNode.tag { + if itemTag.isEqual(to: focusItemTag) { + if let focusableNode = itemNode as? ItemListItemFocusableNode { + focusableNode.focus() + } + } } } } diff --git a/TelegramUI/ItemListTextItem.swift b/TelegramUI/ItemListTextItem.swift index a6b28dc953..c54e9365d0 100644 --- a/TelegramUI/ItemListTextItem.swift +++ b/TelegramUI/ItemListTextItem.swift @@ -107,7 +107,7 @@ class ItemListTextItemNode: ListViewItemNode { return (TelegramTextAttributes.Url, contents) })) } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize: CGSize diff --git a/TelegramUI/ItemListTextWithLabelItem.swift b/TelegramUI/ItemListTextWithLabelItem.swift index 8b58413bca..0b9cabda11 100644 --- a/TelegramUI/ItemListTextWithLabelItem.swift +++ b/TelegramUI/ItemListTextWithLabelItem.swift @@ -15,6 +15,7 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { let textColor: ItemListTextWithLabelItemTextColor let enabledEntitiyTypes: EnabledEntityTypes let multiline: Bool + let selected: Bool? let sectionId: ItemListSectionId let action: (() -> Void)? let longTapAction: (() -> Void)? @@ -22,13 +23,14 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { let tag: Any? - init(theme: PresentationTheme, label: String, text: String, textColor: ItemListTextWithLabelItemTextColor = .primary, enabledEntitiyTypes: EnabledEntityTypes, multiline: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { + init(theme: PresentationTheme, label: String, text: String, textColor: ItemListTextWithLabelItemTextColor = .primary, enabledEntitiyTypes: EnabledEntityTypes, multiline: Bool, selected: Bool? = nil, sectionId: ItemListSectionId, action: (() -> Void)?, longTapAction: (() -> Void)? = nil, linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil, tag: Any? = nil) { self.theme = theme self.label = label self.text = text self.textColor = textColor self.enabledEntitiyTypes = enabledEntitiyTypes self.multiline = multiline + self.selected = selected self.sectionId = sectionId self.action = action self.longTapAction = longTapAction @@ -45,7 +47,7 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { node.insets = layout.insets completion(node, { - return (nil, { apply() }) + return (nil, { apply(.None) }) }) } } @@ -59,7 +61,7 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { - apply() + apply(animation) }) } } @@ -92,6 +94,7 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode private var linkHighlightingNode: LinkHighlightingNode? + private var selectionNode: ItemListSelectableControlNode? var item: ItemListTextWithLabelItem? @@ -146,12 +149,14 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { self.view.addGestureRecognizer(recognizer) } - func asyncLayout() -> (_ item: ItemListTextWithLabelItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ItemListTextWithLabelItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) let currentItem = self.item + let selectionNodeLayout = ItemListSelectableControlNode.asyncLayout(self.selectionNode) + return { item, params, neighbors in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { @@ -163,7 +168,15 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { let rightInset: CGFloat = 8.0 + params.rightInset let separatorHeight = UIScreenPixel - let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + var leftOffset: CGFloat = 0.0 + var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? + if let selected = item.selected { + let (selectionWidth, selectionApply) = selectionNodeLayout(item.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected) + selectionNodeWidthAndApply = (selectionWidth, selectionApply) + leftOffset += selectionWidth - 24.0 + } + + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftOffset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let entities = generateTextEntities(item.text, enabledTypes: item.enabledEntitiyTypes) let baseColor: UIColor @@ -175,11 +188,18 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { } let string = stringWithAppliedEntities(item.text, entities: entities, baseColor: baseColor, linkColor: item.theme.list.itemAccentColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, fixedFont: textFixedFont) - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftOffset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize = CGSize(width: params.width, height: textLayout.size.height + 39.0) let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - return (nodeLayout, { [weak self] in + return (nodeLayout, { [weak self] animation in if let strongSelf = self { + let transition: ContainedViewLayoutTransition + if animation.isAnimated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + strongSelf.item = item if let _ = updatedTheme { @@ -192,14 +212,34 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { let _ = labelApply() let _ = textApply() - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: labelLayout.size) - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 31.0), size: textLayout.size) + if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply { + let selectionFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: selectionWidth, height: nodeLayout.contentSize.height)) + let selectionNode = selectionApply(selectionFrame.size, transition.isAnimated) + if selectionNode !== strongSelf.selectionNode { + strongSelf.selectionNode?.removeFromSupernode() + strongSelf.selectionNode = selectionNode + strongSelf.addSubnode(selectionNode) + selectionNode.frame = selectionFrame + transition.animatePosition(node: selectionNode, from: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY)) + } else { + transition.updateFrame(node: selectionNode, frame: selectionFrame) + } + } else if let selectionNode = strongSelf.selectionNode { + strongSelf.selectionNode = nil + let selectionFrame = selectionNode.frame + transition.updatePosition(node: selectionNode, position: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY), completion: { [weak selectionNode] _ in + selectionNode?.removeFromSupernode() + }) + } + + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 11.0), size: labelLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 31.0), size: textLayout.size) let leftInset: CGFloat let style = ItemListStyle.plain switch style { case .plain: - leftInset = 35.0 + params.leftInset + leftInset = 35.0 + params.leftInset + leftOffset if strongSelf.backgroundNode.supernode != nil { strongSelf.backgroundNode.removeFromSupernode() @@ -254,7 +294,7 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted && self.linkItemAtPoint(point) == nil { + if highlighted && self.linkItemAtPoint(point) == nil && self.selectionNode == nil { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { var anchorNode: ASDisplayNode? diff --git a/TelegramUI/LegacyAttachmentMenu.swift b/TelegramUI/LegacyAttachmentMenu.swift index 21652da664..09f119444e 100644 --- a/TelegramUI/LegacyAttachmentMenu.swift +++ b/TelegramUI/LegacyAttachmentMenu.swift @@ -33,14 +33,14 @@ func legacyAttachmentMenu(account: Account, peer: Peer, editMediaOptions: Messag carouselItem.recipientName = peer.displayTitle carouselItem.cameraPressed = { [weak controller] cameraView in if let controller = controller { - authorizeDeviceAccess(to: .camera, presentationData: account.telegramApplicationContext.currentPresentationData.with { $0 }, present: account.telegramApplicationContext.presentGlobalController, openSettings: account.telegramApplicationContext.applicationBindings.openSettings, { value in + DeviceAccess.authorizeAccess(to: .camera, presentationData: account.telegramApplicationContext.currentPresentationData.with { $0 }, present: account.telegramApplicationContext.presentGlobalController, openSettings: account.telegramApplicationContext.applicationBindings.openSettings, { value in if value { openCamera(cameraView, controller) } }) } } - if (peer is TelegramUser || peer is TelegramSecretChat) && peer.id != account.peerId { + if (peer is TelegramUser) && peer.id != account.peerId { carouselItem.hasTimer = true } carouselItem.sendPressed = { [weak controller, weak carouselItem] currentItem, asFiles in @@ -129,7 +129,7 @@ func legacyPasteMenu(account: Account, peer: Peer, saveEditedPhotos: Bool, allow let baseController = TGViewController(context: legacyController.context)! legacyController.bind(controller: baseController) var hasTimer = false - if (peer is TelegramUser || peer is TelegramSecretChat) && peer.id != account.peerId { + if (peer is TelegramUser) && peer.id != account.peerId { hasTimer = true } let recipientName = peer.displayTitle diff --git a/TelegramUI/LegacyComponentsStickers.swift b/TelegramUI/LegacyComponentsStickers.swift index 25e96830aa..9c1cb00ec2 100644 --- a/TelegramUI/LegacyComponentsStickers.swift +++ b/TelegramUI/LegacyComponentsStickers.swift @@ -19,6 +19,16 @@ func legacyComponentsStickers(postbox: Postbox, namespace: Int32) -> SSignal { if let resource = item.file.resource as? CloudDocumentMediaResource { document.accessHash = resource.accessHash document.datacenterId = Int32(resource.datacenterId) + var stickerPackId: Int64 = 0 + var accessHash: Int64 = 0 + for case let .Sticker(sticker) in item.file.attributes { + if let packReference = sticker.packReference, case let .id(id, h) = packReference { + stickerPackId = id + accessHash = h + } + break + } + document.originInfo = TGMediaOriginInfo(fileReference: resource.fileReference ?? Data(), fileReferences: [:], stickerPackId: stickerPackId, accessHash: accessHash) } document.mimeType = item.file.mimeType if let size = item.file.size { @@ -119,11 +129,11 @@ final class LegacyStickerImageDataSource: TGImageDataSource { let args: [AnyHashable : Any] let highQuality: Bool if uri.hasPrefix("sticker-preview://") { - let argumentsString = uri.substring(from: uri.index(uri.startIndex, offsetBy: "sticker-preview://?".characters.count)) + let argumentsString = String(uri[uri.index(uri.startIndex, offsetBy: "sticker-preview://?".count)...]) args = TGStringUtils.argumentDictionary(inUrlString: argumentsString)! highQuality = Int((args["highQuality"] as! String))! != 0 } else if uri.hasPrefix("sticker://") { - let argumentsString = uri.substring(from: uri.index(uri.startIndex, offsetBy: "sticker://?".characters.count)) + let argumentsString = String(uri[uri.index(uri.startIndex, offsetBy: "sticker://?".count)...]) args = TGStringUtils.argumentDictionary(inUrlString: argumentsString)! highQuality = true } else { @@ -140,7 +150,12 @@ final class LegacyStickerImageDataSource: TGImageDataSource { let fitSize = CGSize(width: CGFloat(width), height: CGFloat(height)) - return LegacyStickerImageDataTask(account: account, file: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), reference: nil, resource: CloudDocumentMediaResource(datacenterId: datacenterId, fileId: documentId, accessHash: accessHash, size: size, fileReference: nil), previewRepresentations: [], mimeType: "image/webp", size: size, attributes: []), small: !highQuality, fitSize: fitSize, completion: { image in + var attributes: [TelegramMediaFileAttribute] = [] + if let originInfoString = args["origin_info"] as? String, let originInfo = TGMediaOriginInfo(stringRepresentation: originInfoString), let stickerPackId = originInfo.stickerPackId?.int64Value, let stickerPackAccessHash = originInfo.stickerPackAccessHash?.int64Value { + attributes.append(.Sticker(displayText: "", packReference: .id(id: stickerPackId, accessHash: stickerPackAccessHash), maskData: nil)) + } + + return LegacyStickerImageDataTask(account: account, file: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: datacenterId, fileId: documentId, accessHash: accessHash, size: size, fileReference: nil), previewRepresentations: [], mimeType: "image/webp", size: size, attributes: attributes), small: !highQuality, fitSize: fitSize, completion: { image in if let image = image { sharedImageCache.setImage(image, forKey: uri, attributes: nil) completion?(TGDataResource(image: image, decoded: true)) diff --git a/TelegramUI/LegacyInstantVideoController.swift b/TelegramUI/LegacyInstantVideoController.swift index 3e309cc4d9..260aaf8e6f 100644 --- a/TelegramUI/LegacyInstantVideoController.swift +++ b/TelegramUI/LegacyInstantVideoController.swift @@ -145,9 +145,15 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: videoUrl.path, adjustments: resourceAdjustments) } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), reference: nil, resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: finalDimensions, flags: [.instantRoundVideo])]) + if let previewImage = previewImage { + if let data = compressImageToJPEG(previewImage, quality: 0.7) { + account.postbox.mediaBox.storeCachedResourceRepresentation(resource, representation: CachedVideoFirstFrameRepresentation(), data: data) + } + } + + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: finalDimensions, flags: [.instantRoundVideo])]) let attributes: [MessageAttribute] = [] - send(.message(text: "", attributes: attributes, media: media, replyToMessageId: nil, localGroupingKey: nil)) + send(.message(text: "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil)) } controller.didDismiss = { [weak legacyController] in if let legacyController = legacyController { diff --git a/TelegramUI/LegacyLiveUploadInterface.swift b/TelegramUI/LegacyLiveUploadInterface.swift index e4fa3b3738..021db69a83 100644 --- a/TelegramUI/LegacyLiveUploadInterface.swift +++ b/TelegramUI/LegacyLiveUploadInterface.swift @@ -21,7 +21,7 @@ final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUploadInter private var size: Int? private let data = Promise() - private var dataValue: MediaResourceData? + private let dataValue = Atomic(value: nil) init(account: Account) { self.account = account @@ -40,13 +40,17 @@ final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUploadInter } strongSelf.size = size - var complete = false - if let dataValue = strongSelf.dataValue, dataValue.complete { - complete = true + let result = strongSelf.dataValue.modify { dataValue in + if let dataValue = dataValue, dataValue.complete { + return MediaResourceData(path: path, offset: 0, size: size, complete: true) + } else { + return MediaResourceData(path: path, offset: 0, size: size, complete: false) + } + } + if let result = result { + print("**set1 \(result) \(result.complete)") + strongSelf.data.set(.single(result)) } - let dataValue = MediaResourceData(path: path, offset: 0, size: size, complete: complete) - strongSelf.dataValue = dataValue - strongSelf.data.set(.single(dataValue)) } } } @@ -56,10 +60,21 @@ final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUploadInter override func fileUpdated(_ completed: Bool) -> Any! { let _ = super.fileUpdated(completed) - if completed, let dataValue = self.dataValue { - self.dataValue = MediaResourceData(path: dataValue.path, offset: dataValue.offset, size: dataValue.size, complete: true) - self.data.set(.single(dataValue)) - return LegacyLiveUploadInterfaceResult(id: self.id) + if completed { + let result = self.dataValue.modify { dataValue in + if let dataValue = dataValue { + return MediaResourceData(path: dataValue.path, offset: dataValue.offset, size: dataValue.size, complete: true) + } else { + return nil + } + } + if let result = result { + print("**set2 \(result) \(completed)") + self.data.set(.single(result)) + return LegacyLiveUploadInterfaceResult(id: self.id) + } else { + return nil + } } else { return nil } diff --git a/TelegramUI/LegacyMediaPickers.swift b/TelegramUI/LegacyMediaPickers.swift index 117e59a647..d5da3e5625 100644 --- a/TelegramUI/LegacyMediaPickers.swift +++ b/TelegramUI/LegacyMediaPickers.swift @@ -17,7 +17,7 @@ func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, account: controller.captionsEnabled = captionsEnabled controller.inhibitDocumentCaptions = false controller.suggestionContext = legacySuggestionContext(account: account, peerId: peer.id) - if (peer is TelegramUser || peer is TelegramSecretChat) && peer.id != account.peerId { + if (peer is TelegramUser) && peer.id != account.peerId { controller.hasTimer = true } controller.dismissalBlock = { @@ -32,7 +32,7 @@ func legacyAssetPicker(applicationContext: TelegramApplicationContext, presentat return Signal { subscriber in let intent = fileMode ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent - authorizeDeviceAccess(to: .mediaLibrary(.send), presentationData: presentationData, present: applicationContext.presentGlobalController, openSettings: applicationContext.applicationBindings.openSettings, { value in + DeviceAccess.authorizeAccess(to: .mediaLibrary(.send), presentationData: presentationData, present: applicationContext.presentGlobalController, openSettings: applicationContext.applicationBindings.openSettings, { value in if !value { subscriber.putError(NoError()) return @@ -215,23 +215,23 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa arc4random_buf(&randomId, 8) let _ = try? heicData.write(to: URL(fileURLWithPath: tempFilePath + ".heic")) let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath + ".heic", randomId: randomId) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), reference: nil, resource: resource, previewRepresentations: [], mimeType: "image/heic", size: nil, attributes: [.FileName(fileName: "image.heic")]) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], mimeType: "image/heic", size: nil, attributes: [.FileName(fileName: "image.heic")]) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } - messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) + messages.append(.message(text: caption ?? "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) } } #endif let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) - let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)], reference: nil) + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)], reference: nil, partialReference: nil) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } - messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) + messages.append(.message(text: caption ?? "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) } } case let .asset(asset): @@ -241,12 +241,12 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa let scaledSize = size.aspectFitted(CGSize(width: 1280.0, height: 1280.0)) let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier) - let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)], reference: nil) + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)], reference: nil, partialReference: nil) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } - messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) + messages.append(.message(text: caption ?? "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) case .tempFile: break } @@ -256,14 +256,14 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), reference: nil, resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) - messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) + messages.append(.message(text: caption ?? "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) case let .asset(asset): var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), reference: nil, resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) - messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) + messages.append(.message(text: caption ?? "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) default: break } @@ -344,12 +344,12 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa } } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), reference: nil, resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: fileAttributes) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: fileAttributes) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } - messages.append(.message(text: caption ?? "", attributes: attributes, media: media, replyToMessageId: nil, localGroupingKey: item.groupedId)) + messages.append(.message(text: caption ?? "", attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) } } } diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index 38e1f9e4a6..5ee8b23c43 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -283,10 +283,13 @@ final class ListMessageSnippetItemNode: ListMessageNode { let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 12.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))) + let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 12.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))) - let (linkNodeLayout, linkNodeApply) = linkNodeMakeLayout(TextNodeLayoutArguments(attributedString: linkText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 12.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: TextNodeCutout(position: .TopLeft, size: CGSize(width: 10.0, height: 8.0)), insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))) - let instantViewImage = PresentationResourcesChat.sharedMediaInstantViewIcon(item.theme) + let (linkNodeLayout, linkNodeApply) = linkNodeMakeLayout(TextNodeLayoutArguments(attributedString: linkText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 12.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: isInstantView ? TextNodeCutout(position: .TopLeft, size: CGSize(width: 10.0, height: 8.0)) : nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))) + var instantViewImage: UIImage? + if isInstantView { + instantViewImage = PresentationResourcesChat.sharedMediaInstantViewIcon(item.theme) + } let (iconTextLayout, iconTextApply) = iconTextMakeLayout(TextNodeLayoutArguments(attributedString: iconText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) diff --git a/TelegramUI/NativeVideoContent.swift b/TelegramUI/NativeVideoContent.swift index 9df5762c18..0efc754c22 100644 --- a/TelegramUI/NativeVideoContent.swift +++ b/TelegramUI/NativeVideoContent.swift @@ -192,7 +192,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent } self.imageNode.frame = CGRect(origin: CGPoint(), size: size) - self.playerNode.frame = CGRect(origin: CGPoint(), size: size) + self.playerNode.frame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -0.0002, dy: -0.0002) } func play() { diff --git a/TelegramUI/NavigateToChatController.swift b/TelegramUI/NavigateToChatController.swift index b14abf8327..c4c1c2f8e9 100644 --- a/TelegramUI/NavigateToChatController.swift +++ b/TelegramUI/NavigateToChatController.swift @@ -75,8 +75,15 @@ private func findOpaqueLayer(rootLayer: CALayer, layer: CALayer) -> Bool { return false } +public func isInlineControllerForChatNotificationOverlayPresentation(_ controller: ViewController) -> Bool { + if controller is InstantPageController { + return true + } + return false +} + public func isOverlayControllerForChatNotificationOverlayPresentation(_ controller: ViewController) -> Bool { - if controller is GalleryController || controller is AvatarGalleryController || controller is ThemeGalleryController || controller is InstantPageGalleryController { + if controller is GalleryController || controller is AvatarGalleryController || controller is ThemeGalleryController || controller is InstantPageGalleryController || controller is InstantVideoController { return true } diff --git a/TelegramUI/OpenChatMessage.swift b/TelegramUI/OpenChatMessage.swift index 6b45299753..5e9986b41f 100644 --- a/TelegramUI/OpenChatMessage.swift +++ b/TelegramUI/OpenChatMessage.swift @@ -188,7 +188,7 @@ func openChatMessage(account: Account, message: Message, standalone: Bool, rever present(legacyLocationController(message: message, mapMedia: mapMedia, account: account, openPeer: { peer in openPeer(peer, .info) }, sendLiveLocation: { coordinate, period in - let outMessage: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period), replyToMessageId: nil, localGroupingKey: nil) + let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period)), replyToMessageId: nil, localGroupingKey: nil) enqueueMessage(outMessage) }, stopLiveLocation: { account.telegramApplicationContext.liveLocationManager?.cancelLiveLocation(peerId: message.id.peerId) @@ -249,11 +249,20 @@ func openChatMessage(account: Account, message: Message, standalone: Bool, rever return (nil, nil) } } |> deliverOnMainQueue).start(next: { peer, isContact in + let contactData: DeviceContactExtendedData + if let vCard = contact.vCardData, let vCardData = vCard.data(using: .utf8), let parsed = DeviceContactExtendedData(vcard: vCardData) { + contactData = parsed + } else { + contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName, lastName: contact.lastName, phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: contact.phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: []) + } + let controller = deviceContactInfoController(account: account, subject: .vcard(peer, nil, contactData)) + navigationController?.pushViewController(controller) + guard let peer = peer else { return } - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + /*let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationTheme: presentationData.theme) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() @@ -286,7 +295,7 @@ func openChatMessage(account: Account, message: Message, standalone: Bool, rever ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) dismissInput() - present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))*/ }) return true } diff --git a/TelegramUI/OpenResolvedUrl.swift b/TelegramUI/OpenResolvedUrl.swift index ae0042dac9..3b385c1fa3 100644 --- a/TelegramUI/OpenResolvedUrl.swift +++ b/TelegramUI/OpenResolvedUrl.swift @@ -4,10 +4,10 @@ import Postbox import Display import SwiftSignalKit -func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, present: (ViewController, Any?) -> Void) { +func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, present: (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void) { switch resolvedUrl { case let .externalUrl(url): - openExternalUrl(account: account, url: url, presentationData: account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: account.telegramApplicationContext, navigationController: navigationController) + openExternalUrl(account: account, url: url, presentationData: account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: account.telegramApplicationContext, navigationController: navigationController, dismissInput: dismissInput) case let .peer(peerId): openPeer(peerId, .chat(textInputState: nil, messageId: nil)) case let .botStart(peerId, payload): @@ -16,21 +16,32 @@ func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, navigationCon let controller = PeerSelectionController(account: account, filter: [.onlyWriteable, .onlyGroups]) controller.peerSelected = { [weak controller] peerId in let _ = (requestStartBotInGroup(account: account, botPeerId: botPeerId, groupPeerId: peerId, payload: payload) - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).start(next: { result in if let navigationController = navigationController { navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) } + switch result { + case let .channelParticipant(participant): + account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.externallyAdded(peerId: peerId, participant: participant) + case .none: + break + } controller?.dismiss() + }, error: { _ in + }) } + dismissInput() present(controller, ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) case let .channelMessage(peerId, messageId): openPeer(peerId, .chat(textInputState: nil, messageId: messageId)) case let .stickerPack(name): + dismissInput() present(StickerPackPreviewController(account: account, stickerPack: .name(name), parentNavigationController: navigationController), nil) case let .instantView(webpage, anchor): navigationController?.pushViewController(InstantPageController(account: account, webPage: webpage, anchor: anchor)) case let .join(link): + dismissInput() present(JoinLinkPreviewController(account: account, link: link, navigateToPeer: { peerId in openPeer(peerId, .chat(textInputState: nil, messageId: nil)) }), nil) @@ -42,7 +53,8 @@ func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, navigationCon } else { server = ProxyServerSettings(host: host, port: port, connection: .socks5(username: username, password: password)) } - navigationController?.view.window?.endEditing(true) + + dismissInput() present(ProxyServerActionSheetController(account: account, theme: presentationData.theme, strings: presentationData.strings, server: server), nil) } } diff --git a/TelegramUI/OpenUrl.swift b/TelegramUI/OpenUrl.swift index 2429ab050b..974346ac4d 100644 --- a/TelegramUI/OpenUrl.swift +++ b/TelegramUI/OpenUrl.swift @@ -5,7 +5,7 @@ import TelegramCore import Postbox import SwiftSignalKit -public func openExternalUrl(account: Account, url: String, presentationData: PresentationData, applicationContext: TelegramApplicationContext, navigationController: NavigationController?) { +public func openExternalUrl(account: Account, url: String, presentationData: PresentationData, applicationContext: TelegramApplicationContext, navigationController: NavigationController?, dismissInput: @escaping () -> Void) { if url.lowercased().hasPrefix("tel:") { applicationContext.applicationBindings.openUrl(url) return @@ -325,6 +325,8 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre navigationController.view.window?.rootViewController?.dismiss(animated: true, completion: nil) (navigationController.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a) } + }, dismissInput: { + dismissInput() }) } }) diff --git a/TelegramUI/PeerAvatar.swift b/TelegramUI/PeerAvatar.swift index 45585de8e7..2447bcb1e8 100644 --- a/TelegramUI/PeerAvatar.swift +++ b/TelegramUI/PeerAvatar.swift @@ -19,7 +19,7 @@ private let roundCorners = { () -> UIImage in return image }() -func peerAvatarImage(account: Account, peer: Peer, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal? { +func peerAvatarImage(account: Account, peer: Peer, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal? { if let smallProfileImage = representation { let resourceData = account.postbox.mediaBox.resourceData(smallProfileImage.resource) let imageData = resourceData @@ -44,6 +44,8 @@ func peerAvatarImage(account: Account, peer: Peer, representation: TelegramMedia var fetchedDataDisposable: Disposable? if let peerReference = PeerReference(peer) { fetchedDataDisposable = fetchedMediaResource(postbox: account.postbox, reference: .avatar(peer: peerReference, resource: smallProfileImage.resource), statsCategory: .generic).start() + } else if let authorOfMessage = authorOfMessage { + fetchedDataDisposable = fetchedMediaResource(postbox: account.postbox, reference: .messageAuthorAvatar(message: authorOfMessage, resource: smallProfileImage.resource), statsCategory: .generic).start() } else { fetchedDataDisposable = fetchedMediaResource(postbox: account.postbox, reference: .standalone(resource: smallProfileImage.resource), statsCategory: .generic).start() } diff --git a/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift b/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift index 3fac79de49..b2af6b60bb 100644 --- a/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift +++ b/TelegramUI/PeerChannelMemberCategoriesContextsManager.swift @@ -69,6 +69,16 @@ final class PeerChannelMemberCategoriesContextsManager { } } + func externallyAdded(peerId: PeerId, participant: RenderedChannelParticipant) { + self.impl.with { impl in + for (contextPeerId, context) in impl.contexts { + if contextPeerId == peerId { + context.replayUpdates([(nil, participant)]) + } + } + } + } + func recent(postbox: Postbox, network: Network, peerId: PeerId, searchQuery: String? = nil, requestUpdate: Bool = true, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { let key: PeerChannelMemberContextKey if let searchQuery = searchQuery { @@ -93,8 +103,10 @@ final class PeerChannelMemberCategoriesContextsManager { |> beforeNext { [weak self] (previous, updated) in if let strongSelf = self { strongSelf.impl.with { impl in - for (_, context) in impl.contexts { - context.replayUpdates([(previous, updated)]) + for (contextPeerId, context) in impl.contexts { + if peerId == contextPeerId { + context.replayUpdates([(previous, updated)]) + } } } } @@ -114,8 +126,10 @@ final class PeerChannelMemberCategoriesContextsManager { |> beforeNext { [weak self] result in if let strongSelf = self, let (previous, updated) = result { strongSelf.impl.with { impl in - for (_, context) in impl.contexts { - context.replayUpdates([(previous, updated)]) + for (contextPeerId, context) in impl.contexts { + if peerId == contextPeerId { + context.replayUpdates([(previous, updated)]) + } } } } @@ -135,8 +149,10 @@ final class PeerChannelMemberCategoriesContextsManager { |> beforeNext { [weak self] result in if let strongSelf = self, let (previous, updated) = result { strongSelf.impl.with { impl in - for (_, context) in impl.contexts { - context.replayUpdates([(previous, updated)]) + for (contextPeerId, context) in impl.contexts { + if peerId == contextPeerId { + context.replayUpdates([(previous, updated)]) + } } } } diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 02a34ad840..31976c3617 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -579,6 +579,8 @@ public class PeerMediaCollectionController: TelegramController { } }, present: { c, a in self?.present(c, in: .window(.root), with: a) + }, dismissInput: { + self?.view.endEditing(true) }) } })) diff --git a/TelegramUI/PeerMediaCollectionControllerNode.swift b/TelegramUI/PeerMediaCollectionControllerNode.swift index e8d2354315..ba4e32d8b8 100644 --- a/TelegramUI/PeerMediaCollectionControllerNode.swift +++ b/TelegramUI/PeerMediaCollectionControllerNode.swift @@ -16,14 +16,32 @@ private func historyNodeImplForMode(_ mode: PeerMediaCollectionMode, account: Ac return ChatHistoryGridNode(account: account, peerId: peerId, messageId: messageId, tagMask: .photoOrVideo, controllerInteraction: controllerInteraction) case .file: let node = ChatHistoryListNode(account: account, chatLocation: .peer(peerId), tagMask: .file, messageId: messageId, controllerInteraction: controllerInteraction, selectedMessages: selectedMessages, mode: .list(search: true, reversed: false)) + node.didEndScrolling = { [weak node] in + guard let node = node else { + return + } + fixSearchableListNodeScrolling(node) + } node.preloadPages = true return node case .music: let node = ChatHistoryListNode(account: account, chatLocation: .peer(peerId), tagMask: .music, messageId: messageId, controllerInteraction: controllerInteraction, selectedMessages: selectedMessages, mode: .list(search: true, reversed: false)) + node.didEndScrolling = { [weak node] in + guard let node = node else { + return + } + fixSearchableListNodeScrolling(node) + } node.preloadPages = true return node case .webpage: let node = ChatHistoryListNode(account: account, chatLocation: .peer(peerId), tagMask: .webPage, messageId: messageId, controllerInteraction: controllerInteraction, selectedMessages: selectedMessages, mode: .list(search: true, reversed: false)) + node.didEndScrolling = { [weak node] in + guard let node = node else { + return + } + fixSearchableListNodeScrolling(node) + } node.preloadPages = true return node } @@ -189,7 +207,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) if !searchDisplayController.isDeactivating { - vanillaInsets.top += 20.0 + vanillaInsets.top += layout.statusBarHeight ?? 0.0 } } @@ -375,7 +393,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { if let searchDisplayController = self.searchDisplayController { if !searchDisplayController.isDeactivating { - vanillaInsets.top += 20.0 + vanillaInsets.top += containerLayout.0.statusBarHeight ?? 0.0 } } diff --git a/TelegramUI/PeerSelectionControllerNode.swift b/TelegramUI/PeerSelectionControllerNode.swift index 846d8b0c30..cb2eb8d825 100644 --- a/TelegramUI/PeerSelectionControllerNode.swift +++ b/TelegramUI/PeerSelectionControllerNode.swift @@ -224,15 +224,20 @@ final class PeerSelectionControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: true, openPeer: { [weak self] peerId in + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, onlyWriteable: true, categories: [.cloudContacts, .global], openPeer: { [weak self] peer in if let strongSelf = self { - let _ = (strongSelf.account.postbox.transaction { transaction -> Peer? in - return transaction.getPeer(peerId) - } |> deliverOnMainQueue).start(next: { peer in - if let strongSelf = self, let peer = peer { - strongSelf.requestOpenPeerFromSearch?(peer) - } - }) + switch peer { + case let .peer(peer, _): + let _ = (strongSelf.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peer.id) + } |> deliverOnMainQueue).start(next: { peer in + if let strongSelf = self, let peer = peer { + strongSelf.requestOpenPeerFromSearch?(peer) + } + }) + case let .deviceContact(stableId, contact): + break + } } }), cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { @@ -317,7 +322,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { self?.requestActivateSearch?() } contactListNode.openPeer = { [weak self] peer in - self?.requestOpenPeer?(peer.id) + if case let .peer(peer, _) = peer { + self?.requestOpenPeer?(peer.id) + } } self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) diff --git a/TelegramUI/PhoneInputNode.swift b/TelegramUI/PhoneInputNode.swift index 1f4a9d6da8..a73e499bb2 100644 --- a/TelegramUI/PhoneInputNode.swift +++ b/TelegramUI/PhoneInputNode.swift @@ -116,10 +116,14 @@ final class PhoneInputNode: ASDisplayNode, UITextFieldDelegate { var codeAndNumber: (Int32?, String) { get { var code: Int32? - if let text = self.countryCodeField.textField.text, let number = Int(removePlus(text)) { + if let text = self.countryCodeField.textField.text, text.count <= 4, let number = Int(removePlus(text)) { code = Int32(number) + return (code, cleanPhoneNumber(self.numberField.textField.text)) + } else if let text = self.countryCodeField.textField.text { + return (nil, cleanPhoneNumber(text + (self.numberField.textField.text ?? ""))) + } else { + return (nil, "") } - return (code, cleanPhoneNumber(self.numberField.textField.text)) } set(value) { self.updateNumber("+" + (value.0 == nil ? "" : "\(value.0!)") + value.1) } diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 3e43cf7c28..0b09053341 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -539,10 +539,20 @@ func rawMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference) -> S } func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return chatMessagePhotoInternal(postbox: postbox, photoReference: photoReference) + |> map { _, generate in + return generate + } +} + +func chatMessagePhotoInternal(postbox: Postbox, photoReference: ImageMediaReference) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { let signal = chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference) - return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in - return { arguments in + return signal + |> map { (thumbnailData, fullSizeData, fullSizeComplete) in + return ({ + return nil + }, { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) let drawingRect = arguments.drawingRect @@ -649,7 +659,7 @@ func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference) -> addCorners(context, arguments: arguments) return context - } + }) } } diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index c20ae51e07..dd44e727d0 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -281,7 +281,7 @@ struct PresentationResourcesChat { static func chatEmptyItemIconImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatEmptyItemIconImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/EmptyChatIcon"), color: theme.chat.serviceMessage.serviceMessagePrimaryTextColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Empty Chat/Chat"), color: theme.chat.serviceMessage.serviceMessagePrimaryTextColor) }) } diff --git a/TelegramUI/PresentationResourcesRootController.swift b/TelegramUI/PresentationResourcesRootController.swift index 2a5e4ef56c..698dddb81d 100644 --- a/TelegramUI/PresentationResourcesRootController.swift +++ b/TelegramUI/PresentationResourcesRootController.swift @@ -87,7 +87,8 @@ struct PresentationResourcesRootController { return generateTintedImage(image: UIImage(bundleImageName: "Chat List/SearchIcon"), color: theme.rootController.navigationBar.accentTextColor).flatMap({ image in let factor: CGFloat = 0.8 let size = CGSize(width: floor(image.size.width * factor), height: floor(image.size.height * factor)) - return generateImage(size, rotatedContext: { size, context in + return generateImage(size, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) }) }) diff --git a/TelegramUI/PresentationStrings.swift b/TelegramUI/PresentationStrings.swift index 21da78f2f8..64aa63ffb9 100644 --- a/TelegramUI/PresentationStrings.swift +++ b/TelegramUI/PresentationStrings.swift @@ -167,6 +167,7 @@ public final class PresentationStrings { public func Profile_CreateEncryptedChatOutdatedError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Profile_CreateEncryptedChatOutdatedError, self._Profile_CreateEncryptedChatOutdatedError_r, [_0, _1]) } + public let ContactInfo_PhoneLabelPager: String private let _PINNED_STICKER: String private let _PINNED_STICKER_r: [(Int, NSRange)] public func PINNED_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -249,6 +250,7 @@ public final class PresentationStrings { public let Calls_NoCallsPlaceholder: String public let Conversation_PinMessageAlert_OnlyPin: String public let PasscodeSettings_UnlockWithFaceId: String + public let ContactInfo_Title: String public let ReportPeer_ReasonOther_Send: String public let Conversation_InstantPagePreview: String public let PasscodeSettings_SimplePasscodeHelp: String @@ -412,6 +414,7 @@ public final class PresentationStrings { } public let Month_GenSeptember: String public let PrivacySettings_LastSeenEverybody: String + public let Contacts_NotRegisteredSection: String public let PhotoEditor_BlurToolRadial: String public let TwoStepAuth_PasswordRemoveConfirmation: String public let Channel_EditAdmin_PermissionEditMessages: String @@ -529,6 +532,7 @@ public final class PresentationStrings { public let PhotoEditor_QualityMedium: String public let Privacy_PaymentsClearInfo: String public let PhotoEditor_CurvesRed: String + public let ContactInfo_PhoneLabelWorkFax: String public let Privacy_PaymentsTitle: String public let SocksProxySetup_ProxyType: String private let _Time_PreciseDate_m8: String @@ -1093,6 +1097,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Conversation_RestrictedMediaTimed, self._Conversation_RestrictedMediaTimed_r, [_0]) } public let Login_InfoDeletePhoto: String + public let ContactInfo_BirthdayLabel: String public let TwoStepAuth_RecoveryCodeExpired: String public let AutoDownloadSettings_Channels: String public let AutoDownloadSettings_Contacts: String @@ -1221,6 +1226,7 @@ public final class PresentationStrings { public func ChangePhone_ErrorOccupied(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_ChangePhone_ErrorOccupied, self._ChangePhone_ErrorOccupied_r, [_0]) } + public let ContactInfo_PhoneLabelMain: String public let Clipboard_SendPhoto: String public let Privacy_GroupsAndChannels_CustomShareHelp: String public let KeyCommand_ChatInfo: String @@ -1382,6 +1388,7 @@ public final class PresentationStrings { } public let Conversation_Moderate_Ban: String public let Group_Status: String + public let ContactInfo_PhoneLabelOther: String public let Conversation_InputTextPlaceholder: String public let TwoStepAuth_RecoveryCode: String public let SharedMedia_CategoryDocs: String @@ -1433,6 +1440,7 @@ public final class PresentationStrings { public let SocksProxySetup_SecretPlaceholder: String public let Channel_EditAdmin_PermissinAddAdminOn: String public let WebSearch_GIFs: String + public let Privacy_ChatsTitle: String public let Conversation_SavedMessages: String public let TwoStepAuth_EnterPasswordTitle: String private let _CHANNEL_MESSAGE_GAME: String @@ -1809,6 +1817,7 @@ public final class PresentationStrings { public let Settings_ProxyConnected: String public let ChatSettings_AutoDownloadVoiceMessages: String public let TwoStepAuth_EmailSkip: String + public let Conversation_ViewContactDetails: String public let Conversation_JumpToDate: String public let AutoDownloadSettings_VideoMessagesTitle: String public let CheckoutInfo_ReceiverInfoEmailPlaceholder: String @@ -1878,6 +1887,7 @@ public final class PresentationStrings { public let PrivacyLastSeenSettings_AlwaysShareWith_Placeholder: String public let Channel_Members_Title: String public let Channel_AdminLog_CanDeleteMessages: String + public let Privacy_DeleteDrafts: String public let Group_Setup_TypePrivateHelp: String private let _Notification_PinnedVideoMessage: String private let _Notification_PinnedVideoMessage_r: [(Int, NSRange)] @@ -2065,6 +2075,7 @@ public final class PresentationStrings { public let Channel_BanUser_BlockFor: String public let Call_StatusConnecting: String public let AutoNightTheme_NotAvailable: String + public let PrivateDataSettings_Title: String public let Bot_Start: String private let _Channel_AdminLog_MessageChangedGroupAbout: String private let _Channel_AdminLog_MessageChangedGroupAbout_r: [(Int, NSRange)] @@ -2147,6 +2158,7 @@ public final class PresentationStrings { public let TwoStepAuth_RecoveryTitle: String public let WatchRemote_AlertOpen: String public let ExplicitContent_AlertChannel: String + public let ContactInfo_PhoneLabelMobile: String public let Widget_AuthRequired: String private let _ForwardedAuthors2: String private let _ForwardedAuthors2_r: [(Int, NSRange)] @@ -2537,6 +2549,7 @@ public final class PresentationStrings { public func Channel_AdminLog_MessageRestrictedNewSetting(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Channel_AdminLog_MessageRestrictedNewSetting, self._Channel_AdminLog_MessageRestrictedNewSetting_r, [_0]) } + public let ContactInfo_PhoneLabelHome: String public let GroupInfo_DeleteAndExitConfirmation: String public let TwoStepAuth_EmailInvalid: String public let Privacy_ContactsTitle: String @@ -2557,6 +2570,7 @@ public final class PresentationStrings { public func CHAT_TITLE_EDITED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_CHAT_TITLE_EDITED, self._CHAT_TITLE_EDITED_r, [_1, _2]) } + public let ContactInfo_PhoneLabelHomeFax: String private let _NetworkUsageSettings_WifiUsageSince: String private let _NetworkUsageSettings_WifiUsageSince_r: [(Int, NSRange)] public func NetworkUsageSettings_WifiUsageSince(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2621,6 +2635,7 @@ public final class PresentationStrings { public func LiveLocationUpdated_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_LiveLocationUpdated_TodayAt, self._LiveLocationUpdated_TodayAt_r, [_0]) } + public let ContactInfo_PhoneLabelWork: String public let ChatSettings_ConnectionType_UseSocks5: String public let Cache_ClearNone: String public let SecretTimer_VideoDescription: String @@ -2688,6 +2703,7 @@ public final class PresentationStrings { public let ChatSettings_Stickers: String public let Camera_FlashOff: String public let TwoStepAuth_Title: String + public let PrivacySettings_DataSettingsHelp: String public let Checkout_ErrorProviderAccountTimeout: String public let TwoStepAuth_SetupPasswordEnterPasswordChange: String public let WebSearch_Images: String @@ -2848,6 +2864,7 @@ public final class PresentationStrings { public func Call_PrivacyErrorMessage(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Call_PrivacyErrorMessage, self._Call_PrivacyErrorMessage_r, [_0]) } + public let PrivacySettings_DataSettings: String public let ChangePhoneNumberNumber_Title: String public let TwoStepAuth_EnterPasswordInvalid: String public let DialogList_SearchSectionMessages: String @@ -2997,6 +3014,7 @@ public final class PresentationStrings { public let GroupInfo_Sound: String public let Channel_EditAdmin_PermissionBanUsers: String public let InfoPlist_NSCameraUsageDescription: String + public let ContactInfo_Job: String public let Wallpaper_PhotoLibrary: String public let Settings_About: String public let Privacy_Calls_IntegrationHelp: String @@ -3099,6 +3117,7 @@ public final class PresentationStrings { public let Channel_AboutItem: String public let PhotoEditor_CurvesGreen: String public let Month_GenJuly: String + public let ContactInfo_URLLabelHomepage: String private let _DialogList_SingleUploadingFileSuffix: String private let _DialogList_SingleUploadingFileSuffix_r: [(Int, NSRange)] public func DialogList_SingleUploadingFileSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -3191,754 +3210,6 @@ public final class PresentationStrings { public let PrivacySettings_PasscodeAndFaceId: String public let Settings_ChatBackground: String public let TermsOfService_Confirm: String - private let _Watch_UserInfo_Mute_zero: String - private let _Watch_UserInfo_Mute_one: String - private let _Watch_UserInfo_Mute_two: String - private let _Watch_UserInfo_Mute_few: String - private let _Watch_UserInfo_Mute_many: String - private let _Watch_UserInfo_Mute_other: String - public func Watch_UserInfo_Mute(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Watch_UserInfo_Mute_zero, "\(value)") - case .one: - return String(format: self._Watch_UserInfo_Mute_one, "\(value)") - case .two: - return String(format: self._Watch_UserInfo_Mute_two, "\(value)") - case .few: - return String(format: self._Watch_UserInfo_Mute_few, "\(value)") - case .many: - return String(format: self._Watch_UserInfo_Mute_many, "\(value)") - case .other: - return String(format: self._Watch_UserInfo_Mute_other, "\(value)") - } - } - private let _SharedMedia_Link_zero: String - private let _SharedMedia_Link_one: String - private let _SharedMedia_Link_two: String - private let _SharedMedia_Link_few: String - private let _SharedMedia_Link_many: String - private let _SharedMedia_Link_other: String - public func SharedMedia_Link(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._SharedMedia_Link_zero, "\(value)") - case .one: - return String(format: self._SharedMedia_Link_one, "\(value)") - case .two: - return String(format: self._SharedMedia_Link_two, "\(value)") - case .few: - return String(format: self._SharedMedia_Link_few, "\(value)") - case .many: - return String(format: self._SharedMedia_Link_many, "\(value)") - case .other: - return String(format: self._SharedMedia_Link_other, "\(value)") - } - } - private let _MessageTimer_Days_zero: String - private let _MessageTimer_Days_one: String - private let _MessageTimer_Days_two: String - private let _MessageTimer_Days_few: String - private let _MessageTimer_Days_many: String - private let _MessageTimer_Days_other: String - public func MessageTimer_Days(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_Days_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_Days_one, "\(value)") - case .two: - return String(format: self._MessageTimer_Days_two, "\(value)") - case .few: - return String(format: self._MessageTimer_Days_few, "\(value)") - case .many: - return String(format: self._MessageTimer_Days_many, "\(value)") - case .other: - return String(format: self._MessageTimer_Days_other, "\(value)") - } - } - private let _LiveLocationUpdated_MinutesAgo_zero: String - private let _LiveLocationUpdated_MinutesAgo_one: String - private let _LiveLocationUpdated_MinutesAgo_two: String - private let _LiveLocationUpdated_MinutesAgo_few: String - private let _LiveLocationUpdated_MinutesAgo_many: String - private let _LiveLocationUpdated_MinutesAgo_other: String - public func LiveLocationUpdated_MinutesAgo(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._LiveLocationUpdated_MinutesAgo_zero, "\(value)") - case .one: - return String(format: self._LiveLocationUpdated_MinutesAgo_one, "\(value)") - case .two: - return String(format: self._LiveLocationUpdated_MinutesAgo_two, "\(value)") - case .few: - return String(format: self._LiveLocationUpdated_MinutesAgo_few, "\(value)") - case .many: - return String(format: self._LiveLocationUpdated_MinutesAgo_many, "\(value)") - case .other: - return String(format: self._LiveLocationUpdated_MinutesAgo_other, "\(value)") - } - } - private let _ForwardedAudios_zero: String - private let _ForwardedAudios_one: String - private let _ForwardedAudios_two: String - private let _ForwardedAudios_few: String - private let _ForwardedAudios_many: String - private let _ForwardedAudios_other: String - public func ForwardedAudios(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedAudios_zero, "\(value)") - case .one: - return String(format: self._ForwardedAudios_one, "\(value)") - case .two: - return String(format: self._ForwardedAudios_two, "\(value)") - case .few: - return String(format: self._ForwardedAudios_few, "\(value)") - case .many: - return String(format: self._ForwardedAudios_many, "\(value)") - case .other: - return String(format: self._ForwardedAudios_other, "\(value)") - } - } - private let _Invitation_Members_zero: String - private let _Invitation_Members_one: String - private let _Invitation_Members_two: String - private let _Invitation_Members_few: String - private let _Invitation_Members_many: String - private let _Invitation_Members_other: String - public func Invitation_Members(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Invitation_Members_zero, "\(value)") - case .one: - return String(format: self._Invitation_Members_one, "\(value)") - case .two: - return String(format: self._Invitation_Members_two, "\(value)") - case .few: - return String(format: self._Invitation_Members_few, "\(value)") - case .many: - return String(format: self._Invitation_Members_many, "\(value)") - case .other: - return String(format: self._Invitation_Members_other, "\(value)") - } - } - private let _ForwardedStickers_zero: String - private let _ForwardedStickers_one: String - private let _ForwardedStickers_two: String - private let _ForwardedStickers_few: String - private let _ForwardedStickers_many: String - private let _ForwardedStickers_other: String - public func ForwardedStickers(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedStickers_zero, "\(value)") - case .one: - return String(format: self._ForwardedStickers_one, "\(value)") - case .two: - return String(format: self._ForwardedStickers_two, "\(value)") - case .few: - return String(format: self._ForwardedStickers_few, "\(value)") - case .many: - return String(format: self._ForwardedStickers_many, "\(value)") - case .other: - return String(format: self._ForwardedStickers_other, "\(value)") - } - } - private let _GroupInfo_ParticipantCount_zero: String - private let _GroupInfo_ParticipantCount_one: String - private let _GroupInfo_ParticipantCount_two: String - private let _GroupInfo_ParticipantCount_few: String - private let _GroupInfo_ParticipantCount_many: String - private let _GroupInfo_ParticipantCount_other: String - public func GroupInfo_ParticipantCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._GroupInfo_ParticipantCount_zero, "\(value)") - case .one: - return String(format: self._GroupInfo_ParticipantCount_one, "\(value)") - case .two: - return String(format: self._GroupInfo_ParticipantCount_two, "\(value)") - case .few: - return String(format: self._GroupInfo_ParticipantCount_few, "\(value)") - case .many: - return String(format: self._GroupInfo_ParticipantCount_many, "\(value)") - case .other: - return String(format: self._GroupInfo_ParticipantCount_other, "\(value)") - } - } - private let _DialogList_LiveLocationChatsCount_zero: String - private let _DialogList_LiveLocationChatsCount_one: String - private let _DialogList_LiveLocationChatsCount_two: String - private let _DialogList_LiveLocationChatsCount_few: String - private let _DialogList_LiveLocationChatsCount_many: String - private let _DialogList_LiveLocationChatsCount_other: String - public func DialogList_LiveLocationChatsCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._DialogList_LiveLocationChatsCount_zero, "\(value)") - case .one: - return String(format: self._DialogList_LiveLocationChatsCount_one, "\(value)") - case .two: - return String(format: self._DialogList_LiveLocationChatsCount_two, "\(value)") - case .few: - return String(format: self._DialogList_LiveLocationChatsCount_few, "\(value)") - case .many: - return String(format: self._DialogList_LiveLocationChatsCount_many, "\(value)") - case .other: - return String(format: self._DialogList_LiveLocationChatsCount_other, "\(value)") - } - } - private let _MessageTimer_Years_zero: String - private let _MessageTimer_Years_one: String - private let _MessageTimer_Years_two: String - private let _MessageTimer_Years_few: String - private let _MessageTimer_Years_many: String - private let _MessageTimer_Years_other: String - public func MessageTimer_Years(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_Years_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_Years_one, "\(value)") - case .two: - return String(format: self._MessageTimer_Years_two, "\(value)") - case .few: - return String(format: self._MessageTimer_Years_few, "\(value)") - case .many: - return String(format: self._MessageTimer_Years_many, "\(value)") - case .other: - return String(format: self._MessageTimer_Years_other, "\(value)") - } - } - private let _ServiceMessage_GameScoreExtended_zero: String - private let _ServiceMessage_GameScoreExtended_one: String - private let _ServiceMessage_GameScoreExtended_two: String - private let _ServiceMessage_GameScoreExtended_few: String - private let _ServiceMessage_GameScoreExtended_many: String - private let _ServiceMessage_GameScoreExtended_other: String - public func ServiceMessage_GameScoreExtended(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ServiceMessage_GameScoreExtended_zero, "\(value)") - case .one: - return String(format: self._ServiceMessage_GameScoreExtended_one, "\(value)") - case .two: - return String(format: self._ServiceMessage_GameScoreExtended_two, "\(value)") - case .few: - return String(format: self._ServiceMessage_GameScoreExtended_few, "\(value)") - case .many: - return String(format: self._ServiceMessage_GameScoreExtended_many, "\(value)") - case .other: - return String(format: self._ServiceMessage_GameScoreExtended_other, "\(value)") - } - } - private let _InviteText_ContactsCount_zero: String - private let _InviteText_ContactsCount_one: String - private let _InviteText_ContactsCount_two: String - private let _InviteText_ContactsCount_few: String - private let _InviteText_ContactsCount_many: String - private let _InviteText_ContactsCount_other: String - public func InviteText_ContactsCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._InviteText_ContactsCount_zero, "\(value)") - case .one: - return String(format: self._InviteText_ContactsCount_one, "\(value)") - case .two: - return String(format: self._InviteText_ContactsCount_two, "\(value)") - case .few: - return String(format: self._InviteText_ContactsCount_few, "\(value)") - case .many: - return String(format: self._InviteText_ContactsCount_many, "\(value)") - case .other: - return String(format: self._InviteText_ContactsCount_other, "\(value)") - } - } - private let _MessageTimer_ShortMinutes_zero: String - private let _MessageTimer_ShortMinutes_one: String - private let _MessageTimer_ShortMinutes_two: String - private let _MessageTimer_ShortMinutes_few: String - private let _MessageTimer_ShortMinutes_many: String - private let _MessageTimer_ShortMinutes_other: String - public func MessageTimer_ShortMinutes(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_ShortMinutes_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_ShortMinutes_one, "\(value)") - case .two: - return String(format: self._MessageTimer_ShortMinutes_two, "\(value)") - case .few: - return String(format: self._MessageTimer_ShortMinutes_few, "\(value)") - case .many: - return String(format: self._MessageTimer_ShortMinutes_many, "\(value)") - case .other: - return String(format: self._MessageTimer_ShortMinutes_other, "\(value)") - } - } - private let _Call_Seconds_zero: String - private let _Call_Seconds_one: String - private let _Call_Seconds_two: String - private let _Call_Seconds_few: String - private let _Call_Seconds_many: String - private let _Call_Seconds_other: String - public func Call_Seconds(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Call_Seconds_zero, "\(value)") - case .one: - return String(format: self._Call_Seconds_one, "\(value)") - case .two: - return String(format: self._Call_Seconds_two, "\(value)") - case .few: - return String(format: self._Call_Seconds_few, "\(value)") - case .many: - return String(format: self._Call_Seconds_many, "\(value)") - case .other: - return String(format: self._Call_Seconds_other, "\(value)") - } - } - private let _Watch_LastSeen_MinutesAgo_zero: String - private let _Watch_LastSeen_MinutesAgo_one: String - private let _Watch_LastSeen_MinutesAgo_two: String - private let _Watch_LastSeen_MinutesAgo_few: String - private let _Watch_LastSeen_MinutesAgo_many: String - private let _Watch_LastSeen_MinutesAgo_other: String - public func Watch_LastSeen_MinutesAgo(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Watch_LastSeen_MinutesAgo_zero, "\(value)") - case .one: - return String(format: self._Watch_LastSeen_MinutesAgo_one, "\(value)") - case .two: - return String(format: self._Watch_LastSeen_MinutesAgo_two, "\(value)") - case .few: - return String(format: self._Watch_LastSeen_MinutesAgo_few, "\(value)") - case .many: - return String(format: self._Watch_LastSeen_MinutesAgo_many, "\(value)") - case .other: - return String(format: self._Watch_LastSeen_MinutesAgo_other, "\(value)") - } - } - private let _Media_SharePhoto_zero: String - private let _Media_SharePhoto_one: String - private let _Media_SharePhoto_two: String - private let _Media_SharePhoto_few: String - private let _Media_SharePhoto_many: String - private let _Media_SharePhoto_other: String - public func Media_SharePhoto(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Media_SharePhoto_zero, "\(value)") - case .one: - return String(format: self._Media_SharePhoto_one, "\(value)") - case .two: - return String(format: self._Media_SharePhoto_two, "\(value)") - case .few: - return String(format: self._Media_SharePhoto_few, "\(value)") - case .many: - return String(format: self._Media_SharePhoto_many, "\(value)") - case .other: - return String(format: self._Media_SharePhoto_other, "\(value)") - } - } - private let _Notification_GameScoreSelfSimple_zero: String - private let _Notification_GameScoreSelfSimple_one: String - private let _Notification_GameScoreSelfSimple_two: String - private let _Notification_GameScoreSelfSimple_few: String - private let _Notification_GameScoreSelfSimple_many: String - private let _Notification_GameScoreSelfSimple_other: String - public func Notification_GameScoreSelfSimple(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Notification_GameScoreSelfSimple_zero, "\(value)") - case .one: - return String(format: self._Notification_GameScoreSelfSimple_one, "\(value)") - case .two: - return String(format: self._Notification_GameScoreSelfSimple_two, "\(value)") - case .few: - return String(format: self._Notification_GameScoreSelfSimple_few, "\(value)") - case .many: - return String(format: self._Notification_GameScoreSelfSimple_many, "\(value)") - case .other: - return String(format: self._Notification_GameScoreSelfSimple_other, "\(value)") - } - } - private let _SharedMedia_Photo_zero: String - private let _SharedMedia_Photo_one: String - private let _SharedMedia_Photo_two: String - private let _SharedMedia_Photo_few: String - private let _SharedMedia_Photo_many: String - private let _SharedMedia_Photo_other: String - public func SharedMedia_Photo(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._SharedMedia_Photo_zero, "\(value)") - case .one: - return String(format: self._SharedMedia_Photo_one, "\(value)") - case .two: - return String(format: self._SharedMedia_Photo_two, "\(value)") - case .few: - return String(format: self._SharedMedia_Photo_few, "\(value)") - case .many: - return String(format: self._SharedMedia_Photo_many, "\(value)") - case .other: - return String(format: self._SharedMedia_Photo_other, "\(value)") - } - } - private let _StickerPack_RemoveStickerCount_zero: String - private let _StickerPack_RemoveStickerCount_one: String - private let _StickerPack_RemoveStickerCount_two: String - private let _StickerPack_RemoveStickerCount_few: String - private let _StickerPack_RemoveStickerCount_many: String - private let _StickerPack_RemoveStickerCount_other: String - public func StickerPack_RemoveStickerCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._StickerPack_RemoveStickerCount_zero, "\(value)") - case .one: - return String(format: self._StickerPack_RemoveStickerCount_one, "\(value)") - case .two: - return String(format: self._StickerPack_RemoveStickerCount_two, "\(value)") - case .few: - return String(format: self._StickerPack_RemoveStickerCount_few, "\(value)") - case .many: - return String(format: self._StickerPack_RemoveStickerCount_many, "\(value)") - case .other: - return String(format: self._StickerPack_RemoveStickerCount_other, "\(value)") - } - } - private let _MessageTimer_Minutes_zero: String - private let _MessageTimer_Minutes_one: String - private let _MessageTimer_Minutes_two: String - private let _MessageTimer_Minutes_few: String - private let _MessageTimer_Minutes_many: String - private let _MessageTimer_Minutes_other: String - public func MessageTimer_Minutes(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_Minutes_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_Minutes_one, "\(value)") - case .two: - return String(format: self._MessageTimer_Minutes_two, "\(value)") - case .few: - return String(format: self._MessageTimer_Minutes_few, "\(value)") - case .many: - return String(format: self._MessageTimer_Minutes_many, "\(value)") - case .other: - return String(format: self._MessageTimer_Minutes_other, "\(value)") - } - } - private let _ForwardedFiles_zero: String - private let _ForwardedFiles_one: String - private let _ForwardedFiles_two: String - private let _ForwardedFiles_few: String - private let _ForwardedFiles_many: String - private let _ForwardedFiles_other: String - public func ForwardedFiles(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedFiles_zero, "\(value)") - case .one: - return String(format: self._ForwardedFiles_one, "\(value)") - case .two: - return String(format: self._ForwardedFiles_two, "\(value)") - case .few: - return String(format: self._ForwardedFiles_few, "\(value)") - case .many: - return String(format: self._ForwardedFiles_many, "\(value)") - case .other: - return String(format: self._ForwardedFiles_other, "\(value)") - } - } - private let _ForwardedGifs_zero: String - private let _ForwardedGifs_one: String - private let _ForwardedGifs_two: String - private let _ForwardedGifs_few: String - private let _ForwardedGifs_many: String - private let _ForwardedGifs_other: String - public func ForwardedGifs(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedGifs_zero, "\(value)") - case .one: - return String(format: self._ForwardedGifs_one, "\(value)") - case .two: - return String(format: self._ForwardedGifs_two, "\(value)") - case .few: - return String(format: self._ForwardedGifs_few, "\(value)") - case .many: - return String(format: self._ForwardedGifs_many, "\(value)") - case .other: - return String(format: self._ForwardedGifs_other, "\(value)") - } - } - private let _Conversation_StatusMembers_zero: String - private let _Conversation_StatusMembers_one: String - private let _Conversation_StatusMembers_two: String - private let _Conversation_StatusMembers_few: String - private let _Conversation_StatusMembers_many: String - private let _Conversation_StatusMembers_other: String - public func Conversation_StatusMembers(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Conversation_StatusMembers_zero, "\(value)") - case .one: - return String(format: self._Conversation_StatusMembers_one, "\(value)") - case .two: - return String(format: self._Conversation_StatusMembers_two, "\(value)") - case .few: - return String(format: self._Conversation_StatusMembers_few, "\(value)") - case .many: - return String(format: self._Conversation_StatusMembers_many, "\(value)") - case .other: - return String(format: self._Conversation_StatusMembers_other, "\(value)") - } - } - private let _MuteExpires_Hours_zero: String - private let _MuteExpires_Hours_one: String - private let _MuteExpires_Hours_two: String - private let _MuteExpires_Hours_few: String - private let _MuteExpires_Hours_many: String - private let _MuteExpires_Hours_other: String - public func MuteExpires_Hours(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MuteExpires_Hours_zero, "\(value)") - case .one: - return String(format: self._MuteExpires_Hours_one, "\(value)") - case .two: - return String(format: self._MuteExpires_Hours_two, "\(value)") - case .few: - return String(format: self._MuteExpires_Hours_few, "\(value)") - case .many: - return String(format: self._MuteExpires_Hours_many, "\(value)") - case .other: - return String(format: self._MuteExpires_Hours_other, "\(value)") - } - } - private let _Contacts_ImportersCount_zero: String - private let _Contacts_ImportersCount_one: String - private let _Contacts_ImportersCount_two: String - private let _Contacts_ImportersCount_few: String - private let _Contacts_ImportersCount_many: String - private let _Contacts_ImportersCount_other: String - public func Contacts_ImportersCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Contacts_ImportersCount_zero, "\(value)") - case .one: - return String(format: self._Contacts_ImportersCount_one, "\(value)") - case .two: - return String(format: self._Contacts_ImportersCount_two, "\(value)") - case .few: - return String(format: self._Contacts_ImportersCount_few, "\(value)") - case .many: - return String(format: self._Contacts_ImportersCount_many, "\(value)") - case .other: - return String(format: self._Contacts_ImportersCount_other, "\(value)") - } - } - private let _MuteExpires_Days_zero: String - private let _MuteExpires_Days_one: String - private let _MuteExpires_Days_two: String - private let _MuteExpires_Days_few: String - private let _MuteExpires_Days_many: String - private let _MuteExpires_Days_other: String - public func MuteExpires_Days(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MuteExpires_Days_zero, "\(value)") - case .one: - return String(format: self._MuteExpires_Days_one, "\(value)") - case .two: - return String(format: self._MuteExpires_Days_two, "\(value)") - case .few: - return String(format: self._MuteExpires_Days_few, "\(value)") - case .many: - return String(format: self._MuteExpires_Days_many, "\(value)") - case .other: - return String(format: self._MuteExpires_Days_other, "\(value)") - } - } - private let _MuteFor_Hours_zero: String - private let _MuteFor_Hours_one: String - private let _MuteFor_Hours_two: String - private let _MuteFor_Hours_few: String - private let _MuteFor_Hours_many: String - private let _MuteFor_Hours_other: String - public func MuteFor_Hours(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MuteFor_Hours_zero, "\(value)") - case .one: - return String(format: self._MuteFor_Hours_one, "\(value)") - case .two: - return String(format: self._MuteFor_Hours_two, "\(value)") - case .few: - return String(format: self._MuteFor_Hours_few, "\(value)") - case .many: - return String(format: self._MuteFor_Hours_many, "\(value)") - case .other: - return String(format: self._MuteFor_Hours_other, "\(value)") - } - } - private let _MuteFor_Days_zero: String - private let _MuteFor_Days_one: String - private let _MuteFor_Days_two: String - private let _MuteFor_Days_few: String - private let _MuteFor_Days_many: String - private let _MuteFor_Days_other: String - public func MuteFor_Days(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MuteFor_Days_zero, "\(value)") - case .one: - return String(format: self._MuteFor_Days_one, "\(value)") - case .two: - return String(format: self._MuteFor_Days_two, "\(value)") - case .few: - return String(format: self._MuteFor_Days_few, "\(value)") - case .many: - return String(format: self._MuteFor_Days_many, "\(value)") - case .other: - return String(format: self._MuteFor_Days_other, "\(value)") - } - } - private let _ServiceMessage_GameScoreSelfExtended_zero: String - private let _ServiceMessage_GameScoreSelfExtended_one: String - private let _ServiceMessage_GameScoreSelfExtended_two: String - private let _ServiceMessage_GameScoreSelfExtended_few: String - private let _ServiceMessage_GameScoreSelfExtended_many: String - private let _ServiceMessage_GameScoreSelfExtended_other: String - public func ServiceMessage_GameScoreSelfExtended(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ServiceMessage_GameScoreSelfExtended_zero, "\(value)") - case .one: - return String(format: self._ServiceMessage_GameScoreSelfExtended_one, "\(value)") - case .two: - return String(format: self._ServiceMessage_GameScoreSelfExtended_two, "\(value)") - case .few: - return String(format: self._ServiceMessage_GameScoreSelfExtended_few, "\(value)") - case .many: - return String(format: self._ServiceMessage_GameScoreSelfExtended_many, "\(value)") - case .other: - return String(format: self._ServiceMessage_GameScoreSelfExtended_other, "\(value)") - } - } - private let _MessageTimer_ShortHours_zero: String - private let _MessageTimer_ShortHours_one: String - private let _MessageTimer_ShortHours_two: String - private let _MessageTimer_ShortHours_few: String - private let _MessageTimer_ShortHours_many: String - private let _MessageTimer_ShortHours_other: String - public func MessageTimer_ShortHours(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_ShortHours_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_ShortHours_one, "\(value)") - case .two: - return String(format: self._MessageTimer_ShortHours_two, "\(value)") - case .few: - return String(format: self._MessageTimer_ShortHours_few, "\(value)") - case .many: - return String(format: self._MessageTimer_ShortHours_many, "\(value)") - case .other: - return String(format: self._MessageTimer_ShortHours_other, "\(value)") - } - } - private let _MessageTimer_ShortDays_zero: String - private let _MessageTimer_ShortDays_one: String - private let _MessageTimer_ShortDays_two: String - private let _MessageTimer_ShortDays_few: String - private let _MessageTimer_ShortDays_many: String - private let _MessageTimer_ShortDays_other: String - public func MessageTimer_ShortDays(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_ShortDays_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_ShortDays_one, "\(value)") - case .two: - return String(format: self._MessageTimer_ShortDays_two, "\(value)") - case .few: - return String(format: self._MessageTimer_ShortDays_few, "\(value)") - case .many: - return String(format: self._MessageTimer_ShortDays_many, "\(value)") - case .other: - return String(format: self._MessageTimer_ShortDays_other, "\(value)") - } - } - private let _Conversation_LiveLocationMembersCount_zero: String - private let _Conversation_LiveLocationMembersCount_one: String - private let _Conversation_LiveLocationMembersCount_two: String - private let _Conversation_LiveLocationMembersCount_few: String - private let _Conversation_LiveLocationMembersCount_many: String - private let _Conversation_LiveLocationMembersCount_other: String - public func Conversation_LiveLocationMembersCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Conversation_LiveLocationMembersCount_zero, "\(value)") - case .one: - return String(format: self._Conversation_LiveLocationMembersCount_one, "\(value)") - case .two: - return String(format: self._Conversation_LiveLocationMembersCount_two, "\(value)") - case .few: - return String(format: self._Conversation_LiveLocationMembersCount_few, "\(value)") - case .many: - return String(format: self._Conversation_LiveLocationMembersCount_many, "\(value)") - case .other: - return String(format: self._Conversation_LiveLocationMembersCount_other, "\(value)") - } - } - private let _Notification_GameScoreExtended_zero: String - private let _Notification_GameScoreExtended_one: String - private let _Notification_GameScoreExtended_two: String - private let _Notification_GameScoreExtended_few: String - private let _Notification_GameScoreExtended_many: String - private let _Notification_GameScoreExtended_other: String - public func Notification_GameScoreExtended(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Notification_GameScoreExtended_zero, "\(value)") - case .one: - return String(format: self._Notification_GameScoreExtended_one, "\(value)") - case .two: - return String(format: self._Notification_GameScoreExtended_two, "\(value)") - case .few: - return String(format: self._Notification_GameScoreExtended_few, "\(value)") - case .many: - return String(format: self._Notification_GameScoreExtended_many, "\(value)") - case .other: - return String(format: self._Notification_GameScoreExtended_other, "\(value)") - } - } - private let _ForwardedAuthorsOthers_zero: String - private let _ForwardedAuthorsOthers_one: String - private let _ForwardedAuthorsOthers_two: String - private let _ForwardedAuthorsOthers_few: String - private let _ForwardedAuthorsOthers_many: String - private let _ForwardedAuthorsOthers_other: String - public func ForwardedAuthorsOthers(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedAuthorsOthers_zero, "\(value)") - case .one: - return String(format: self._ForwardedAuthorsOthers_one, "\(value)") - case .two: - return String(format: self._ForwardedAuthorsOthers_two, "\(value)") - case .few: - return String(format: self._ForwardedAuthorsOthers_few, "\(value)") - case .many: - return String(format: self._ForwardedAuthorsOthers_many, "\(value)") - case .other: - return String(format: self._ForwardedAuthorsOthers_other, "\(value)") - } - } private let _Media_ShareItem_zero: String private let _Media_ShareItem_one: String private let _Media_ShareItem_two: String @@ -3961,468 +3232,6 @@ public final class PresentationStrings { return String(format: self._Media_ShareItem_other, "\(value)") } } - private let _SharedMedia_Video_zero: String - private let _SharedMedia_Video_one: String - private let _SharedMedia_Video_two: String - private let _SharedMedia_Video_few: String - private let _SharedMedia_Video_many: String - private let _SharedMedia_Video_other: String - public func SharedMedia_Video(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._SharedMedia_Video_zero, "\(value)") - case .one: - return String(format: self._SharedMedia_Video_one, "\(value)") - case .two: - return String(format: self._SharedMedia_Video_two, "\(value)") - case .few: - return String(format: self._SharedMedia_Video_few, "\(value)") - case .many: - return String(format: self._SharedMedia_Video_many, "\(value)") - case .other: - return String(format: self._SharedMedia_Video_other, "\(value)") - } - } - private let _Forward_ConfirmMultipleFiles_zero: String - private let _Forward_ConfirmMultipleFiles_one: String - private let _Forward_ConfirmMultipleFiles_two: String - private let _Forward_ConfirmMultipleFiles_few: String - private let _Forward_ConfirmMultipleFiles_many: String - private let _Forward_ConfirmMultipleFiles_other: String - public func Forward_ConfirmMultipleFiles(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Forward_ConfirmMultipleFiles_zero, "\(value)") - case .one: - return String(format: self._Forward_ConfirmMultipleFiles_one, "\(value)") - case .two: - return String(format: self._Forward_ConfirmMultipleFiles_two, "\(value)") - case .few: - return String(format: self._Forward_ConfirmMultipleFiles_few, "\(value)") - case .many: - return String(format: self._Forward_ConfirmMultipleFiles_many, "\(value)") - case .other: - return String(format: self._Forward_ConfirmMultipleFiles_other, "\(value)") - } - } - private let _MessageTimer_ShortWeeks_zero: String - private let _MessageTimer_ShortWeeks_one: String - private let _MessageTimer_ShortWeeks_two: String - private let _MessageTimer_ShortWeeks_few: String - private let _MessageTimer_ShortWeeks_many: String - private let _MessageTimer_ShortWeeks_other: String - public func MessageTimer_ShortWeeks(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_ShortWeeks_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_ShortWeeks_one, "\(value)") - case .two: - return String(format: self._MessageTimer_ShortWeeks_two, "\(value)") - case .few: - return String(format: self._MessageTimer_ShortWeeks_few, "\(value)") - case .many: - return String(format: self._MessageTimer_ShortWeeks_many, "\(value)") - case .other: - return String(format: self._MessageTimer_ShortWeeks_other, "\(value)") - } - } - private let _Call_Minutes_zero: String - private let _Call_Minutes_one: String - private let _Call_Minutes_two: String - private let _Call_Minutes_few: String - private let _Call_Minutes_many: String - private let _Call_Minutes_other: String - public func Call_Minutes(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Call_Minutes_zero, "\(value)") - case .one: - return String(format: self._Call_Minutes_one, "\(value)") - case .two: - return String(format: self._Call_Minutes_two, "\(value)") - case .few: - return String(format: self._Call_Minutes_few, "\(value)") - case .many: - return String(format: self._Call_Minutes_many, "\(value)") - case .other: - return String(format: self._Call_Minutes_other, "\(value)") - } - } - private let _LastSeen_MinutesAgo_zero: String - private let _LastSeen_MinutesAgo_one: String - private let _LastSeen_MinutesAgo_two: String - private let _LastSeen_MinutesAgo_few: String - private let _LastSeen_MinutesAgo_many: String - private let _LastSeen_MinutesAgo_other: String - public func LastSeen_MinutesAgo(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._LastSeen_MinutesAgo_zero, "\(value)") - case .one: - return String(format: self._LastSeen_MinutesAgo_one, "\(value)") - case .two: - return String(format: self._LastSeen_MinutesAgo_two, "\(value)") - case .few: - return String(format: self._LastSeen_MinutesAgo_few, "\(value)") - case .many: - return String(format: self._LastSeen_MinutesAgo_many, "\(value)") - case .other: - return String(format: self._LastSeen_MinutesAgo_other, "\(value)") - } - } - private let _StickerPack_AddMaskCount_zero: String - private let _StickerPack_AddMaskCount_one: String - private let _StickerPack_AddMaskCount_two: String - private let _StickerPack_AddMaskCount_few: String - private let _StickerPack_AddMaskCount_many: String - private let _StickerPack_AddMaskCount_other: String - public func StickerPack_AddMaskCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._StickerPack_AddMaskCount_zero, "\(value)") - case .one: - return String(format: self._StickerPack_AddMaskCount_one, "\(value)") - case .two: - return String(format: self._StickerPack_AddMaskCount_two, "\(value)") - case .few: - return String(format: self._StickerPack_AddMaskCount_few, "\(value)") - case .many: - return String(format: self._StickerPack_AddMaskCount_many, "\(value)") - case .other: - return String(format: self._StickerPack_AddMaskCount_other, "\(value)") - } - } - private let _Notification_GameScoreSimple_zero: String - private let _Notification_GameScoreSimple_one: String - private let _Notification_GameScoreSimple_two: String - private let _Notification_GameScoreSimple_few: String - private let _Notification_GameScoreSimple_many: String - private let _Notification_GameScoreSimple_other: String - public func Notification_GameScoreSimple(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Notification_GameScoreSimple_zero, "\(value)") - case .one: - return String(format: self._Notification_GameScoreSimple_one, "\(value)") - case .two: - return String(format: self._Notification_GameScoreSimple_two, "\(value)") - case .few: - return String(format: self._Notification_GameScoreSimple_few, "\(value)") - case .many: - return String(format: self._Notification_GameScoreSimple_many, "\(value)") - case .other: - return String(format: self._Notification_GameScoreSimple_other, "\(value)") - } - } - private let _Notification_GameScoreSelfExtended_zero: String - private let _Notification_GameScoreSelfExtended_one: String - private let _Notification_GameScoreSelfExtended_two: String - private let _Notification_GameScoreSelfExtended_few: String - private let _Notification_GameScoreSelfExtended_many: String - private let _Notification_GameScoreSelfExtended_other: String - public func Notification_GameScoreSelfExtended(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Notification_GameScoreSelfExtended_zero, "\(value)") - case .one: - return String(format: self._Notification_GameScoreSelfExtended_one, "\(value)") - case .two: - return String(format: self._Notification_GameScoreSelfExtended_two, "\(value)") - case .few: - return String(format: self._Notification_GameScoreSelfExtended_few, "\(value)") - case .many: - return String(format: self._Notification_GameScoreSelfExtended_many, "\(value)") - case .other: - return String(format: self._Notification_GameScoreSelfExtended_other, "\(value)") - } - } - private let _SharedMedia_File_zero: String - private let _SharedMedia_File_one: String - private let _SharedMedia_File_two: String - private let _SharedMedia_File_few: String - private let _SharedMedia_File_many: String - private let _SharedMedia_File_other: String - public func SharedMedia_File(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._SharedMedia_File_zero, "\(value)") - case .one: - return String(format: self._SharedMedia_File_one, "\(value)") - case .two: - return String(format: self._SharedMedia_File_two, "\(value)") - case .few: - return String(format: self._SharedMedia_File_few, "\(value)") - case .many: - return String(format: self._SharedMedia_File_many, "\(value)") - case .other: - return String(format: self._SharedMedia_File_other, "\(value)") - } - } - private let _MessageTimer_Months_zero: String - private let _MessageTimer_Months_one: String - private let _MessageTimer_Months_two: String - private let _MessageTimer_Months_few: String - private let _MessageTimer_Months_many: String - private let _MessageTimer_Months_other: String - public func MessageTimer_Months(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_Months_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_Months_one, "\(value)") - case .two: - return String(format: self._MessageTimer_Months_two, "\(value)") - case .few: - return String(format: self._MessageTimer_Months_few, "\(value)") - case .many: - return String(format: self._MessageTimer_Months_many, "\(value)") - case .other: - return String(format: self._MessageTimer_Months_other, "\(value)") - } - } - private let _AttachmentMenu_SendGif_zero: String - private let _AttachmentMenu_SendGif_one: String - private let _AttachmentMenu_SendGif_two: String - private let _AttachmentMenu_SendGif_few: String - private let _AttachmentMenu_SendGif_many: String - private let _AttachmentMenu_SendGif_other: String - public func AttachmentMenu_SendGif(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._AttachmentMenu_SendGif_zero, "\(value)") - case .one: - return String(format: self._AttachmentMenu_SendGif_one, "\(value)") - case .two: - return String(format: self._AttachmentMenu_SendGif_two, "\(value)") - case .few: - return String(format: self._AttachmentMenu_SendGif_few, "\(value)") - case .many: - return String(format: self._AttachmentMenu_SendGif_many, "\(value)") - case .other: - return String(format: self._AttachmentMenu_SendGif_other, "\(value)") - } - } - private let _LiveLocation_MenuChatsCount_zero: String - private let _LiveLocation_MenuChatsCount_one: String - private let _LiveLocation_MenuChatsCount_two: String - private let _LiveLocation_MenuChatsCount_few: String - private let _LiveLocation_MenuChatsCount_many: String - private let _LiveLocation_MenuChatsCount_other: String - public func LiveLocation_MenuChatsCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._LiveLocation_MenuChatsCount_zero, "\(value)") - case .one: - return String(format: self._LiveLocation_MenuChatsCount_one, "\(value)") - case .two: - return String(format: self._LiveLocation_MenuChatsCount_two, "\(value)") - case .few: - return String(format: self._LiveLocation_MenuChatsCount_few, "\(value)") - case .many: - return String(format: self._LiveLocation_MenuChatsCount_many, "\(value)") - case .other: - return String(format: self._LiveLocation_MenuChatsCount_other, "\(value)") - } - } - private let _StickerPack_AddStickerCount_zero: String - private let _StickerPack_AddStickerCount_one: String - private let _StickerPack_AddStickerCount_two: String - private let _StickerPack_AddStickerCount_few: String - private let _StickerPack_AddStickerCount_many: String - private let _StickerPack_AddStickerCount_other: String - public func StickerPack_AddStickerCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._StickerPack_AddStickerCount_zero, "\(value)") - case .one: - return String(format: self._StickerPack_AddStickerCount_one, "\(value)") - case .two: - return String(format: self._StickerPack_AddStickerCount_two, "\(value)") - case .few: - return String(format: self._StickerPack_AddStickerCount_few, "\(value)") - case .many: - return String(format: self._StickerPack_AddStickerCount_many, "\(value)") - case .other: - return String(format: self._StickerPack_AddStickerCount_other, "\(value)") - } - } - private let _UserCount_zero: String - private let _UserCount_one: String - private let _UserCount_two: String - private let _UserCount_few: String - private let _UserCount_many: String - private let _UserCount_other: String - public func UserCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._UserCount_zero, "\(value)") - case .one: - return String(format: self._UserCount_one, "\(value)") - case .two: - return String(format: self._UserCount_two, "\(value)") - case .few: - return String(format: self._UserCount_few, "\(value)") - case .many: - return String(format: self._UserCount_many, "\(value)") - case .other: - return String(format: self._UserCount_other, "\(value)") - } - } - private let _StickerPack_StickerCount_zero: String - private let _StickerPack_StickerCount_one: String - private let _StickerPack_StickerCount_two: String - private let _StickerPack_StickerCount_few: String - private let _StickerPack_StickerCount_many: String - private let _StickerPack_StickerCount_other: String - public func StickerPack_StickerCount(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._StickerPack_StickerCount_zero, "\(value)") - case .one: - return String(format: self._StickerPack_StickerCount_one, "\(value)") - case .two: - return String(format: self._StickerPack_StickerCount_two, "\(value)") - case .few: - return String(format: self._StickerPack_StickerCount_few, "\(value)") - case .many: - return String(format: self._StickerPack_StickerCount_many, "\(value)") - case .other: - return String(format: self._StickerPack_StickerCount_other, "\(value)") - } - } - private let _MessageTimer_Seconds_zero: String - private let _MessageTimer_Seconds_one: String - private let _MessageTimer_Seconds_two: String - private let _MessageTimer_Seconds_few: String - private let _MessageTimer_Seconds_many: String - private let _MessageTimer_Seconds_other: String - public func MessageTimer_Seconds(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_Seconds_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_Seconds_one, "\(value)") - case .two: - return String(format: self._MessageTimer_Seconds_two, "\(value)") - case .few: - return String(format: self._MessageTimer_Seconds_few, "\(value)") - case .many: - return String(format: self._MessageTimer_Seconds_many, "\(value)") - case .other: - return String(format: self._MessageTimer_Seconds_other, "\(value)") - } - } - private let _LastSeen_HoursAgo_zero: String - private let _LastSeen_HoursAgo_one: String - private let _LastSeen_HoursAgo_two: String - private let _LastSeen_HoursAgo_few: String - private let _LastSeen_HoursAgo_many: String - private let _LastSeen_HoursAgo_other: String - public func LastSeen_HoursAgo(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._LastSeen_HoursAgo_zero, "\(value)") - case .one: - return String(format: self._LastSeen_HoursAgo_one, "\(value)") - case .two: - return String(format: self._LastSeen_HoursAgo_two, "\(value)") - case .few: - return String(format: self._LastSeen_HoursAgo_few, "\(value)") - case .many: - return String(format: self._LastSeen_HoursAgo_many, "\(value)") - case .other: - return String(format: self._LastSeen_HoursAgo_other, "\(value)") - } - } - private let _Map_ETAHours_zero: String - private let _Map_ETAHours_one: String - private let _Map_ETAHours_two: String - private let _Map_ETAHours_few: String - private let _Map_ETAHours_many: String - private let _Map_ETAHours_other: String - public func Map_ETAHours(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Map_ETAHours_zero, "\(value)") - case .one: - return String(format: self._Map_ETAHours_one, "\(value)") - case .two: - return String(format: self._Map_ETAHours_two, "\(value)") - case .few: - return String(format: self._Map_ETAHours_few, "\(value)") - case .many: - return String(format: self._Map_ETAHours_many, "\(value)") - case .other: - return String(format: self._Map_ETAHours_other, "\(value)") - } - } - private let _MessageTimer_ShortSeconds_zero: String - private let _MessageTimer_ShortSeconds_one: String - private let _MessageTimer_ShortSeconds_two: String - private let _MessageTimer_ShortSeconds_few: String - private let _MessageTimer_ShortSeconds_many: String - private let _MessageTimer_ShortSeconds_other: String - public func MessageTimer_ShortSeconds(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_ShortSeconds_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_ShortSeconds_one, "\(value)") - case .two: - return String(format: self._MessageTimer_ShortSeconds_two, "\(value)") - case .few: - return String(format: self._MessageTimer_ShortSeconds_few, "\(value)") - case .many: - return String(format: self._MessageTimer_ShortSeconds_many, "\(value)") - case .other: - return String(format: self._MessageTimer_ShortSeconds_other, "\(value)") - } - } - private let _PasscodeSettings_FailedAttempts_zero: String - private let _PasscodeSettings_FailedAttempts_one: String - private let _PasscodeSettings_FailedAttempts_two: String - private let _PasscodeSettings_FailedAttempts_few: String - private let _PasscodeSettings_FailedAttempts_many: String - private let _PasscodeSettings_FailedAttempts_other: String - public func PasscodeSettings_FailedAttempts(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._PasscodeSettings_FailedAttempts_zero, "\(value)") - case .one: - return String(format: self._PasscodeSettings_FailedAttempts_one, "\(value)") - case .two: - return String(format: self._PasscodeSettings_FailedAttempts_two, "\(value)") - case .few: - return String(format: self._PasscodeSettings_FailedAttempts_few, "\(value)") - case .many: - return String(format: self._PasscodeSettings_FailedAttempts_many, "\(value)") - case .other: - return String(format: self._PasscodeSettings_FailedAttempts_other, "\(value)") - } - } - private let _QuickSend_Photos_zero: String - private let _QuickSend_Photos_one: String - private let _QuickSend_Photos_two: String - private let _QuickSend_Photos_few: String - private let _QuickSend_Photos_many: String - private let _QuickSend_Photos_other: String - public func QuickSend_Photos(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._QuickSend_Photos_zero, "\(value)") - case .one: - return String(format: self._QuickSend_Photos_one, "\(value)") - case .two: - return String(format: self._QuickSend_Photos_two, "\(value)") - case .few: - return String(format: self._QuickSend_Photos_few, "\(value)") - case .many: - return String(format: self._QuickSend_Photos_many, "\(value)") - case .other: - return String(format: self._QuickSend_Photos_other, "\(value)") - } - } private let _Media_ShareVideo_zero: String private let _Media_ShareVideo_one: String private let _Media_ShareVideo_two: String @@ -4445,70 +3254,136 @@ public final class PresentationStrings { return String(format: self._Media_ShareVideo_other, "\(value)") } } - private let _Call_ShortMinutes_zero: String - private let _Call_ShortMinutes_one: String - private let _Call_ShortMinutes_two: String - private let _Call_ShortMinutes_few: String - private let _Call_ShortMinutes_many: String - private let _Call_ShortMinutes_other: String - public func Call_ShortMinutes(_ value: Int32) -> String { + private let _Call_Minutes_zero: String + private let _Call_Minutes_one: String + private let _Call_Minutes_two: String + private let _Call_Minutes_few: String + private let _Call_Minutes_many: String + private let _Call_Minutes_other: String + public func Call_Minutes(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._Call_ShortMinutes_zero, "\(value)") + return String(format: self._Call_Minutes_zero, "\(value)") case .one: - return String(format: self._Call_ShortMinutes_one, "\(value)") + return String(format: self._Call_Minutes_one, "\(value)") case .two: - return String(format: self._Call_ShortMinutes_two, "\(value)") + return String(format: self._Call_Minutes_two, "\(value)") case .few: - return String(format: self._Call_ShortMinutes_few, "\(value)") + return String(format: self._Call_Minutes_few, "\(value)") case .many: - return String(format: self._Call_ShortMinutes_many, "\(value)") + return String(format: self._Call_Minutes_many, "\(value)") case .other: - return String(format: self._Call_ShortMinutes_other, "\(value)") + return String(format: self._Call_Minutes_other, "\(value)") } } - private let _PrivacyLastSeenSettings_AddUsers_zero: String - private let _PrivacyLastSeenSettings_AddUsers_one: String - private let _PrivacyLastSeenSettings_AddUsers_two: String - private let _PrivacyLastSeenSettings_AddUsers_few: String - private let _PrivacyLastSeenSettings_AddUsers_many: String - private let _PrivacyLastSeenSettings_AddUsers_other: String - public func PrivacyLastSeenSettings_AddUsers(_ value: Int32) -> String { + private let _LiveLocation_MenuChatsCount_zero: String + private let _LiveLocation_MenuChatsCount_one: String + private let _LiveLocation_MenuChatsCount_two: String + private let _LiveLocation_MenuChatsCount_few: String + private let _LiveLocation_MenuChatsCount_many: String + private let _LiveLocation_MenuChatsCount_other: String + public func LiveLocation_MenuChatsCount(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._PrivacyLastSeenSettings_AddUsers_zero, "\(value)") + return String(format: self._LiveLocation_MenuChatsCount_zero, "\(value)") case .one: - return String(format: self._PrivacyLastSeenSettings_AddUsers_one, "\(value)") + return String(format: self._LiveLocation_MenuChatsCount_one, "\(value)") case .two: - return String(format: self._PrivacyLastSeenSettings_AddUsers_two, "\(value)") + return String(format: self._LiveLocation_MenuChatsCount_two, "\(value)") case .few: - return String(format: self._PrivacyLastSeenSettings_AddUsers_few, "\(value)") + return String(format: self._LiveLocation_MenuChatsCount_few, "\(value)") case .many: - return String(format: self._PrivacyLastSeenSettings_AddUsers_many, "\(value)") + return String(format: self._LiveLocation_MenuChatsCount_many, "\(value)") case .other: - return String(format: self._PrivacyLastSeenSettings_AddUsers_other, "\(value)") + return String(format: self._LiveLocation_MenuChatsCount_other, "\(value)") } } - private let _Watch_LastSeen_HoursAgo_zero: String - private let _Watch_LastSeen_HoursAgo_one: String - private let _Watch_LastSeen_HoursAgo_two: String - private let _Watch_LastSeen_HoursAgo_few: String - private let _Watch_LastSeen_HoursAgo_many: String - private let _Watch_LastSeen_HoursAgo_other: String - public func Watch_LastSeen_HoursAgo(_ value: Int32) -> String { + private let _SharedMedia_Photo_zero: String + private let _SharedMedia_Photo_one: String + private let _SharedMedia_Photo_two: String + private let _SharedMedia_Photo_few: String + private let _SharedMedia_Photo_many: String + private let _SharedMedia_Photo_other: String + public func SharedMedia_Photo(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._Watch_LastSeen_HoursAgo_zero, "\(value)") + return String(format: self._SharedMedia_Photo_zero, "\(value)") case .one: - return String(format: self._Watch_LastSeen_HoursAgo_one, "\(value)") + return String(format: self._SharedMedia_Photo_one, "\(value)") case .two: - return String(format: self._Watch_LastSeen_HoursAgo_two, "\(value)") + return String(format: self._SharedMedia_Photo_two, "\(value)") case .few: - return String(format: self._Watch_LastSeen_HoursAgo_few, "\(value)") + return String(format: self._SharedMedia_Photo_few, "\(value)") case .many: - return String(format: self._Watch_LastSeen_HoursAgo_many, "\(value)") + return String(format: self._SharedMedia_Photo_many, "\(value)") case .other: - return String(format: self._Watch_LastSeen_HoursAgo_other, "\(value)") + return String(format: self._SharedMedia_Photo_other, "\(value)") + } + } + private let _AttachmentMenu_SendGif_zero: String + private let _AttachmentMenu_SendGif_one: String + private let _AttachmentMenu_SendGif_two: String + private let _AttachmentMenu_SendGif_few: String + private let _AttachmentMenu_SendGif_many: String + private let _AttachmentMenu_SendGif_other: String + public func AttachmentMenu_SendGif(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._AttachmentMenu_SendGif_zero, "\(value)") + case .one: + return String(format: self._AttachmentMenu_SendGif_one, "\(value)") + case .two: + return String(format: self._AttachmentMenu_SendGif_two, "\(value)") + case .few: + return String(format: self._AttachmentMenu_SendGif_few, "\(value)") + case .many: + return String(format: self._AttachmentMenu_SendGif_many, "\(value)") + case .other: + return String(format: self._AttachmentMenu_SendGif_other, "\(value)") + } + } + private let _MuteFor_Days_zero: String + private let _MuteFor_Days_one: String + private let _MuteFor_Days_two: String + private let _MuteFor_Days_few: String + private let _MuteFor_Days_many: String + private let _MuteFor_Days_other: String + public func MuteFor_Days(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteFor_Days_zero, "\(value)") + case .one: + return String(format: self._MuteFor_Days_one, "\(value)") + case .two: + return String(format: self._MuteFor_Days_two, "\(value)") + case .few: + return String(format: self._MuteFor_Days_few, "\(value)") + case .many: + return String(format: self._MuteFor_Days_many, "\(value)") + case .other: + return String(format: self._MuteFor_Days_other, "\(value)") + } + } + private let _ForwardedGifs_zero: String + private let _ForwardedGifs_one: String + private let _ForwardedGifs_two: String + private let _ForwardedGifs_few: String + private let _ForwardedGifs_many: String + private let _ForwardedGifs_other: String + public func ForwardedGifs(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedGifs_zero, "\(value)") + case .one: + return String(format: self._ForwardedGifs_one, "\(value)") + case .two: + return String(format: self._ForwardedGifs_two, "\(value)") + case .few: + return String(format: self._ForwardedGifs_few, "\(value)") + case .many: + return String(format: self._ForwardedGifs_many, "\(value)") + case .other: + return String(format: self._ForwardedGifs_other, "\(value)") } } private let _ForwardedVideoMessages_zero: String @@ -4533,48 +3408,356 @@ public final class PresentationStrings { return String(format: self._ForwardedVideoMessages_other, "\(value)") } } - private let _ServiceMessage_GameScoreSelfSimple_zero: String - private let _ServiceMessage_GameScoreSelfSimple_one: String - private let _ServiceMessage_GameScoreSelfSimple_two: String - private let _ServiceMessage_GameScoreSelfSimple_few: String - private let _ServiceMessage_GameScoreSelfSimple_many: String - private let _ServiceMessage_GameScoreSelfSimple_other: String - public func ServiceMessage_GameScoreSelfSimple(_ value: Int32) -> String { + private let _Forward_ConfirmMultipleFiles_zero: String + private let _Forward_ConfirmMultipleFiles_one: String + private let _Forward_ConfirmMultipleFiles_two: String + private let _Forward_ConfirmMultipleFiles_few: String + private let _Forward_ConfirmMultipleFiles_many: String + private let _Forward_ConfirmMultipleFiles_other: String + public func Forward_ConfirmMultipleFiles(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._ServiceMessage_GameScoreSelfSimple_zero, "\(value)") + return String(format: self._Forward_ConfirmMultipleFiles_zero, "\(value)") case .one: - return String(format: self._ServiceMessage_GameScoreSelfSimple_one, "\(value)") + return String(format: self._Forward_ConfirmMultipleFiles_one, "\(value)") case .two: - return String(format: self._ServiceMessage_GameScoreSelfSimple_two, "\(value)") + return String(format: self._Forward_ConfirmMultipleFiles_two, "\(value)") case .few: - return String(format: self._ServiceMessage_GameScoreSelfSimple_few, "\(value)") + return String(format: self._Forward_ConfirmMultipleFiles_few, "\(value)") case .many: - return String(format: self._ServiceMessage_GameScoreSelfSimple_many, "\(value)") + return String(format: self._Forward_ConfirmMultipleFiles_many, "\(value)") case .other: - return String(format: self._ServiceMessage_GameScoreSelfSimple_other, "\(value)") + return String(format: self._Forward_ConfirmMultipleFiles_other, "\(value)") } } - private let _ServiceMessage_GameScoreSimple_zero: String - private let _ServiceMessage_GameScoreSimple_one: String - private let _ServiceMessage_GameScoreSimple_two: String - private let _ServiceMessage_GameScoreSimple_few: String - private let _ServiceMessage_GameScoreSimple_many: String - private let _ServiceMessage_GameScoreSimple_other: String - public func ServiceMessage_GameScoreSimple(_ value: Int32) -> String { + private let _MessageTimer_Months_zero: String + private let _MessageTimer_Months_one: String + private let _MessageTimer_Months_two: String + private let _MessageTimer_Months_few: String + private let _MessageTimer_Months_many: String + private let _MessageTimer_Months_other: String + public func MessageTimer_Months(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._ServiceMessage_GameScoreSimple_zero, "\(value)") + return String(format: self._MessageTimer_Months_zero, "\(value)") case .one: - return String(format: self._ServiceMessage_GameScoreSimple_one, "\(value)") + return String(format: self._MessageTimer_Months_one, "\(value)") case .two: - return String(format: self._ServiceMessage_GameScoreSimple_two, "\(value)") + return String(format: self._MessageTimer_Months_two, "\(value)") case .few: - return String(format: self._ServiceMessage_GameScoreSimple_few, "\(value)") + return String(format: self._MessageTimer_Months_few, "\(value)") case .many: - return String(format: self._ServiceMessage_GameScoreSimple_many, "\(value)") + return String(format: self._MessageTimer_Months_many, "\(value)") case .other: - return String(format: self._ServiceMessage_GameScoreSimple_other, "\(value)") + return String(format: self._MessageTimer_Months_other, "\(value)") + } + } + private let _Map_ETAHours_zero: String + private let _Map_ETAHours_one: String + private let _Map_ETAHours_two: String + private let _Map_ETAHours_few: String + private let _Map_ETAHours_many: String + private let _Map_ETAHours_other: String + public func Map_ETAHours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Map_ETAHours_zero, "\(value)") + case .one: + return String(format: self._Map_ETAHours_one, "\(value)") + case .two: + return String(format: self._Map_ETAHours_two, "\(value)") + case .few: + return String(format: self._Map_ETAHours_few, "\(value)") + case .many: + return String(format: self._Map_ETAHours_many, "\(value)") + case .other: + return String(format: self._Map_ETAHours_other, "\(value)") + } + } + private let _StickerPack_RemoveStickerCount_zero: String + private let _StickerPack_RemoveStickerCount_one: String + private let _StickerPack_RemoveStickerCount_two: String + private let _StickerPack_RemoveStickerCount_few: String + private let _StickerPack_RemoveStickerCount_many: String + private let _StickerPack_RemoveStickerCount_other: String + public func StickerPack_RemoveStickerCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_RemoveStickerCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_RemoveStickerCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_RemoveStickerCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_RemoveStickerCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_RemoveStickerCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_RemoveStickerCount_other, "\(value)") + } + } + private let _MessageTimer_ShortWeeks_zero: String + private let _MessageTimer_ShortWeeks_one: String + private let _MessageTimer_ShortWeeks_two: String + private let _MessageTimer_ShortWeeks_few: String + private let _MessageTimer_ShortWeeks_many: String + private let _MessageTimer_ShortWeeks_other: String + public func MessageTimer_ShortWeeks(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortWeeks_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortWeeks_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortWeeks_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortWeeks_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortWeeks_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortWeeks_other, "\(value)") + } + } + private let _Call_Seconds_zero: String + private let _Call_Seconds_one: String + private let _Call_Seconds_two: String + private let _Call_Seconds_few: String + private let _Call_Seconds_many: String + private let _Call_Seconds_other: String + public func Call_Seconds(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Call_Seconds_zero, "\(value)") + case .one: + return String(format: self._Call_Seconds_one, "\(value)") + case .two: + return String(format: self._Call_Seconds_two, "\(value)") + case .few: + return String(format: self._Call_Seconds_few, "\(value)") + case .many: + return String(format: self._Call_Seconds_many, "\(value)") + case .other: + return String(format: self._Call_Seconds_other, "\(value)") + } + } + private let _ForwardedAudios_zero: String + private let _ForwardedAudios_one: String + private let _ForwardedAudios_two: String + private let _ForwardedAudios_few: String + private let _ForwardedAudios_many: String + private let _ForwardedAudios_other: String + public func ForwardedAudios(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedAudios_zero, "\(value)") + case .one: + return String(format: self._ForwardedAudios_one, "\(value)") + case .two: + return String(format: self._ForwardedAudios_two, "\(value)") + case .few: + return String(format: self._ForwardedAudios_few, "\(value)") + case .many: + return String(format: self._ForwardedAudios_many, "\(value)") + case .other: + return String(format: self._ForwardedAudios_other, "\(value)") + } + } + private let _Conversation_StatusMembers_zero: String + private let _Conversation_StatusMembers_one: String + private let _Conversation_StatusMembers_two: String + private let _Conversation_StatusMembers_few: String + private let _Conversation_StatusMembers_many: String + private let _Conversation_StatusMembers_other: String + public func Conversation_StatusMembers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Conversation_StatusMembers_zero, "\(value)") + case .one: + return String(format: self._Conversation_StatusMembers_one, "\(value)") + case .two: + return String(format: self._Conversation_StatusMembers_two, "\(value)") + case .few: + return String(format: self._Conversation_StatusMembers_few, "\(value)") + case .many: + return String(format: self._Conversation_StatusMembers_many, "\(value)") + case .other: + return String(format: self._Conversation_StatusMembers_other, "\(value)") + } + } + private let _PasscodeSettings_FailedAttempts_zero: String + private let _PasscodeSettings_FailedAttempts_one: String + private let _PasscodeSettings_FailedAttempts_two: String + private let _PasscodeSettings_FailedAttempts_few: String + private let _PasscodeSettings_FailedAttempts_many: String + private let _PasscodeSettings_FailedAttempts_other: String + public func PasscodeSettings_FailedAttempts(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._PasscodeSettings_FailedAttempts_zero, "\(value)") + case .one: + return String(format: self._PasscodeSettings_FailedAttempts_one, "\(value)") + case .two: + return String(format: self._PasscodeSettings_FailedAttempts_two, "\(value)") + case .few: + return String(format: self._PasscodeSettings_FailedAttempts_few, "\(value)") + case .many: + return String(format: self._PasscodeSettings_FailedAttempts_many, "\(value)") + case .other: + return String(format: self._PasscodeSettings_FailedAttempts_other, "\(value)") + } + } + private let _MessageTimer_ShortDays_zero: String + private let _MessageTimer_ShortDays_one: String + private let _MessageTimer_ShortDays_two: String + private let _MessageTimer_ShortDays_few: String + private let _MessageTimer_ShortDays_many: String + private let _MessageTimer_ShortDays_other: String + public func MessageTimer_ShortDays(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortDays_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortDays_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortDays_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortDays_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortDays_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortDays_other, "\(value)") + } + } + private let _Notification_GameScoreExtended_zero: String + private let _Notification_GameScoreExtended_one: String + private let _Notification_GameScoreExtended_two: String + private let _Notification_GameScoreExtended_few: String + private let _Notification_GameScoreExtended_many: String + private let _Notification_GameScoreExtended_other: String + public func Notification_GameScoreExtended(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Notification_GameScoreExtended_zero, "\(value)") + case .one: + return String(format: self._Notification_GameScoreExtended_one, "\(value)") + case .two: + return String(format: self._Notification_GameScoreExtended_two, "\(value)") + case .few: + return String(format: self._Notification_GameScoreExtended_few, "\(value)") + case .many: + return String(format: self._Notification_GameScoreExtended_many, "\(value)") + case .other: + return String(format: self._Notification_GameScoreExtended_other, "\(value)") + } + } + private let _MuteExpires_Hours_zero: String + private let _MuteExpires_Hours_one: String + private let _MuteExpires_Hours_two: String + private let _MuteExpires_Hours_few: String + private let _MuteExpires_Hours_many: String + private let _MuteExpires_Hours_other: String + public func MuteExpires_Hours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteExpires_Hours_zero, "\(value)") + case .one: + return String(format: self._MuteExpires_Hours_one, "\(value)") + case .two: + return String(format: self._MuteExpires_Hours_two, "\(value)") + case .few: + return String(format: self._MuteExpires_Hours_few, "\(value)") + case .many: + return String(format: self._MuteExpires_Hours_many, "\(value)") + case .other: + return String(format: self._MuteExpires_Hours_other, "\(value)") + } + } + private let _InviteText_ContactsCount_zero: String + private let _InviteText_ContactsCount_one: String + private let _InviteText_ContactsCount_two: String + private let _InviteText_ContactsCount_few: String + private let _InviteText_ContactsCount_many: String + private let _InviteText_ContactsCount_other: String + public func InviteText_ContactsCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._InviteText_ContactsCount_zero, "\(value)") + case .one: + return String(format: self._InviteText_ContactsCount_one, "\(value)") + case .two: + return String(format: self._InviteText_ContactsCount_two, "\(value)") + case .few: + return String(format: self._InviteText_ContactsCount_few, "\(value)") + case .many: + return String(format: self._InviteText_ContactsCount_many, "\(value)") + case .other: + return String(format: self._InviteText_ContactsCount_other, "\(value)") + } + } + private let _StickerPack_AddStickerCount_zero: String + private let _StickerPack_AddStickerCount_one: String + private let _StickerPack_AddStickerCount_two: String + private let _StickerPack_AddStickerCount_few: String + private let _StickerPack_AddStickerCount_many: String + private let _StickerPack_AddStickerCount_other: String + public func StickerPack_AddStickerCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_AddStickerCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_AddStickerCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_AddStickerCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_AddStickerCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_AddStickerCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_AddStickerCount_other, "\(value)") + } + } + private let _ServiceMessage_GameScoreSelfExtended_zero: String + private let _ServiceMessage_GameScoreSelfExtended_one: String + private let _ServiceMessage_GameScoreSelfExtended_two: String + private let _ServiceMessage_GameScoreSelfExtended_few: String + private let _ServiceMessage_GameScoreSelfExtended_many: String + private let _ServiceMessage_GameScoreSelfExtended_other: String + public func ServiceMessage_GameScoreSelfExtended(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ServiceMessage_GameScoreSelfExtended_zero, "\(value)") + case .one: + return String(format: self._ServiceMessage_GameScoreSelfExtended_one, "\(value)") + case .two: + return String(format: self._ServiceMessage_GameScoreSelfExtended_two, "\(value)") + case .few: + return String(format: self._ServiceMessage_GameScoreSelfExtended_few, "\(value)") + case .many: + return String(format: self._ServiceMessage_GameScoreSelfExtended_many, "\(value)") + case .other: + return String(format: self._ServiceMessage_GameScoreSelfExtended_other, "\(value)") + } + } + private let _Notification_GameScoreSelfExtended_zero: String + private let _Notification_GameScoreSelfExtended_one: String + private let _Notification_GameScoreSelfExtended_two: String + private let _Notification_GameScoreSelfExtended_few: String + private let _Notification_GameScoreSelfExtended_many: String + private let _Notification_GameScoreSelfExtended_other: String + public func Notification_GameScoreSelfExtended(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Notification_GameScoreSelfExtended_zero, "\(value)") + case .one: + return String(format: self._Notification_GameScoreSelfExtended_one, "\(value)") + case .two: + return String(format: self._Notification_GameScoreSelfExtended_two, "\(value)") + case .few: + return String(format: self._Notification_GameScoreSelfExtended_few, "\(value)") + case .many: + return String(format: self._Notification_GameScoreSelfExtended_many, "\(value)") + case .other: + return String(format: self._Notification_GameScoreSelfExtended_other, "\(value)") } } private let _AttachmentMenu_SendItem_zero: String @@ -4599,26 +3782,26 @@ public final class PresentationStrings { return String(format: self._AttachmentMenu_SendItem_other, "\(value)") } } - private let _StickerPack_RemoveMaskCount_zero: String - private let _StickerPack_RemoveMaskCount_one: String - private let _StickerPack_RemoveMaskCount_two: String - private let _StickerPack_RemoveMaskCount_few: String - private let _StickerPack_RemoveMaskCount_many: String - private let _StickerPack_RemoveMaskCount_other: String - public func StickerPack_RemoveMaskCount(_ value: Int32) -> String { + private let _MessageTimer_ShortHours_zero: String + private let _MessageTimer_ShortHours_one: String + private let _MessageTimer_ShortHours_two: String + private let _MessageTimer_ShortHours_few: String + private let _MessageTimer_ShortHours_many: String + private let _MessageTimer_ShortHours_other: String + public func MessageTimer_ShortHours(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._StickerPack_RemoveMaskCount_zero, "\(value)") + return String(format: self._MessageTimer_ShortHours_zero, "\(value)") case .one: - return String(format: self._StickerPack_RemoveMaskCount_one, "\(value)") + return String(format: self._MessageTimer_ShortHours_one, "\(value)") case .two: - return String(format: self._StickerPack_RemoveMaskCount_two, "\(value)") + return String(format: self._MessageTimer_ShortHours_two, "\(value)") case .few: - return String(format: self._StickerPack_RemoveMaskCount_few, "\(value)") + return String(format: self._MessageTimer_ShortHours_few, "\(value)") case .many: - return String(format: self._StickerPack_RemoveMaskCount_many, "\(value)") + return String(format: self._MessageTimer_ShortHours_many, "\(value)") case .other: - return String(format: self._StickerPack_RemoveMaskCount_other, "\(value)") + return String(format: self._MessageTimer_ShortHours_other, "\(value)") } } private let _ForwardedContacts_zero: String @@ -4643,182 +3826,6 @@ public final class PresentationStrings { return String(format: self._ForwardedContacts_other, "\(value)") } } - private let _Conversation_StatusOnline_zero: String - private let _Conversation_StatusOnline_one: String - private let _Conversation_StatusOnline_two: String - private let _Conversation_StatusOnline_few: String - private let _Conversation_StatusOnline_many: String - private let _Conversation_StatusOnline_other: String - public func Conversation_StatusOnline(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Conversation_StatusOnline_zero, "\(value)") - case .one: - return String(format: self._Conversation_StatusOnline_one, "\(value)") - case .two: - return String(format: self._Conversation_StatusOnline_two, "\(value)") - case .few: - return String(format: self._Conversation_StatusOnline_few, "\(value)") - case .many: - return String(format: self._Conversation_StatusOnline_many, "\(value)") - case .other: - return String(format: self._Conversation_StatusOnline_other, "\(value)") - } - } - private let _Call_ShortSeconds_zero: String - private let _Call_ShortSeconds_one: String - private let _Call_ShortSeconds_two: String - private let _Call_ShortSeconds_few: String - private let _Call_ShortSeconds_many: String - private let _Call_ShortSeconds_other: String - public func Call_ShortSeconds(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._Call_ShortSeconds_zero, "\(value)") - case .one: - return String(format: self._Call_ShortSeconds_one, "\(value)") - case .two: - return String(format: self._Call_ShortSeconds_two, "\(value)") - case .few: - return String(format: self._Call_ShortSeconds_few, "\(value)") - case .many: - return String(format: self._Call_ShortSeconds_many, "\(value)") - case .other: - return String(format: self._Call_ShortSeconds_other, "\(value)") - } - } - private let _ForwardedLocations_zero: String - private let _ForwardedLocations_one: String - private let _ForwardedLocations_two: String - private let _ForwardedLocations_few: String - private let _ForwardedLocations_many: String - private let _ForwardedLocations_other: String - public func ForwardedLocations(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedLocations_zero, "\(value)") - case .one: - return String(format: self._ForwardedLocations_one, "\(value)") - case .two: - return String(format: self._ForwardedLocations_two, "\(value)") - case .few: - return String(format: self._ForwardedLocations_few, "\(value)") - case .many: - return String(format: self._ForwardedLocations_many, "\(value)") - case .other: - return String(format: self._ForwardedLocations_other, "\(value)") - } - } - private let _SharedMedia_DeleteItemsConfirmation_zero: String - private let _SharedMedia_DeleteItemsConfirmation_one: String - private let _SharedMedia_DeleteItemsConfirmation_two: String - private let _SharedMedia_DeleteItemsConfirmation_few: String - private let _SharedMedia_DeleteItemsConfirmation_many: String - private let _SharedMedia_DeleteItemsConfirmation_other: String - public func SharedMedia_DeleteItemsConfirmation(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._SharedMedia_DeleteItemsConfirmation_zero, "\(value)") - case .one: - return String(format: self._SharedMedia_DeleteItemsConfirmation_one, "\(value)") - case .two: - return String(format: self._SharedMedia_DeleteItemsConfirmation_two, "\(value)") - case .few: - return String(format: self._SharedMedia_DeleteItemsConfirmation_few, "\(value)") - case .many: - return String(format: self._SharedMedia_DeleteItemsConfirmation_many, "\(value)") - case .other: - return String(format: self._SharedMedia_DeleteItemsConfirmation_other, "\(value)") - } - } - private let _ForwardedMessages_zero: String - private let _ForwardedMessages_one: String - private let _ForwardedMessages_two: String - private let _ForwardedMessages_few: String - private let _ForwardedMessages_many: String - private let _ForwardedMessages_other: String - public func ForwardedMessages(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedMessages_zero, "\(value)") - case .one: - return String(format: self._ForwardedMessages_one, "\(value)") - case .two: - return String(format: self._ForwardedMessages_two, "\(value)") - case .few: - return String(format: self._ForwardedMessages_few, "\(value)") - case .many: - return String(format: self._ForwardedMessages_many, "\(value)") - case .other: - return String(format: self._ForwardedMessages_other, "\(value)") - } - } - private let _ForwardedPhotos_zero: String - private let _ForwardedPhotos_one: String - private let _ForwardedPhotos_two: String - private let _ForwardedPhotos_few: String - private let _ForwardedPhotos_many: String - private let _ForwardedPhotos_other: String - public func ForwardedPhotos(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedPhotos_zero, "\(value)") - case .one: - return String(format: self._ForwardedPhotos_one, "\(value)") - case .two: - return String(format: self._ForwardedPhotos_two, "\(value)") - case .few: - return String(format: self._ForwardedPhotos_few, "\(value)") - case .many: - return String(format: self._ForwardedPhotos_many, "\(value)") - case .other: - return String(format: self._ForwardedPhotos_other, "\(value)") - } - } - private let _AttachmentMenu_SendVideo_zero: String - private let _AttachmentMenu_SendVideo_one: String - private let _AttachmentMenu_SendVideo_two: String - private let _AttachmentMenu_SendVideo_few: String - private let _AttachmentMenu_SendVideo_many: String - private let _AttachmentMenu_SendVideo_other: String - public func AttachmentMenu_SendVideo(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._AttachmentMenu_SendVideo_zero, "\(value)") - case .one: - return String(format: self._AttachmentMenu_SendVideo_one, "\(value)") - case .two: - return String(format: self._AttachmentMenu_SendVideo_two, "\(value)") - case .few: - return String(format: self._AttachmentMenu_SendVideo_few, "\(value)") - case .many: - return String(format: self._AttachmentMenu_SendVideo_many, "\(value)") - case .other: - return String(format: self._AttachmentMenu_SendVideo_other, "\(value)") - } - } - private let _AttachmentMenu_SendPhoto_zero: String - private let _AttachmentMenu_SendPhoto_one: String - private let _AttachmentMenu_SendPhoto_two: String - private let _AttachmentMenu_SendPhoto_few: String - private let _AttachmentMenu_SendPhoto_many: String - private let _AttachmentMenu_SendPhoto_other: String - public func AttachmentMenu_SendPhoto(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._AttachmentMenu_SendPhoto_zero, "\(value)") - case .one: - return String(format: self._AttachmentMenu_SendPhoto_one, "\(value)") - case .two: - return String(format: self._AttachmentMenu_SendPhoto_two, "\(value)") - case .few: - return String(format: self._AttachmentMenu_SendPhoto_few, "\(value)") - case .many: - return String(format: self._AttachmentMenu_SendPhoto_many, "\(value)") - case .other: - return String(format: self._AttachmentMenu_SendPhoto_other, "\(value)") - } - } private let _SharedMedia_Generic_zero: String private let _SharedMedia_Generic_one: String private let _SharedMedia_Generic_two: String @@ -4841,6 +3848,116 @@ public final class PresentationStrings { return String(format: self._SharedMedia_Generic_other, "\(value)") } } + private let _LiveLocationUpdated_MinutesAgo_zero: String + private let _LiveLocationUpdated_MinutesAgo_one: String + private let _LiveLocationUpdated_MinutesAgo_two: String + private let _LiveLocationUpdated_MinutesAgo_few: String + private let _LiveLocationUpdated_MinutesAgo_many: String + private let _LiveLocationUpdated_MinutesAgo_other: String + public func LiveLocationUpdated_MinutesAgo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._LiveLocationUpdated_MinutesAgo_zero, "\(value)") + case .one: + return String(format: self._LiveLocationUpdated_MinutesAgo_one, "\(value)") + case .two: + return String(format: self._LiveLocationUpdated_MinutesAgo_two, "\(value)") + case .few: + return String(format: self._LiveLocationUpdated_MinutesAgo_few, "\(value)") + case .many: + return String(format: self._LiveLocationUpdated_MinutesAgo_many, "\(value)") + case .other: + return String(format: self._LiveLocationUpdated_MinutesAgo_other, "\(value)") + } + } + private let _Notification_GameScoreSelfSimple_zero: String + private let _Notification_GameScoreSelfSimple_one: String + private let _Notification_GameScoreSelfSimple_two: String + private let _Notification_GameScoreSelfSimple_few: String + private let _Notification_GameScoreSelfSimple_many: String + private let _Notification_GameScoreSelfSimple_other: String + public func Notification_GameScoreSelfSimple(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Notification_GameScoreSelfSimple_zero, "\(value)") + case .one: + return String(format: self._Notification_GameScoreSelfSimple_one, "\(value)") + case .two: + return String(format: self._Notification_GameScoreSelfSimple_two, "\(value)") + case .few: + return String(format: self._Notification_GameScoreSelfSimple_few, "\(value)") + case .many: + return String(format: self._Notification_GameScoreSelfSimple_many, "\(value)") + case .other: + return String(format: self._Notification_GameScoreSelfSimple_other, "\(value)") + } + } + private let _ServiceMessage_GameScoreSimple_zero: String + private let _ServiceMessage_GameScoreSimple_one: String + private let _ServiceMessage_GameScoreSimple_two: String + private let _ServiceMessage_GameScoreSimple_few: String + private let _ServiceMessage_GameScoreSimple_many: String + private let _ServiceMessage_GameScoreSimple_other: String + public func ServiceMessage_GameScoreSimple(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ServiceMessage_GameScoreSimple_zero, "\(value)") + case .one: + return String(format: self._ServiceMessage_GameScoreSimple_one, "\(value)") + case .two: + return String(format: self._ServiceMessage_GameScoreSimple_two, "\(value)") + case .few: + return String(format: self._ServiceMessage_GameScoreSimple_few, "\(value)") + case .many: + return String(format: self._ServiceMessage_GameScoreSimple_many, "\(value)") + case .other: + return String(format: self._ServiceMessage_GameScoreSimple_other, "\(value)") + } + } + private let _QuickSend_Photos_zero: String + private let _QuickSend_Photos_one: String + private let _QuickSend_Photos_two: String + private let _QuickSend_Photos_few: String + private let _QuickSend_Photos_many: String + private let _QuickSend_Photos_other: String + public func QuickSend_Photos(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._QuickSend_Photos_zero, "\(value)") + case .one: + return String(format: self._QuickSend_Photos_one, "\(value)") + case .two: + return String(format: self._QuickSend_Photos_two, "\(value)") + case .few: + return String(format: self._QuickSend_Photos_few, "\(value)") + case .many: + return String(format: self._QuickSend_Photos_many, "\(value)") + case .other: + return String(format: self._QuickSend_Photos_other, "\(value)") + } + } + private let _Media_SharePhoto_zero: String + private let _Media_SharePhoto_one: String + private let _Media_SharePhoto_two: String + private let _Media_SharePhoto_few: String + private let _Media_SharePhoto_many: String + private let _Media_SharePhoto_other: String + public func Media_SharePhoto(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Media_SharePhoto_zero, "\(value)") + case .one: + return String(format: self._Media_SharePhoto_one, "\(value)") + case .two: + return String(format: self._Media_SharePhoto_two, "\(value)") + case .few: + return String(format: self._Media_SharePhoto_few, "\(value)") + case .many: + return String(format: self._Media_SharePhoto_many, "\(value)") + case .other: + return String(format: self._Media_SharePhoto_other, "\(value)") + } + } private let _MuteExpires_Minutes_zero: String private let _MuteExpires_Minutes_one: String private let _MuteExpires_Minutes_two: String @@ -4863,48 +3980,664 @@ public final class PresentationStrings { return String(format: self._MuteExpires_Minutes_other, "\(value)") } } - private let _Map_ETAMinutes_zero: String - private let _Map_ETAMinutes_one: String - private let _Map_ETAMinutes_two: String - private let _Map_ETAMinutes_few: String - private let _Map_ETAMinutes_many: String - private let _Map_ETAMinutes_other: String - public func Map_ETAMinutes(_ value: Int32) -> String { + private let _Conversation_LiveLocationMembersCount_zero: String + private let _Conversation_LiveLocationMembersCount_one: String + private let _Conversation_LiveLocationMembersCount_two: String + private let _Conversation_LiveLocationMembersCount_few: String + private let _Conversation_LiveLocationMembersCount_many: String + private let _Conversation_LiveLocationMembersCount_other: String + public func Conversation_LiveLocationMembersCount(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._Map_ETAMinutes_zero, "\(value)") + return String(format: self._Conversation_LiveLocationMembersCount_zero, "\(value)") case .one: - return String(format: self._Map_ETAMinutes_one, "\(value)") + return String(format: self._Conversation_LiveLocationMembersCount_one, "\(value)") case .two: - return String(format: self._Map_ETAMinutes_two, "\(value)") + return String(format: self._Conversation_LiveLocationMembersCount_two, "\(value)") case .few: - return String(format: self._Map_ETAMinutes_few, "\(value)") + return String(format: self._Conversation_LiveLocationMembersCount_few, "\(value)") case .many: - return String(format: self._Map_ETAMinutes_many, "\(value)") + return String(format: self._Conversation_LiveLocationMembersCount_many, "\(value)") case .other: - return String(format: self._Map_ETAMinutes_other, "\(value)") + return String(format: self._Conversation_LiveLocationMembersCount_other, "\(value)") } } - private let _Conversation_StatusSubscribers_zero: String - private let _Conversation_StatusSubscribers_one: String - private let _Conversation_StatusSubscribers_two: String - private let _Conversation_StatusSubscribers_few: String - private let _Conversation_StatusSubscribers_many: String - private let _Conversation_StatusSubscribers_other: String - public func Conversation_StatusSubscribers(_ value: Int32) -> String { + private let _MessageTimer_Days_zero: String + private let _MessageTimer_Days_one: String + private let _MessageTimer_Days_two: String + private let _MessageTimer_Days_few: String + private let _MessageTimer_Days_many: String + private let _MessageTimer_Days_other: String + public func MessageTimer_Days(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._Conversation_StatusSubscribers_zero, "\(value)") + return String(format: self._MessageTimer_Days_zero, "\(value)") case .one: - return String(format: self._Conversation_StatusSubscribers_one, "\(value)") + return String(format: self._MessageTimer_Days_one, "\(value)") case .two: - return String(format: self._Conversation_StatusSubscribers_two, "\(value)") + return String(format: self._MessageTimer_Days_two, "\(value)") case .few: - return String(format: self._Conversation_StatusSubscribers_few, "\(value)") + return String(format: self._MessageTimer_Days_few, "\(value)") case .many: - return String(format: self._Conversation_StatusSubscribers_many, "\(value)") + return String(format: self._MessageTimer_Days_many, "\(value)") case .other: - return String(format: self._Conversation_StatusSubscribers_other, "\(value)") + return String(format: self._MessageTimer_Days_other, "\(value)") + } + } + private let _MessageTimer_ShortSeconds_zero: String + private let _MessageTimer_ShortSeconds_one: String + private let _MessageTimer_ShortSeconds_two: String + private let _MessageTimer_ShortSeconds_few: String + private let _MessageTimer_ShortSeconds_many: String + private let _MessageTimer_ShortSeconds_other: String + public func MessageTimer_ShortSeconds(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortSeconds_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortSeconds_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortSeconds_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortSeconds_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortSeconds_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortSeconds_other, "\(value)") + } + } + private let _Conversation_StatusOnline_zero: String + private let _Conversation_StatusOnline_one: String + private let _Conversation_StatusOnline_two: String + private let _Conversation_StatusOnline_few: String + private let _Conversation_StatusOnline_many: String + private let _Conversation_StatusOnline_other: String + public func Conversation_StatusOnline(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Conversation_StatusOnline_zero, "\(value)") + case .one: + return String(format: self._Conversation_StatusOnline_one, "\(value)") + case .two: + return String(format: self._Conversation_StatusOnline_two, "\(value)") + case .few: + return String(format: self._Conversation_StatusOnline_few, "\(value)") + case .many: + return String(format: self._Conversation_StatusOnline_many, "\(value)") + case .other: + return String(format: self._Conversation_StatusOnline_other, "\(value)") + } + } + private let _ForwardedMessages_zero: String + private let _ForwardedMessages_one: String + private let _ForwardedMessages_two: String + private let _ForwardedMessages_few: String + private let _ForwardedMessages_many: String + private let _ForwardedMessages_other: String + public func ForwardedMessages(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedMessages_zero, "\(value)") + case .one: + return String(format: self._ForwardedMessages_one, "\(value)") + case .two: + return String(format: self._ForwardedMessages_two, "\(value)") + case .few: + return String(format: self._ForwardedMessages_few, "\(value)") + case .many: + return String(format: self._ForwardedMessages_many, "\(value)") + case .other: + return String(format: self._ForwardedMessages_other, "\(value)") + } + } + private let _MessageTimer_Seconds_zero: String + private let _MessageTimer_Seconds_one: String + private let _MessageTimer_Seconds_two: String + private let _MessageTimer_Seconds_few: String + private let _MessageTimer_Seconds_many: String + private let _MessageTimer_Seconds_other: String + public func MessageTimer_Seconds(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Seconds_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Seconds_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Seconds_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Seconds_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Seconds_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Seconds_other, "\(value)") + } + } + private let _SharedMedia_Link_zero: String + private let _SharedMedia_Link_one: String + private let _SharedMedia_Link_two: String + private let _SharedMedia_Link_few: String + private let _SharedMedia_Link_many: String + private let _SharedMedia_Link_other: String + public func SharedMedia_Link(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_Link_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_Link_one, "\(value)") + case .two: + return String(format: self._SharedMedia_Link_two, "\(value)") + case .few: + return String(format: self._SharedMedia_Link_few, "\(value)") + case .many: + return String(format: self._SharedMedia_Link_many, "\(value)") + case .other: + return String(format: self._SharedMedia_Link_other, "\(value)") + } + } + private let _Invitation_Members_zero: String + private let _Invitation_Members_one: String + private let _Invitation_Members_two: String + private let _Invitation_Members_few: String + private let _Invitation_Members_many: String + private let _Invitation_Members_other: String + public func Invitation_Members(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Invitation_Members_zero, "\(value)") + case .one: + return String(format: self._Invitation_Members_one, "\(value)") + case .two: + return String(format: self._Invitation_Members_two, "\(value)") + case .few: + return String(format: self._Invitation_Members_few, "\(value)") + case .many: + return String(format: self._Invitation_Members_many, "\(value)") + case .other: + return String(format: self._Invitation_Members_other, "\(value)") + } + } + private let _Contacts_ImportersCount_zero: String + private let _Contacts_ImportersCount_one: String + private let _Contacts_ImportersCount_two: String + private let _Contacts_ImportersCount_few: String + private let _Contacts_ImportersCount_many: String + private let _Contacts_ImportersCount_other: String + public func Contacts_ImportersCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Contacts_ImportersCount_zero, "\(value)") + case .one: + return String(format: self._Contacts_ImportersCount_one, "\(value)") + case .two: + return String(format: self._Contacts_ImportersCount_two, "\(value)") + case .few: + return String(format: self._Contacts_ImportersCount_few, "\(value)") + case .many: + return String(format: self._Contacts_ImportersCount_many, "\(value)") + case .other: + return String(format: self._Contacts_ImportersCount_other, "\(value)") + } + } + private let _ForwardedFiles_zero: String + private let _ForwardedFiles_one: String + private let _ForwardedFiles_two: String + private let _ForwardedFiles_few: String + private let _ForwardedFiles_many: String + private let _ForwardedFiles_other: String + public func ForwardedFiles(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedFiles_zero, "\(value)") + case .one: + return String(format: self._ForwardedFiles_one, "\(value)") + case .two: + return String(format: self._ForwardedFiles_two, "\(value)") + case .few: + return String(format: self._ForwardedFiles_few, "\(value)") + case .many: + return String(format: self._ForwardedFiles_many, "\(value)") + case .other: + return String(format: self._ForwardedFiles_other, "\(value)") + } + } + private let _Watch_UserInfo_Mute_zero: String + private let _Watch_UserInfo_Mute_one: String + private let _Watch_UserInfo_Mute_two: String + private let _Watch_UserInfo_Mute_few: String + private let _Watch_UserInfo_Mute_many: String + private let _Watch_UserInfo_Mute_other: String + public func Watch_UserInfo_Mute(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Watch_UserInfo_Mute_zero, "\(value)") + case .one: + return String(format: self._Watch_UserInfo_Mute_one, "\(value)") + case .two: + return String(format: self._Watch_UserInfo_Mute_two, "\(value)") + case .few: + return String(format: self._Watch_UserInfo_Mute_few, "\(value)") + case .many: + return String(format: self._Watch_UserInfo_Mute_many, "\(value)") + case .other: + return String(format: self._Watch_UserInfo_Mute_other, "\(value)") + } + } + private let _Watch_LastSeen_HoursAgo_zero: String + private let _Watch_LastSeen_HoursAgo_one: String + private let _Watch_LastSeen_HoursAgo_two: String + private let _Watch_LastSeen_HoursAgo_few: String + private let _Watch_LastSeen_HoursAgo_many: String + private let _Watch_LastSeen_HoursAgo_other: String + public func Watch_LastSeen_HoursAgo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Watch_LastSeen_HoursAgo_zero, "\(value)") + case .one: + return String(format: self._Watch_LastSeen_HoursAgo_one, "\(value)") + case .two: + return String(format: self._Watch_LastSeen_HoursAgo_two, "\(value)") + case .few: + return String(format: self._Watch_LastSeen_HoursAgo_few, "\(value)") + case .many: + return String(format: self._Watch_LastSeen_HoursAgo_many, "\(value)") + case .other: + return String(format: self._Watch_LastSeen_HoursAgo_other, "\(value)") + } + } + private let _SharedMedia_Video_zero: String + private let _SharedMedia_Video_one: String + private let _SharedMedia_Video_two: String + private let _SharedMedia_Video_few: String + private let _SharedMedia_Video_many: String + private let _SharedMedia_Video_other: String + public func SharedMedia_Video(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_Video_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_Video_one, "\(value)") + case .two: + return String(format: self._SharedMedia_Video_two, "\(value)") + case .few: + return String(format: self._SharedMedia_Video_few, "\(value)") + case .many: + return String(format: self._SharedMedia_Video_many, "\(value)") + case .other: + return String(format: self._SharedMedia_Video_other, "\(value)") + } + } + private let _AttachmentMenu_SendVideo_zero: String + private let _AttachmentMenu_SendVideo_one: String + private let _AttachmentMenu_SendVideo_two: String + private let _AttachmentMenu_SendVideo_few: String + private let _AttachmentMenu_SendVideo_many: String + private let _AttachmentMenu_SendVideo_other: String + public func AttachmentMenu_SendVideo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._AttachmentMenu_SendVideo_zero, "\(value)") + case .one: + return String(format: self._AttachmentMenu_SendVideo_one, "\(value)") + case .two: + return String(format: self._AttachmentMenu_SendVideo_two, "\(value)") + case .few: + return String(format: self._AttachmentMenu_SendVideo_few, "\(value)") + case .many: + return String(format: self._AttachmentMenu_SendVideo_many, "\(value)") + case .other: + return String(format: self._AttachmentMenu_SendVideo_other, "\(value)") + } + } + private let _ForwardedStickers_zero: String + private let _ForwardedStickers_one: String + private let _ForwardedStickers_two: String + private let _ForwardedStickers_few: String + private let _ForwardedStickers_many: String + private let _ForwardedStickers_other: String + public func ForwardedStickers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedStickers_zero, "\(value)") + case .one: + return String(format: self._ForwardedStickers_one, "\(value)") + case .two: + return String(format: self._ForwardedStickers_two, "\(value)") + case .few: + return String(format: self._ForwardedStickers_few, "\(value)") + case .many: + return String(format: self._ForwardedStickers_many, "\(value)") + case .other: + return String(format: self._ForwardedStickers_other, "\(value)") + } + } + private let _MessageTimer_Years_zero: String + private let _MessageTimer_Years_one: String + private let _MessageTimer_Years_two: String + private let _MessageTimer_Years_few: String + private let _MessageTimer_Years_many: String + private let _MessageTimer_Years_other: String + public func MessageTimer_Years(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Years_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Years_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Years_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Years_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Years_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Years_other, "\(value)") + } + } + private let _SharedMedia_DeleteItemsConfirmation_zero: String + private let _SharedMedia_DeleteItemsConfirmation_one: String + private let _SharedMedia_DeleteItemsConfirmation_two: String + private let _SharedMedia_DeleteItemsConfirmation_few: String + private let _SharedMedia_DeleteItemsConfirmation_many: String + private let _SharedMedia_DeleteItemsConfirmation_other: String + public func SharedMedia_DeleteItemsConfirmation(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_DeleteItemsConfirmation_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_DeleteItemsConfirmation_one, "\(value)") + case .two: + return String(format: self._SharedMedia_DeleteItemsConfirmation_two, "\(value)") + case .few: + return String(format: self._SharedMedia_DeleteItemsConfirmation_few, "\(value)") + case .many: + return String(format: self._SharedMedia_DeleteItemsConfirmation_many, "\(value)") + case .other: + return String(format: self._SharedMedia_DeleteItemsConfirmation_other, "\(value)") + } + } + private let _MessageTimer_ShortMinutes_zero: String + private let _MessageTimer_ShortMinutes_one: String + private let _MessageTimer_ShortMinutes_two: String + private let _MessageTimer_ShortMinutes_few: String + private let _MessageTimer_ShortMinutes_many: String + private let _MessageTimer_ShortMinutes_other: String + public func MessageTimer_ShortMinutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortMinutes_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortMinutes_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortMinutes_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortMinutes_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortMinutes_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortMinutes_other, "\(value)") + } + } + private let _AttachmentMenu_SendPhoto_zero: String + private let _AttachmentMenu_SendPhoto_one: String + private let _AttachmentMenu_SendPhoto_two: String + private let _AttachmentMenu_SendPhoto_few: String + private let _AttachmentMenu_SendPhoto_many: String + private let _AttachmentMenu_SendPhoto_other: String + public func AttachmentMenu_SendPhoto(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._AttachmentMenu_SendPhoto_zero, "\(value)") + case .one: + return String(format: self._AttachmentMenu_SendPhoto_one, "\(value)") + case .two: + return String(format: self._AttachmentMenu_SendPhoto_two, "\(value)") + case .few: + return String(format: self._AttachmentMenu_SendPhoto_few, "\(value)") + case .many: + return String(format: self._AttachmentMenu_SendPhoto_many, "\(value)") + case .other: + return String(format: self._AttachmentMenu_SendPhoto_other, "\(value)") + } + } + private let _GroupInfo_ParticipantCount_zero: String + private let _GroupInfo_ParticipantCount_one: String + private let _GroupInfo_ParticipantCount_two: String + private let _GroupInfo_ParticipantCount_few: String + private let _GroupInfo_ParticipantCount_many: String + private let _GroupInfo_ParticipantCount_other: String + public func GroupInfo_ParticipantCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._GroupInfo_ParticipantCount_zero, "\(value)") + case .one: + return String(format: self._GroupInfo_ParticipantCount_one, "\(value)") + case .two: + return String(format: self._GroupInfo_ParticipantCount_two, "\(value)") + case .few: + return String(format: self._GroupInfo_ParticipantCount_few, "\(value)") + case .many: + return String(format: self._GroupInfo_ParticipantCount_many, "\(value)") + case .other: + return String(format: self._GroupInfo_ParticipantCount_other, "\(value)") + } + } + private let _Call_ShortSeconds_zero: String + private let _Call_ShortSeconds_one: String + private let _Call_ShortSeconds_two: String + private let _Call_ShortSeconds_few: String + private let _Call_ShortSeconds_many: String + private let _Call_ShortSeconds_other: String + public func Call_ShortSeconds(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Call_ShortSeconds_zero, "\(value)") + case .one: + return String(format: self._Call_ShortSeconds_one, "\(value)") + case .two: + return String(format: self._Call_ShortSeconds_two, "\(value)") + case .few: + return String(format: self._Call_ShortSeconds_few, "\(value)") + case .many: + return String(format: self._Call_ShortSeconds_many, "\(value)") + case .other: + return String(format: self._Call_ShortSeconds_other, "\(value)") + } + } + private let _Notification_GameScoreSimple_zero: String + private let _Notification_GameScoreSimple_one: String + private let _Notification_GameScoreSimple_two: String + private let _Notification_GameScoreSimple_few: String + private let _Notification_GameScoreSimple_many: String + private let _Notification_GameScoreSimple_other: String + public func Notification_GameScoreSimple(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Notification_GameScoreSimple_zero, "\(value)") + case .one: + return String(format: self._Notification_GameScoreSimple_one, "\(value)") + case .two: + return String(format: self._Notification_GameScoreSimple_two, "\(value)") + case .few: + return String(format: self._Notification_GameScoreSimple_few, "\(value)") + case .many: + return String(format: self._Notification_GameScoreSimple_many, "\(value)") + case .other: + return String(format: self._Notification_GameScoreSimple_other, "\(value)") + } + } + private let _StickerPack_StickerCount_zero: String + private let _StickerPack_StickerCount_one: String + private let _StickerPack_StickerCount_two: String + private let _StickerPack_StickerCount_few: String + private let _StickerPack_StickerCount_many: String + private let _StickerPack_StickerCount_other: String + public func StickerPack_StickerCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_StickerCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_StickerCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_StickerCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_StickerCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_StickerCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_StickerCount_other, "\(value)") + } + } + private let _StickerPack_RemoveMaskCount_zero: String + private let _StickerPack_RemoveMaskCount_one: String + private let _StickerPack_RemoveMaskCount_two: String + private let _StickerPack_RemoveMaskCount_few: String + private let _StickerPack_RemoveMaskCount_many: String + private let _StickerPack_RemoveMaskCount_other: String + public func StickerPack_RemoveMaskCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_RemoveMaskCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_RemoveMaskCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_RemoveMaskCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_RemoveMaskCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_RemoveMaskCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_RemoveMaskCount_other, "\(value)") + } + } + private let _UserCount_zero: String + private let _UserCount_one: String + private let _UserCount_two: String + private let _UserCount_few: String + private let _UserCount_many: String + private let _UserCount_other: String + public func UserCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._UserCount_zero, "\(value)") + case .one: + return String(format: self._UserCount_one, "\(value)") + case .two: + return String(format: self._UserCount_two, "\(value)") + case .few: + return String(format: self._UserCount_few, "\(value)") + case .many: + return String(format: self._UserCount_many, "\(value)") + case .other: + return String(format: self._UserCount_other, "\(value)") + } + } + private let _SharedMedia_File_zero: String + private let _SharedMedia_File_one: String + private let _SharedMedia_File_two: String + private let _SharedMedia_File_few: String + private let _SharedMedia_File_many: String + private let _SharedMedia_File_other: String + public func SharedMedia_File(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_File_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_File_one, "\(value)") + case .two: + return String(format: self._SharedMedia_File_two, "\(value)") + case .few: + return String(format: self._SharedMedia_File_few, "\(value)") + case .many: + return String(format: self._SharedMedia_File_many, "\(value)") + case .other: + return String(format: self._SharedMedia_File_other, "\(value)") + } + } + private let _Call_ShortMinutes_zero: String + private let _Call_ShortMinutes_one: String + private let _Call_ShortMinutes_two: String + private let _Call_ShortMinutes_few: String + private let _Call_ShortMinutes_many: String + private let _Call_ShortMinutes_other: String + public func Call_ShortMinutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Call_ShortMinutes_zero, "\(value)") + case .one: + return String(format: self._Call_ShortMinutes_one, "\(value)") + case .two: + return String(format: self._Call_ShortMinutes_two, "\(value)") + case .few: + return String(format: self._Call_ShortMinutes_few, "\(value)") + case .many: + return String(format: self._Call_ShortMinutes_many, "\(value)") + case .other: + return String(format: self._Call_ShortMinutes_other, "\(value)") + } + } + private let _MuteExpires_Days_zero: String + private let _MuteExpires_Days_one: String + private let _MuteExpires_Days_two: String + private let _MuteExpires_Days_few: String + private let _MuteExpires_Days_many: String + private let _MuteExpires_Days_other: String + public func MuteExpires_Days(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteExpires_Days_zero, "\(value)") + case .one: + return String(format: self._MuteExpires_Days_one, "\(value)") + case .two: + return String(format: self._MuteExpires_Days_two, "\(value)") + case .few: + return String(format: self._MuteExpires_Days_few, "\(value)") + case .many: + return String(format: self._MuteExpires_Days_many, "\(value)") + case .other: + return String(format: self._MuteExpires_Days_other, "\(value)") + } + } + private let _Watch_LastSeen_MinutesAgo_zero: String + private let _Watch_LastSeen_MinutesAgo_one: String + private let _Watch_LastSeen_MinutesAgo_two: String + private let _Watch_LastSeen_MinutesAgo_few: String + private let _Watch_LastSeen_MinutesAgo_many: String + private let _Watch_LastSeen_MinutesAgo_other: String + public func Watch_LastSeen_MinutesAgo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Watch_LastSeen_MinutesAgo_zero, "\(value)") + case .one: + return String(format: self._Watch_LastSeen_MinutesAgo_one, "\(value)") + case .two: + return String(format: self._Watch_LastSeen_MinutesAgo_two, "\(value)") + case .few: + return String(format: self._Watch_LastSeen_MinutesAgo_few, "\(value)") + case .many: + return String(format: self._Watch_LastSeen_MinutesAgo_many, "\(value)") + case .other: + return String(format: self._Watch_LastSeen_MinutesAgo_other, "\(value)") + } + } + private let _LastSeen_MinutesAgo_zero: String + private let _LastSeen_MinutesAgo_one: String + private let _LastSeen_MinutesAgo_two: String + private let _LastSeen_MinutesAgo_few: String + private let _LastSeen_MinutesAgo_many: String + private let _LastSeen_MinutesAgo_other: String + public func LastSeen_MinutesAgo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._LastSeen_MinutesAgo_zero, "\(value)") + case .one: + return String(format: self._LastSeen_MinutesAgo_one, "\(value)") + case .two: + return String(format: self._LastSeen_MinutesAgo_two, "\(value)") + case .few: + return String(format: self._LastSeen_MinutesAgo_few, "\(value)") + case .many: + return String(format: self._LastSeen_MinutesAgo_many, "\(value)") + case .other: + return String(format: self._LastSeen_MinutesAgo_other, "\(value)") } } private let _ForwardedVideos_zero: String @@ -4929,6 +4662,116 @@ public final class PresentationStrings { return String(format: self._ForwardedVideos_other, "\(value)") } } + private let _DialogList_LiveLocationChatsCount_zero: String + private let _DialogList_LiveLocationChatsCount_one: String + private let _DialogList_LiveLocationChatsCount_two: String + private let _DialogList_LiveLocationChatsCount_few: String + private let _DialogList_LiveLocationChatsCount_many: String + private let _DialogList_LiveLocationChatsCount_other: String + public func DialogList_LiveLocationChatsCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._DialogList_LiveLocationChatsCount_zero, "\(value)") + case .one: + return String(format: self._DialogList_LiveLocationChatsCount_one, "\(value)") + case .two: + return String(format: self._DialogList_LiveLocationChatsCount_two, "\(value)") + case .few: + return String(format: self._DialogList_LiveLocationChatsCount_few, "\(value)") + case .many: + return String(format: self._DialogList_LiveLocationChatsCount_many, "\(value)") + case .other: + return String(format: self._DialogList_LiveLocationChatsCount_other, "\(value)") + } + } + private let _MessageTimer_Minutes_zero: String + private let _MessageTimer_Minutes_one: String + private let _MessageTimer_Minutes_two: String + private let _MessageTimer_Minutes_few: String + private let _MessageTimer_Minutes_many: String + private let _MessageTimer_Minutes_other: String + public func MessageTimer_Minutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Minutes_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Minutes_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Minutes_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Minutes_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Minutes_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Minutes_other, "\(value)") + } + } + private let _Map_ETAMinutes_zero: String + private let _Map_ETAMinutes_one: String + private let _Map_ETAMinutes_two: String + private let _Map_ETAMinutes_few: String + private let _Map_ETAMinutes_many: String + private let _Map_ETAMinutes_other: String + public func Map_ETAMinutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Map_ETAMinutes_zero, "\(value)") + case .one: + return String(format: self._Map_ETAMinutes_one, "\(value)") + case .two: + return String(format: self._Map_ETAMinutes_two, "\(value)") + case .few: + return String(format: self._Map_ETAMinutes_few, "\(value)") + case .many: + return String(format: self._Map_ETAMinutes_many, "\(value)") + case .other: + return String(format: self._Map_ETAMinutes_other, "\(value)") + } + } + private let _LastSeen_HoursAgo_zero: String + private let _LastSeen_HoursAgo_one: String + private let _LastSeen_HoursAgo_two: String + private let _LastSeen_HoursAgo_few: String + private let _LastSeen_HoursAgo_many: String + private let _LastSeen_HoursAgo_other: String + public func LastSeen_HoursAgo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._LastSeen_HoursAgo_zero, "\(value)") + case .one: + return String(format: self._LastSeen_HoursAgo_one, "\(value)") + case .two: + return String(format: self._LastSeen_HoursAgo_two, "\(value)") + case .few: + return String(format: self._LastSeen_HoursAgo_few, "\(value)") + case .many: + return String(format: self._LastSeen_HoursAgo_many, "\(value)") + case .other: + return String(format: self._LastSeen_HoursAgo_other, "\(value)") + } + } + private let _StickerPack_AddMaskCount_zero: String + private let _StickerPack_AddMaskCount_one: String + private let _StickerPack_AddMaskCount_two: String + private let _StickerPack_AddMaskCount_few: String + private let _StickerPack_AddMaskCount_many: String + private let _StickerPack_AddMaskCount_other: String + public func StickerPack_AddMaskCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_AddMaskCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_AddMaskCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_AddMaskCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_AddMaskCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_AddMaskCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_AddMaskCount_other, "\(value)") + } + } private let _MessageTimer_Weeks_zero: String private let _MessageTimer_Weeks_one: String private let _MessageTimer_Weeks_two: String @@ -4951,6 +4794,50 @@ public final class PresentationStrings { return String(format: self._MessageTimer_Weeks_other, "\(value)") } } + private let _ForwardedAuthorsOthers_zero: String + private let _ForwardedAuthorsOthers_one: String + private let _ForwardedAuthorsOthers_two: String + private let _ForwardedAuthorsOthers_few: String + private let _ForwardedAuthorsOthers_many: String + private let _ForwardedAuthorsOthers_other: String + public func ForwardedAuthorsOthers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedAuthorsOthers_zero, "\(value)") + case .one: + return String(format: self._ForwardedAuthorsOthers_one, "\(value)") + case .two: + return String(format: self._ForwardedAuthorsOthers_two, "\(value)") + case .few: + return String(format: self._ForwardedAuthorsOthers_few, "\(value)") + case .many: + return String(format: self._ForwardedAuthorsOthers_many, "\(value)") + case .other: + return String(format: self._ForwardedAuthorsOthers_other, "\(value)") + } + } + private let _ServiceMessage_GameScoreExtended_zero: String + private let _ServiceMessage_GameScoreExtended_one: String + private let _ServiceMessage_GameScoreExtended_two: String + private let _ServiceMessage_GameScoreExtended_few: String + private let _ServiceMessage_GameScoreExtended_many: String + private let _ServiceMessage_GameScoreExtended_other: String + public func ServiceMessage_GameScoreExtended(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ServiceMessage_GameScoreExtended_zero, "\(value)") + case .one: + return String(format: self._ServiceMessage_GameScoreExtended_one, "\(value)") + case .two: + return String(format: self._ServiceMessage_GameScoreExtended_two, "\(value)") + case .few: + return String(format: self._ServiceMessage_GameScoreExtended_few, "\(value)") + case .many: + return String(format: self._ServiceMessage_GameScoreExtended_many, "\(value)") + case .other: + return String(format: self._ServiceMessage_GameScoreExtended_other, "\(value)") + } + } private let _MessageTimer_Hours_zero: String private let _MessageTimer_Hours_one: String private let _MessageTimer_Hours_two: String @@ -4973,6 +4860,138 @@ public final class PresentationStrings { return String(format: self._MessageTimer_Hours_other, "\(value)") } } + private let _ForwardedLocations_zero: String + private let _ForwardedLocations_one: String + private let _ForwardedLocations_two: String + private let _ForwardedLocations_few: String + private let _ForwardedLocations_many: String + private let _ForwardedLocations_other: String + public func ForwardedLocations(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedLocations_zero, "\(value)") + case .one: + return String(format: self._ForwardedLocations_one, "\(value)") + case .two: + return String(format: self._ForwardedLocations_two, "\(value)") + case .few: + return String(format: self._ForwardedLocations_few, "\(value)") + case .many: + return String(format: self._ForwardedLocations_many, "\(value)") + case .other: + return String(format: self._ForwardedLocations_other, "\(value)") + } + } + private let _PrivacyLastSeenSettings_AddUsers_zero: String + private let _PrivacyLastSeenSettings_AddUsers_one: String + private let _PrivacyLastSeenSettings_AddUsers_two: String + private let _PrivacyLastSeenSettings_AddUsers_few: String + private let _PrivacyLastSeenSettings_AddUsers_many: String + private let _PrivacyLastSeenSettings_AddUsers_other: String + public func PrivacyLastSeenSettings_AddUsers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._PrivacyLastSeenSettings_AddUsers_zero, "\(value)") + case .one: + return String(format: self._PrivacyLastSeenSettings_AddUsers_one, "\(value)") + case .two: + return String(format: self._PrivacyLastSeenSettings_AddUsers_two, "\(value)") + case .few: + return String(format: self._PrivacyLastSeenSettings_AddUsers_few, "\(value)") + case .many: + return String(format: self._PrivacyLastSeenSettings_AddUsers_many, "\(value)") + case .other: + return String(format: self._PrivacyLastSeenSettings_AddUsers_other, "\(value)") + } + } + private let _MuteFor_Hours_zero: String + private let _MuteFor_Hours_one: String + private let _MuteFor_Hours_two: String + private let _MuteFor_Hours_few: String + private let _MuteFor_Hours_many: String + private let _MuteFor_Hours_other: String + public func MuteFor_Hours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteFor_Hours_zero, "\(value)") + case .one: + return String(format: self._MuteFor_Hours_one, "\(value)") + case .two: + return String(format: self._MuteFor_Hours_two, "\(value)") + case .few: + return String(format: self._MuteFor_Hours_few, "\(value)") + case .many: + return String(format: self._MuteFor_Hours_many, "\(value)") + case .other: + return String(format: self._MuteFor_Hours_other, "\(value)") + } + } + private let _ForwardedPhotos_zero: String + private let _ForwardedPhotos_one: String + private let _ForwardedPhotos_two: String + private let _ForwardedPhotos_few: String + private let _ForwardedPhotos_many: String + private let _ForwardedPhotos_other: String + public func ForwardedPhotos(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedPhotos_zero, "\(value)") + case .one: + return String(format: self._ForwardedPhotos_one, "\(value)") + case .two: + return String(format: self._ForwardedPhotos_two, "\(value)") + case .few: + return String(format: self._ForwardedPhotos_few, "\(value)") + case .many: + return String(format: self._ForwardedPhotos_many, "\(value)") + case .other: + return String(format: self._ForwardedPhotos_other, "\(value)") + } + } + private let _ServiceMessage_GameScoreSelfSimple_zero: String + private let _ServiceMessage_GameScoreSelfSimple_one: String + private let _ServiceMessage_GameScoreSelfSimple_two: String + private let _ServiceMessage_GameScoreSelfSimple_few: String + private let _ServiceMessage_GameScoreSelfSimple_many: String + private let _ServiceMessage_GameScoreSelfSimple_other: String + public func ServiceMessage_GameScoreSelfSimple(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ServiceMessage_GameScoreSelfSimple_zero, "\(value)") + case .one: + return String(format: self._ServiceMessage_GameScoreSelfSimple_one, "\(value)") + case .two: + return String(format: self._ServiceMessage_GameScoreSelfSimple_two, "\(value)") + case .few: + return String(format: self._ServiceMessage_GameScoreSelfSimple_few, "\(value)") + case .many: + return String(format: self._ServiceMessage_GameScoreSelfSimple_many, "\(value)") + case .other: + return String(format: self._ServiceMessage_GameScoreSelfSimple_other, "\(value)") + } + } + private let _Conversation_StatusSubscribers_zero: String + private let _Conversation_StatusSubscribers_one: String + private let _Conversation_StatusSubscribers_two: String + private let _Conversation_StatusSubscribers_few: String + private let _Conversation_StatusSubscribers_many: String + private let _Conversation_StatusSubscribers_other: String + public func Conversation_StatusSubscribers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Conversation_StatusSubscribers_zero, "\(value)") + case .one: + return String(format: self._Conversation_StatusSubscribers_one, "\(value)") + case .two: + return String(format: self._Conversation_StatusSubscribers_two, "\(value)") + case .few: + return String(format: self._Conversation_StatusSubscribers_few, "\(value)") + case .many: + return String(format: self._Conversation_StatusSubscribers_many, "\(value)") + case .other: + return String(format: self._Conversation_StatusSubscribers_other, "\(value)") + } + } init(languageCode: String, dict: [String: String]) { @@ -5044,6 +5063,7 @@ public final class PresentationStrings { self.Channel_Username_Help = getValue(dict, "Channel.Username.Help") self._Profile_CreateEncryptedChatOutdatedError = getValue(dict, "Profile.CreateEncryptedChatOutdatedError") self._Profile_CreateEncryptedChatOutdatedError_r = extractArgumentRanges(self._Profile_CreateEncryptedChatOutdatedError) + self.ContactInfo_PhoneLabelPager = getValue(dict, "ContactInfo.PhoneLabelPager") self._PINNED_STICKER = getValue(dict, "PINNED_STICKER") self._PINNED_STICKER_r = extractArgumentRanges(self._PINNED_STICKER) self.AutoDownloadSettings_Title = getValue(dict, "AutoDownloadSettings.Title") @@ -5096,6 +5116,7 @@ public final class PresentationStrings { self.Calls_NoCallsPlaceholder = getValue(dict, "Calls.NoCallsPlaceholder") self.Conversation_PinMessageAlert_OnlyPin = getValue(dict, "Conversation.PinMessageAlert.OnlyPin") self.PasscodeSettings_UnlockWithFaceId = getValue(dict, "PasscodeSettings.UnlockWithFaceId") + self.ContactInfo_Title = getValue(dict, "ContactInfo.Title") self.ReportPeer_ReasonOther_Send = getValue(dict, "ReportPeer.ReasonOther.Send") self.Conversation_InstantPagePreview = getValue(dict, "Conversation.InstantPagePreview") self.PasscodeSettings_SimplePasscodeHelp = getValue(dict, "PasscodeSettings.SimplePasscodeHelp") @@ -5211,6 +5232,7 @@ public final class PresentationStrings { self._CHANNEL_MESSAGE_NOTEXT_r = extractArgumentRanges(self._CHANNEL_MESSAGE_NOTEXT) self.Month_GenSeptember = getValue(dict, "Month.GenSeptember") self.PrivacySettings_LastSeenEverybody = getValue(dict, "PrivacySettings.LastSeenEverybody") + self.Contacts_NotRegisteredSection = getValue(dict, "Contacts.NotRegisteredSection") self.PhotoEditor_BlurToolRadial = getValue(dict, "PhotoEditor.BlurToolRadial") self.TwoStepAuth_PasswordRemoveConfirmation = getValue(dict, "TwoStepAuth.PasswordRemoveConfirmation") self.Channel_EditAdmin_PermissionEditMessages = getValue(dict, "Channel.EditAdmin.PermissionEditMessages") @@ -5292,6 +5314,7 @@ public final class PresentationStrings { self.PhotoEditor_QualityMedium = getValue(dict, "PhotoEditor.QualityMedium") self.Privacy_PaymentsClearInfo = getValue(dict, "Privacy.PaymentsClearInfo") self.PhotoEditor_CurvesRed = getValue(dict, "PhotoEditor.CurvesRed") + self.ContactInfo_PhoneLabelWorkFax = getValue(dict, "ContactInfo.PhoneLabelWorkFax") self.Privacy_PaymentsTitle = getValue(dict, "Privacy.PaymentsTitle") self.SocksProxySetup_ProxyType = getValue(dict, "SocksProxySetup.ProxyType") self._Time_PreciseDate_m8 = getValue(dict, "Time.PreciseDate_m8") @@ -5715,6 +5738,7 @@ public final class PresentationStrings { self._Conversation_RestrictedMediaTimed = getValue(dict, "Conversation.RestrictedMediaTimed") self._Conversation_RestrictedMediaTimed_r = extractArgumentRanges(self._Conversation_RestrictedMediaTimed) self.Login_InfoDeletePhoto = getValue(dict, "Login.InfoDeletePhoto") + self.ContactInfo_BirthdayLabel = getValue(dict, "ContactInfo.BirthdayLabel") self.TwoStepAuth_RecoveryCodeExpired = getValue(dict, "TwoStepAuth.RecoveryCodeExpired") self.AutoDownloadSettings_Channels = getValue(dict, "AutoDownloadSettings.Channels") self.AutoDownloadSettings_Contacts = getValue(dict, "AutoDownloadSettings.Contacts") @@ -5801,6 +5825,7 @@ public final class PresentationStrings { self.Channel_Setup_TypePublic = getValue(dict, "Channel.Setup.TypePublic") self._ChangePhone_ErrorOccupied = getValue(dict, "ChangePhone.ErrorOccupied") self._ChangePhone_ErrorOccupied_r = extractArgumentRanges(self._ChangePhone_ErrorOccupied) + self.ContactInfo_PhoneLabelMain = getValue(dict, "ContactInfo.PhoneLabelMain") self.Clipboard_SendPhoto = getValue(dict, "Clipboard.SendPhoto") self.Privacy_GroupsAndChannels_CustomShareHelp = getValue(dict, "Privacy.GroupsAndChannels.CustomShareHelp") self.KeyCommand_ChatInfo = getValue(dict, "KeyCommand.ChatInfo") @@ -5920,6 +5945,7 @@ public final class PresentationStrings { self._Channel_AdminLog_MessageInvitedName_r = extractArgumentRanges(self._Channel_AdminLog_MessageInvitedName) self.Conversation_Moderate_Ban = getValue(dict, "Conversation.Moderate.Ban") self.Group_Status = getValue(dict, "Group.Status") + self.ContactInfo_PhoneLabelOther = getValue(dict, "ContactInfo.PhoneLabelOther") self.Conversation_InputTextPlaceholder = getValue(dict, "Conversation.InputTextPlaceholder") self.TwoStepAuth_RecoveryCode = getValue(dict, "TwoStepAuth.RecoveryCode") self.SharedMedia_CategoryDocs = getValue(dict, "SharedMedia.CategoryDocs") @@ -5953,6 +5979,7 @@ public final class PresentationStrings { self.SocksProxySetup_SecretPlaceholder = getValue(dict, "SocksProxySetup.SecretPlaceholder") self.Channel_EditAdmin_PermissinAddAdminOn = getValue(dict, "Channel.EditAdmin.PermissinAddAdminOn") self.WebSearch_GIFs = getValue(dict, "WebSearch.GIFs") + self.Privacy_ChatsTitle = getValue(dict, "Privacy.ChatsTitle") self.Conversation_SavedMessages = getValue(dict, "Conversation.SavedMessages") self.TwoStepAuth_EnterPasswordTitle = getValue(dict, "TwoStepAuth.EnterPasswordTitle") self._CHANNEL_MESSAGE_GAME = getValue(dict, "CHANNEL_MESSAGE_GAME") @@ -6203,6 +6230,7 @@ public final class PresentationStrings { self.Settings_ProxyConnected = getValue(dict, "Settings.ProxyConnected") self.ChatSettings_AutoDownloadVoiceMessages = getValue(dict, "ChatSettings.AutoDownloadVoiceMessages") self.TwoStepAuth_EmailSkip = getValue(dict, "TwoStepAuth.EmailSkip") + self.Conversation_ViewContactDetails = getValue(dict, "Conversation.ViewContactDetails") self.Conversation_JumpToDate = getValue(dict, "Conversation.JumpToDate") self.AutoDownloadSettings_VideoMessagesTitle = getValue(dict, "AutoDownloadSettings.VideoMessagesTitle") self.CheckoutInfo_ReceiverInfoEmailPlaceholder = getValue(dict, "CheckoutInfo.ReceiverInfoEmailPlaceholder") @@ -6254,6 +6282,7 @@ public final class PresentationStrings { self.PrivacyLastSeenSettings_AlwaysShareWith_Placeholder = getValue(dict, "PrivacyLastSeenSettings.AlwaysShareWith.Placeholder") self.Channel_Members_Title = getValue(dict, "Channel.Members.Title") self.Channel_AdminLog_CanDeleteMessages = getValue(dict, "Channel.AdminLog.CanDeleteMessages") + self.Privacy_DeleteDrafts = getValue(dict, "Privacy.DeleteDrafts") self.Group_Setup_TypePrivateHelp = getValue(dict, "Group.Setup.TypePrivateHelp") self._Notification_PinnedVideoMessage = getValue(dict, "Notification.PinnedVideoMessage") self._Notification_PinnedVideoMessage_r = extractArgumentRanges(self._Notification_PinnedVideoMessage) @@ -6372,6 +6401,7 @@ public final class PresentationStrings { self.Channel_BanUser_BlockFor = getValue(dict, "Channel.BanUser.BlockFor") self.Call_StatusConnecting = getValue(dict, "Call.StatusConnecting") self.AutoNightTheme_NotAvailable = getValue(dict, "AutoNightTheme.NotAvailable") + self.PrivateDataSettings_Title = getValue(dict, "PrivateDataSettings.Title") self.Bot_Start = getValue(dict, "Bot.Start") self._Channel_AdminLog_MessageChangedGroupAbout = getValue(dict, "Channel.AdminLog.MessageChangedGroupAbout") self._Channel_AdminLog_MessageChangedGroupAbout_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedGroupAbout) @@ -6433,6 +6463,7 @@ public final class PresentationStrings { self.TwoStepAuth_RecoveryTitle = getValue(dict, "TwoStepAuth.RecoveryTitle") self.WatchRemote_AlertOpen = getValue(dict, "WatchRemote.AlertOpen") self.ExplicitContent_AlertChannel = getValue(dict, "ExplicitContent.AlertChannel") + self.ContactInfo_PhoneLabelMobile = getValue(dict, "ContactInfo.PhoneLabelMobile") self.Widget_AuthRequired = getValue(dict, "Widget.AuthRequired") self._ForwardedAuthors2 = getValue(dict, "ForwardedAuthors2") self._ForwardedAuthors2_r = extractArgumentRanges(self._ForwardedAuthors2) @@ -6703,6 +6734,7 @@ public final class PresentationStrings { self.Watch_Notification_Joined = getValue(dict, "Watch.Notification.Joined") self._Channel_AdminLog_MessageRestrictedNewSetting = getValue(dict, "Channel.AdminLog.MessageRestrictedNewSetting") self._Channel_AdminLog_MessageRestrictedNewSetting_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestrictedNewSetting) + self.ContactInfo_PhoneLabelHome = getValue(dict, "ContactInfo.PhoneLabelHome") self.GroupInfo_DeleteAndExitConfirmation = getValue(dict, "GroupInfo.DeleteAndExitConfirmation") self.TwoStepAuth_EmailInvalid = getValue(dict, "TwoStepAuth.EmailInvalid") self.Privacy_ContactsTitle = getValue(dict, "Privacy.ContactsTitle") @@ -6714,6 +6746,7 @@ public final class PresentationStrings { self._Login_EmailCodeSubject_r = extractArgumentRanges(self._Login_EmailCodeSubject) self._CHAT_TITLE_EDITED = getValue(dict, "CHAT_TITLE_EDITED") self._CHAT_TITLE_EDITED_r = extractArgumentRanges(self._CHAT_TITLE_EDITED) + self.ContactInfo_PhoneLabelHomeFax = getValue(dict, "ContactInfo.PhoneLabelHomeFax") self._NetworkUsageSettings_WifiUsageSince = getValue(dict, "NetworkUsageSettings.WifiUsageSince") self._NetworkUsageSettings_WifiUsageSince_r = extractArgumentRanges(self._NetworkUsageSettings_WifiUsageSince) self.Watch_LastSeen_Lately = getValue(dict, "Watch.LastSeen.Lately") @@ -6751,6 +6784,7 @@ public final class PresentationStrings { self.BlockedUsers_Title = getValue(dict, "BlockedUsers.Title") self._LiveLocationUpdated_TodayAt = getValue(dict, "LiveLocationUpdated.TodayAt") self._LiveLocationUpdated_TodayAt_r = extractArgumentRanges(self._LiveLocationUpdated_TodayAt) + self.ContactInfo_PhoneLabelWork = getValue(dict, "ContactInfo.PhoneLabelWork") self.ChatSettings_ConnectionType_UseSocks5 = getValue(dict, "ChatSettings.ConnectionType.UseSocks5") self.Cache_ClearNone = getValue(dict, "Cache.ClearNone") self.SecretTimer_VideoDescription = getValue(dict, "SecretTimer.VideoDescription") @@ -6803,6 +6837,7 @@ public final class PresentationStrings { self.ChatSettings_Stickers = getValue(dict, "ChatSettings.Stickers") self.Camera_FlashOff = getValue(dict, "Camera.FlashOff") self.TwoStepAuth_Title = getValue(dict, "TwoStepAuth.Title") + self.PrivacySettings_DataSettingsHelp = getValue(dict, "PrivacySettings.DataSettingsHelp") self.Checkout_ErrorProviderAccountTimeout = getValue(dict, "Checkout.ErrorProviderAccountTimeout") self.TwoStepAuth_SetupPasswordEnterPasswordChange = getValue(dict, "TwoStepAuth.SetupPasswordEnterPasswordChange") self.WebSearch_Images = getValue(dict, "WebSearch.Images") @@ -6909,6 +6944,7 @@ public final class PresentationStrings { self.Notification_RenamedGroup = getValue(dict, "Notification.RenamedGroup") self._Call_PrivacyErrorMessage = getValue(dict, "Call.PrivacyErrorMessage") self._Call_PrivacyErrorMessage_r = extractArgumentRanges(self._Call_PrivacyErrorMessage) + self.PrivacySettings_DataSettings = getValue(dict, "PrivacySettings.DataSettings") self.ChangePhoneNumberNumber_Title = getValue(dict, "ChangePhoneNumberNumber.Title") self.TwoStepAuth_EnterPasswordInvalid = getValue(dict, "TwoStepAuth.EnterPasswordInvalid") self.DialogList_SearchSectionMessages = getValue(dict, "DialogList.SearchSectionMessages") @@ -7022,6 +7058,7 @@ public final class PresentationStrings { self.GroupInfo_Sound = getValue(dict, "GroupInfo.Sound") self.Channel_EditAdmin_PermissionBanUsers = getValue(dict, "Channel.EditAdmin.PermissionBanUsers") self.InfoPlist_NSCameraUsageDescription = getValue(dict, "InfoPlist.NSCameraUsageDescription") + self.ContactInfo_Job = getValue(dict, "ContactInfo.Job") self.Wallpaper_PhotoLibrary = getValue(dict, "Wallpaper.PhotoLibrary") self.Settings_About = getValue(dict, "Settings.About") self.Privacy_Calls_IntegrationHelp = getValue(dict, "Privacy.Calls.IntegrationHelp") @@ -7091,6 +7128,7 @@ public final class PresentationStrings { self.Channel_AboutItem = getValue(dict, "Channel.AboutItem") self.PhotoEditor_CurvesGreen = getValue(dict, "PhotoEditor.CurvesGreen") self.Month_GenJuly = getValue(dict, "Month.GenJuly") + self.ContactInfo_URLLabelHomepage = getValue(dict, "ContactInfo.URLLabelHomepage") self._DialogList_SingleUploadingFileSuffix = getValue(dict, "DialogList.SingleUploadingFileSuffix") self._DialogList_SingleUploadingFileSuffix_r = extractArgumentRanges(self._DialogList_SingleUploadingFileSuffix) self.ChannelIntro_CreateChannel = getValue(dict, "ChannelIntro.CreateChannel") @@ -7159,492 +7197,492 @@ public final class PresentationStrings { self.PrivacySettings_PasscodeAndFaceId = getValue(dict, "PrivacySettings.PasscodeAndFaceId") self.Settings_ChatBackground = getValue(dict, "Settings.ChatBackground") self.TermsOfService_Confirm = getValue(dict, "TermsOfService.Confirm") - self._Watch_UserInfo_Mute_zero = getValueWithForm(dict, "Watch.UserInfo.Mute", .zero) - self._Watch_UserInfo_Mute_one = getValueWithForm(dict, "Watch.UserInfo.Mute", .one) - self._Watch_UserInfo_Mute_two = getValueWithForm(dict, "Watch.UserInfo.Mute", .two) - self._Watch_UserInfo_Mute_few = getValueWithForm(dict, "Watch.UserInfo.Mute", .few) - self._Watch_UserInfo_Mute_many = getValueWithForm(dict, "Watch.UserInfo.Mute", .many) - self._Watch_UserInfo_Mute_other = getValueWithForm(dict, "Watch.UserInfo.Mute", .other) - self._SharedMedia_Link_zero = getValueWithForm(dict, "SharedMedia.Link", .zero) - self._SharedMedia_Link_one = getValueWithForm(dict, "SharedMedia.Link", .one) - self._SharedMedia_Link_two = getValueWithForm(dict, "SharedMedia.Link", .two) - self._SharedMedia_Link_few = getValueWithForm(dict, "SharedMedia.Link", .few) - self._SharedMedia_Link_many = getValueWithForm(dict, "SharedMedia.Link", .many) - self._SharedMedia_Link_other = getValueWithForm(dict, "SharedMedia.Link", .other) - self._MessageTimer_Days_zero = getValueWithForm(dict, "MessageTimer.Days", .zero) - self._MessageTimer_Days_one = getValueWithForm(dict, "MessageTimer.Days", .one) - self._MessageTimer_Days_two = getValueWithForm(dict, "MessageTimer.Days", .two) - self._MessageTimer_Days_few = getValueWithForm(dict, "MessageTimer.Days", .few) - self._MessageTimer_Days_many = getValueWithForm(dict, "MessageTimer.Days", .many) - self._MessageTimer_Days_other = getValueWithForm(dict, "MessageTimer.Days", .other) - self._LiveLocationUpdated_MinutesAgo_zero = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .zero) - self._LiveLocationUpdated_MinutesAgo_one = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .one) - self._LiveLocationUpdated_MinutesAgo_two = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .two) - self._LiveLocationUpdated_MinutesAgo_few = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .few) - self._LiveLocationUpdated_MinutesAgo_many = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .many) - self._LiveLocationUpdated_MinutesAgo_other = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .other) - self._ForwardedAudios_zero = getValueWithForm(dict, "ForwardedAudios", .zero) - self._ForwardedAudios_one = getValueWithForm(dict, "ForwardedAudios", .one) - self._ForwardedAudios_two = getValueWithForm(dict, "ForwardedAudios", .two) - self._ForwardedAudios_few = getValueWithForm(dict, "ForwardedAudios", .few) - self._ForwardedAudios_many = getValueWithForm(dict, "ForwardedAudios", .many) - self._ForwardedAudios_other = getValueWithForm(dict, "ForwardedAudios", .other) - self._Invitation_Members_zero = getValueWithForm(dict, "Invitation.Members", .zero) - self._Invitation_Members_one = getValueWithForm(dict, "Invitation.Members", .one) - self._Invitation_Members_two = getValueWithForm(dict, "Invitation.Members", .two) - self._Invitation_Members_few = getValueWithForm(dict, "Invitation.Members", .few) - self._Invitation_Members_many = getValueWithForm(dict, "Invitation.Members", .many) - self._Invitation_Members_other = getValueWithForm(dict, "Invitation.Members", .other) - self._ForwardedStickers_zero = getValueWithForm(dict, "ForwardedStickers", .zero) - self._ForwardedStickers_one = getValueWithForm(dict, "ForwardedStickers", .one) - self._ForwardedStickers_two = getValueWithForm(dict, "ForwardedStickers", .two) - self._ForwardedStickers_few = getValueWithForm(dict, "ForwardedStickers", .few) - self._ForwardedStickers_many = getValueWithForm(dict, "ForwardedStickers", .many) - self._ForwardedStickers_other = getValueWithForm(dict, "ForwardedStickers", .other) - self._GroupInfo_ParticipantCount_zero = getValueWithForm(dict, "GroupInfo.ParticipantCount", .zero) - self._GroupInfo_ParticipantCount_one = getValueWithForm(dict, "GroupInfo.ParticipantCount", .one) - self._GroupInfo_ParticipantCount_two = getValueWithForm(dict, "GroupInfo.ParticipantCount", .two) - self._GroupInfo_ParticipantCount_few = getValueWithForm(dict, "GroupInfo.ParticipantCount", .few) - self._GroupInfo_ParticipantCount_many = getValueWithForm(dict, "GroupInfo.ParticipantCount", .many) - self._GroupInfo_ParticipantCount_other = getValueWithForm(dict, "GroupInfo.ParticipantCount", .other) - self._DialogList_LiveLocationChatsCount_zero = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .zero) - self._DialogList_LiveLocationChatsCount_one = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .one) - self._DialogList_LiveLocationChatsCount_two = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .two) - self._DialogList_LiveLocationChatsCount_few = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .few) - self._DialogList_LiveLocationChatsCount_many = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .many) - self._DialogList_LiveLocationChatsCount_other = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .other) - self._MessageTimer_Years_zero = getValueWithForm(dict, "MessageTimer.Years", .zero) - self._MessageTimer_Years_one = getValueWithForm(dict, "MessageTimer.Years", .one) - self._MessageTimer_Years_two = getValueWithForm(dict, "MessageTimer.Years", .two) - self._MessageTimer_Years_few = getValueWithForm(dict, "MessageTimer.Years", .few) - self._MessageTimer_Years_many = getValueWithForm(dict, "MessageTimer.Years", .many) - self._MessageTimer_Years_other = getValueWithForm(dict, "MessageTimer.Years", .other) - self._ServiceMessage_GameScoreExtended_zero = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .zero) - self._ServiceMessage_GameScoreExtended_one = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .one) - self._ServiceMessage_GameScoreExtended_two = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .two) - self._ServiceMessage_GameScoreExtended_few = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .few) - self._ServiceMessage_GameScoreExtended_many = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .many) - self._ServiceMessage_GameScoreExtended_other = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .other) - self._InviteText_ContactsCount_zero = getValueWithForm(dict, "InviteText.ContactsCount", .zero) - self._InviteText_ContactsCount_one = getValueWithForm(dict, "InviteText.ContactsCount", .one) - self._InviteText_ContactsCount_two = getValueWithForm(dict, "InviteText.ContactsCount", .two) - self._InviteText_ContactsCount_few = getValueWithForm(dict, "InviteText.ContactsCount", .few) - self._InviteText_ContactsCount_many = getValueWithForm(dict, "InviteText.ContactsCount", .many) - self._InviteText_ContactsCount_other = getValueWithForm(dict, "InviteText.ContactsCount", .other) - self._MessageTimer_ShortMinutes_zero = getValueWithForm(dict, "MessageTimer.ShortMinutes", .zero) - self._MessageTimer_ShortMinutes_one = getValueWithForm(dict, "MessageTimer.ShortMinutes", .one) - self._MessageTimer_ShortMinutes_two = getValueWithForm(dict, "MessageTimer.ShortMinutes", .two) - self._MessageTimer_ShortMinutes_few = getValueWithForm(dict, "MessageTimer.ShortMinutes", .few) - self._MessageTimer_ShortMinutes_many = getValueWithForm(dict, "MessageTimer.ShortMinutes", .many) - self._MessageTimer_ShortMinutes_other = getValueWithForm(dict, "MessageTimer.ShortMinutes", .other) - self._Call_Seconds_zero = getValueWithForm(dict, "Call.Seconds", .zero) - self._Call_Seconds_one = getValueWithForm(dict, "Call.Seconds", .one) - self._Call_Seconds_two = getValueWithForm(dict, "Call.Seconds", .two) - self._Call_Seconds_few = getValueWithForm(dict, "Call.Seconds", .few) - self._Call_Seconds_many = getValueWithForm(dict, "Call.Seconds", .many) - self._Call_Seconds_other = getValueWithForm(dict, "Call.Seconds", .other) - self._Watch_LastSeen_MinutesAgo_zero = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .zero) - self._Watch_LastSeen_MinutesAgo_one = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .one) - self._Watch_LastSeen_MinutesAgo_two = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .two) - self._Watch_LastSeen_MinutesAgo_few = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .few) - self._Watch_LastSeen_MinutesAgo_many = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .many) - self._Watch_LastSeen_MinutesAgo_other = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .other) - self._Media_SharePhoto_zero = getValueWithForm(dict, "Media.SharePhoto", .zero) - self._Media_SharePhoto_one = getValueWithForm(dict, "Media.SharePhoto", .one) - self._Media_SharePhoto_two = getValueWithForm(dict, "Media.SharePhoto", .two) - self._Media_SharePhoto_few = getValueWithForm(dict, "Media.SharePhoto", .few) - self._Media_SharePhoto_many = getValueWithForm(dict, "Media.SharePhoto", .many) - self._Media_SharePhoto_other = getValueWithForm(dict, "Media.SharePhoto", .other) - self._Notification_GameScoreSelfSimple_zero = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .zero) - self._Notification_GameScoreSelfSimple_one = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .one) - self._Notification_GameScoreSelfSimple_two = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .two) - self._Notification_GameScoreSelfSimple_few = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .few) - self._Notification_GameScoreSelfSimple_many = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .many) - self._Notification_GameScoreSelfSimple_other = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .other) - self._SharedMedia_Photo_zero = getValueWithForm(dict, "SharedMedia.Photo", .zero) - self._SharedMedia_Photo_one = getValueWithForm(dict, "SharedMedia.Photo", .one) - self._SharedMedia_Photo_two = getValueWithForm(dict, "SharedMedia.Photo", .two) - self._SharedMedia_Photo_few = getValueWithForm(dict, "SharedMedia.Photo", .few) - self._SharedMedia_Photo_many = getValueWithForm(dict, "SharedMedia.Photo", .many) - self._SharedMedia_Photo_other = getValueWithForm(dict, "SharedMedia.Photo", .other) - self._StickerPack_RemoveStickerCount_zero = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .zero) - self._StickerPack_RemoveStickerCount_one = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .one) - self._StickerPack_RemoveStickerCount_two = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .two) - self._StickerPack_RemoveStickerCount_few = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .few) - self._StickerPack_RemoveStickerCount_many = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .many) - self._StickerPack_RemoveStickerCount_other = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .other) - self._MessageTimer_Minutes_zero = getValueWithForm(dict, "MessageTimer.Minutes", .zero) - self._MessageTimer_Minutes_one = getValueWithForm(dict, "MessageTimer.Minutes", .one) - self._MessageTimer_Minutes_two = getValueWithForm(dict, "MessageTimer.Minutes", .two) - self._MessageTimer_Minutes_few = getValueWithForm(dict, "MessageTimer.Minutes", .few) - self._MessageTimer_Minutes_many = getValueWithForm(dict, "MessageTimer.Minutes", .many) - self._MessageTimer_Minutes_other = getValueWithForm(dict, "MessageTimer.Minutes", .other) - self._ForwardedFiles_zero = getValueWithForm(dict, "ForwardedFiles", .zero) - self._ForwardedFiles_one = getValueWithForm(dict, "ForwardedFiles", .one) - self._ForwardedFiles_two = getValueWithForm(dict, "ForwardedFiles", .two) - self._ForwardedFiles_few = getValueWithForm(dict, "ForwardedFiles", .few) - self._ForwardedFiles_many = getValueWithForm(dict, "ForwardedFiles", .many) - self._ForwardedFiles_other = getValueWithForm(dict, "ForwardedFiles", .other) - self._ForwardedGifs_zero = getValueWithForm(dict, "ForwardedGifs", .zero) - self._ForwardedGifs_one = getValueWithForm(dict, "ForwardedGifs", .one) - self._ForwardedGifs_two = getValueWithForm(dict, "ForwardedGifs", .two) - self._ForwardedGifs_few = getValueWithForm(dict, "ForwardedGifs", .few) - self._ForwardedGifs_many = getValueWithForm(dict, "ForwardedGifs", .many) - self._ForwardedGifs_other = getValueWithForm(dict, "ForwardedGifs", .other) - self._Conversation_StatusMembers_zero = getValueWithForm(dict, "Conversation.StatusMembers", .zero) - self._Conversation_StatusMembers_one = getValueWithForm(dict, "Conversation.StatusMembers", .one) - self._Conversation_StatusMembers_two = getValueWithForm(dict, "Conversation.StatusMembers", .two) - self._Conversation_StatusMembers_few = getValueWithForm(dict, "Conversation.StatusMembers", .few) - self._Conversation_StatusMembers_many = getValueWithForm(dict, "Conversation.StatusMembers", .many) - self._Conversation_StatusMembers_other = getValueWithForm(dict, "Conversation.StatusMembers", .other) - self._MuteExpires_Hours_zero = getValueWithForm(dict, "MuteExpires.Hours", .zero) - self._MuteExpires_Hours_one = getValueWithForm(dict, "MuteExpires.Hours", .one) - self._MuteExpires_Hours_two = getValueWithForm(dict, "MuteExpires.Hours", .two) - self._MuteExpires_Hours_few = getValueWithForm(dict, "MuteExpires.Hours", .few) - self._MuteExpires_Hours_many = getValueWithForm(dict, "MuteExpires.Hours", .many) - self._MuteExpires_Hours_other = getValueWithForm(dict, "MuteExpires.Hours", .other) - self._Contacts_ImportersCount_zero = getValueWithForm(dict, "Contacts.ImportersCount", .zero) - self._Contacts_ImportersCount_one = getValueWithForm(dict, "Contacts.ImportersCount", .one) - self._Contacts_ImportersCount_two = getValueWithForm(dict, "Contacts.ImportersCount", .two) - self._Contacts_ImportersCount_few = getValueWithForm(dict, "Contacts.ImportersCount", .few) - self._Contacts_ImportersCount_many = getValueWithForm(dict, "Contacts.ImportersCount", .many) - self._Contacts_ImportersCount_other = getValueWithForm(dict, "Contacts.ImportersCount", .other) - self._MuteExpires_Days_zero = getValueWithForm(dict, "MuteExpires.Days", .zero) - self._MuteExpires_Days_one = getValueWithForm(dict, "MuteExpires.Days", .one) - self._MuteExpires_Days_two = getValueWithForm(dict, "MuteExpires.Days", .two) - self._MuteExpires_Days_few = getValueWithForm(dict, "MuteExpires.Days", .few) - self._MuteExpires_Days_many = getValueWithForm(dict, "MuteExpires.Days", .many) - self._MuteExpires_Days_other = getValueWithForm(dict, "MuteExpires.Days", .other) - self._MuteFor_Hours_zero = getValueWithForm(dict, "MuteFor.Hours", .zero) - self._MuteFor_Hours_one = getValueWithForm(dict, "MuteFor.Hours", .one) - self._MuteFor_Hours_two = getValueWithForm(dict, "MuteFor.Hours", .two) - self._MuteFor_Hours_few = getValueWithForm(dict, "MuteFor.Hours", .few) - self._MuteFor_Hours_many = getValueWithForm(dict, "MuteFor.Hours", .many) - self._MuteFor_Hours_other = getValueWithForm(dict, "MuteFor.Hours", .other) - self._MuteFor_Days_zero = getValueWithForm(dict, "MuteFor.Days", .zero) - self._MuteFor_Days_one = getValueWithForm(dict, "MuteFor.Days", .one) - self._MuteFor_Days_two = getValueWithForm(dict, "MuteFor.Days", .two) - self._MuteFor_Days_few = getValueWithForm(dict, "MuteFor.Days", .few) - self._MuteFor_Days_many = getValueWithForm(dict, "MuteFor.Days", .many) - self._MuteFor_Days_other = getValueWithForm(dict, "MuteFor.Days", .other) - self._ServiceMessage_GameScoreSelfExtended_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .zero) - self._ServiceMessage_GameScoreSelfExtended_one = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .one) - self._ServiceMessage_GameScoreSelfExtended_two = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .two) - self._ServiceMessage_GameScoreSelfExtended_few = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .few) - self._ServiceMessage_GameScoreSelfExtended_many = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .many) - self._ServiceMessage_GameScoreSelfExtended_other = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .other) - self._MessageTimer_ShortHours_zero = getValueWithForm(dict, "MessageTimer.ShortHours", .zero) - self._MessageTimer_ShortHours_one = getValueWithForm(dict, "MessageTimer.ShortHours", .one) - self._MessageTimer_ShortHours_two = getValueWithForm(dict, "MessageTimer.ShortHours", .two) - self._MessageTimer_ShortHours_few = getValueWithForm(dict, "MessageTimer.ShortHours", .few) - self._MessageTimer_ShortHours_many = getValueWithForm(dict, "MessageTimer.ShortHours", .many) - self._MessageTimer_ShortHours_other = getValueWithForm(dict, "MessageTimer.ShortHours", .other) - self._MessageTimer_ShortDays_zero = getValueWithForm(dict, "MessageTimer.ShortDays", .zero) - self._MessageTimer_ShortDays_one = getValueWithForm(dict, "MessageTimer.ShortDays", .one) - self._MessageTimer_ShortDays_two = getValueWithForm(dict, "MessageTimer.ShortDays", .two) - self._MessageTimer_ShortDays_few = getValueWithForm(dict, "MessageTimer.ShortDays", .few) - self._MessageTimer_ShortDays_many = getValueWithForm(dict, "MessageTimer.ShortDays", .many) - self._MessageTimer_ShortDays_other = getValueWithForm(dict, "MessageTimer.ShortDays", .other) - self._Conversation_LiveLocationMembersCount_zero = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .zero) - self._Conversation_LiveLocationMembersCount_one = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .one) - self._Conversation_LiveLocationMembersCount_two = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .two) - self._Conversation_LiveLocationMembersCount_few = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .few) - self._Conversation_LiveLocationMembersCount_many = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .many) - self._Conversation_LiveLocationMembersCount_other = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .other) - self._Notification_GameScoreExtended_zero = getValueWithForm(dict, "Notification.GameScoreExtended", .zero) - self._Notification_GameScoreExtended_one = getValueWithForm(dict, "Notification.GameScoreExtended", .one) - self._Notification_GameScoreExtended_two = getValueWithForm(dict, "Notification.GameScoreExtended", .two) - self._Notification_GameScoreExtended_few = getValueWithForm(dict, "Notification.GameScoreExtended", .few) - self._Notification_GameScoreExtended_many = getValueWithForm(dict, "Notification.GameScoreExtended", .many) - self._Notification_GameScoreExtended_other = getValueWithForm(dict, "Notification.GameScoreExtended", .other) - self._ForwardedAuthorsOthers_zero = getValueWithForm(dict, "ForwardedAuthorsOthers", .zero) - self._ForwardedAuthorsOthers_one = getValueWithForm(dict, "ForwardedAuthorsOthers", .one) - self._ForwardedAuthorsOthers_two = getValueWithForm(dict, "ForwardedAuthorsOthers", .two) - self._ForwardedAuthorsOthers_few = getValueWithForm(dict, "ForwardedAuthorsOthers", .few) - self._ForwardedAuthorsOthers_many = getValueWithForm(dict, "ForwardedAuthorsOthers", .many) - self._ForwardedAuthorsOthers_other = getValueWithForm(dict, "ForwardedAuthorsOthers", .other) self._Media_ShareItem_zero = getValueWithForm(dict, "Media.ShareItem", .zero) self._Media_ShareItem_one = getValueWithForm(dict, "Media.ShareItem", .one) self._Media_ShareItem_two = getValueWithForm(dict, "Media.ShareItem", .two) self._Media_ShareItem_few = getValueWithForm(dict, "Media.ShareItem", .few) self._Media_ShareItem_many = getValueWithForm(dict, "Media.ShareItem", .many) self._Media_ShareItem_other = getValueWithForm(dict, "Media.ShareItem", .other) - self._SharedMedia_Video_zero = getValueWithForm(dict, "SharedMedia.Video", .zero) - self._SharedMedia_Video_one = getValueWithForm(dict, "SharedMedia.Video", .one) - self._SharedMedia_Video_two = getValueWithForm(dict, "SharedMedia.Video", .two) - self._SharedMedia_Video_few = getValueWithForm(dict, "SharedMedia.Video", .few) - self._SharedMedia_Video_many = getValueWithForm(dict, "SharedMedia.Video", .many) - self._SharedMedia_Video_other = getValueWithForm(dict, "SharedMedia.Video", .other) - self._Forward_ConfirmMultipleFiles_zero = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .zero) - self._Forward_ConfirmMultipleFiles_one = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .one) - self._Forward_ConfirmMultipleFiles_two = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .two) - self._Forward_ConfirmMultipleFiles_few = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .few) - self._Forward_ConfirmMultipleFiles_many = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .many) - self._Forward_ConfirmMultipleFiles_other = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .other) - self._MessageTimer_ShortWeeks_zero = getValueWithForm(dict, "MessageTimer.ShortWeeks", .zero) - self._MessageTimer_ShortWeeks_one = getValueWithForm(dict, "MessageTimer.ShortWeeks", .one) - self._MessageTimer_ShortWeeks_two = getValueWithForm(dict, "MessageTimer.ShortWeeks", .two) - self._MessageTimer_ShortWeeks_few = getValueWithForm(dict, "MessageTimer.ShortWeeks", .few) - self._MessageTimer_ShortWeeks_many = getValueWithForm(dict, "MessageTimer.ShortWeeks", .many) - self._MessageTimer_ShortWeeks_other = getValueWithForm(dict, "MessageTimer.ShortWeeks", .other) - self._Call_Minutes_zero = getValueWithForm(dict, "Call.Minutes", .zero) - self._Call_Minutes_one = getValueWithForm(dict, "Call.Minutes", .one) - self._Call_Minutes_two = getValueWithForm(dict, "Call.Minutes", .two) - self._Call_Minutes_few = getValueWithForm(dict, "Call.Minutes", .few) - self._Call_Minutes_many = getValueWithForm(dict, "Call.Minutes", .many) - self._Call_Minutes_other = getValueWithForm(dict, "Call.Minutes", .other) - self._LastSeen_MinutesAgo_zero = getValueWithForm(dict, "LastSeen.MinutesAgo", .zero) - self._LastSeen_MinutesAgo_one = getValueWithForm(dict, "LastSeen.MinutesAgo", .one) - self._LastSeen_MinutesAgo_two = getValueWithForm(dict, "LastSeen.MinutesAgo", .two) - self._LastSeen_MinutesAgo_few = getValueWithForm(dict, "LastSeen.MinutesAgo", .few) - self._LastSeen_MinutesAgo_many = getValueWithForm(dict, "LastSeen.MinutesAgo", .many) - self._LastSeen_MinutesAgo_other = getValueWithForm(dict, "LastSeen.MinutesAgo", .other) - self._StickerPack_AddMaskCount_zero = getValueWithForm(dict, "StickerPack.AddMaskCount", .zero) - self._StickerPack_AddMaskCount_one = getValueWithForm(dict, "StickerPack.AddMaskCount", .one) - self._StickerPack_AddMaskCount_two = getValueWithForm(dict, "StickerPack.AddMaskCount", .two) - self._StickerPack_AddMaskCount_few = getValueWithForm(dict, "StickerPack.AddMaskCount", .few) - self._StickerPack_AddMaskCount_many = getValueWithForm(dict, "StickerPack.AddMaskCount", .many) - self._StickerPack_AddMaskCount_other = getValueWithForm(dict, "StickerPack.AddMaskCount", .other) - self._Notification_GameScoreSimple_zero = getValueWithForm(dict, "Notification.GameScoreSimple", .zero) - self._Notification_GameScoreSimple_one = getValueWithForm(dict, "Notification.GameScoreSimple", .one) - self._Notification_GameScoreSimple_two = getValueWithForm(dict, "Notification.GameScoreSimple", .two) - self._Notification_GameScoreSimple_few = getValueWithForm(dict, "Notification.GameScoreSimple", .few) - self._Notification_GameScoreSimple_many = getValueWithForm(dict, "Notification.GameScoreSimple", .many) - self._Notification_GameScoreSimple_other = getValueWithForm(dict, "Notification.GameScoreSimple", .other) - self._Notification_GameScoreSelfExtended_zero = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .zero) - self._Notification_GameScoreSelfExtended_one = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .one) - self._Notification_GameScoreSelfExtended_two = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .two) - self._Notification_GameScoreSelfExtended_few = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .few) - self._Notification_GameScoreSelfExtended_many = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .many) - self._Notification_GameScoreSelfExtended_other = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .other) - self._SharedMedia_File_zero = getValueWithForm(dict, "SharedMedia.File", .zero) - self._SharedMedia_File_one = getValueWithForm(dict, "SharedMedia.File", .one) - self._SharedMedia_File_two = getValueWithForm(dict, "SharedMedia.File", .two) - self._SharedMedia_File_few = getValueWithForm(dict, "SharedMedia.File", .few) - self._SharedMedia_File_many = getValueWithForm(dict, "SharedMedia.File", .many) - self._SharedMedia_File_other = getValueWithForm(dict, "SharedMedia.File", .other) - self._MessageTimer_Months_zero = getValueWithForm(dict, "MessageTimer.Months", .zero) - self._MessageTimer_Months_one = getValueWithForm(dict, "MessageTimer.Months", .one) - self._MessageTimer_Months_two = getValueWithForm(dict, "MessageTimer.Months", .two) - self._MessageTimer_Months_few = getValueWithForm(dict, "MessageTimer.Months", .few) - self._MessageTimer_Months_many = getValueWithForm(dict, "MessageTimer.Months", .many) - self._MessageTimer_Months_other = getValueWithForm(dict, "MessageTimer.Months", .other) - self._AttachmentMenu_SendGif_zero = getValueWithForm(dict, "AttachmentMenu.SendGif", .zero) - self._AttachmentMenu_SendGif_one = getValueWithForm(dict, "AttachmentMenu.SendGif", .one) - self._AttachmentMenu_SendGif_two = getValueWithForm(dict, "AttachmentMenu.SendGif", .two) - self._AttachmentMenu_SendGif_few = getValueWithForm(dict, "AttachmentMenu.SendGif", .few) - self._AttachmentMenu_SendGif_many = getValueWithForm(dict, "AttachmentMenu.SendGif", .many) - self._AttachmentMenu_SendGif_other = getValueWithForm(dict, "AttachmentMenu.SendGif", .other) - self._LiveLocation_MenuChatsCount_zero = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .zero) - self._LiveLocation_MenuChatsCount_one = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .one) - self._LiveLocation_MenuChatsCount_two = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .two) - self._LiveLocation_MenuChatsCount_few = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .few) - self._LiveLocation_MenuChatsCount_many = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .many) - self._LiveLocation_MenuChatsCount_other = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .other) - self._StickerPack_AddStickerCount_zero = getValueWithForm(dict, "StickerPack.AddStickerCount", .zero) - self._StickerPack_AddStickerCount_one = getValueWithForm(dict, "StickerPack.AddStickerCount", .one) - self._StickerPack_AddStickerCount_two = getValueWithForm(dict, "StickerPack.AddStickerCount", .two) - self._StickerPack_AddStickerCount_few = getValueWithForm(dict, "StickerPack.AddStickerCount", .few) - self._StickerPack_AddStickerCount_many = getValueWithForm(dict, "StickerPack.AddStickerCount", .many) - self._StickerPack_AddStickerCount_other = getValueWithForm(dict, "StickerPack.AddStickerCount", .other) - self._UserCount_zero = getValueWithForm(dict, "UserCount", .zero) - self._UserCount_one = getValueWithForm(dict, "UserCount", .one) - self._UserCount_two = getValueWithForm(dict, "UserCount", .two) - self._UserCount_few = getValueWithForm(dict, "UserCount", .few) - self._UserCount_many = getValueWithForm(dict, "UserCount", .many) - self._UserCount_other = getValueWithForm(dict, "UserCount", .other) - self._StickerPack_StickerCount_zero = getValueWithForm(dict, "StickerPack.StickerCount", .zero) - self._StickerPack_StickerCount_one = getValueWithForm(dict, "StickerPack.StickerCount", .one) - self._StickerPack_StickerCount_two = getValueWithForm(dict, "StickerPack.StickerCount", .two) - self._StickerPack_StickerCount_few = getValueWithForm(dict, "StickerPack.StickerCount", .few) - self._StickerPack_StickerCount_many = getValueWithForm(dict, "StickerPack.StickerCount", .many) - self._StickerPack_StickerCount_other = getValueWithForm(dict, "StickerPack.StickerCount", .other) - self._MessageTimer_Seconds_zero = getValueWithForm(dict, "MessageTimer.Seconds", .zero) - self._MessageTimer_Seconds_one = getValueWithForm(dict, "MessageTimer.Seconds", .one) - self._MessageTimer_Seconds_two = getValueWithForm(dict, "MessageTimer.Seconds", .two) - self._MessageTimer_Seconds_few = getValueWithForm(dict, "MessageTimer.Seconds", .few) - self._MessageTimer_Seconds_many = getValueWithForm(dict, "MessageTimer.Seconds", .many) - self._MessageTimer_Seconds_other = getValueWithForm(dict, "MessageTimer.Seconds", .other) - self._LastSeen_HoursAgo_zero = getValueWithForm(dict, "LastSeen.HoursAgo", .zero) - self._LastSeen_HoursAgo_one = getValueWithForm(dict, "LastSeen.HoursAgo", .one) - self._LastSeen_HoursAgo_two = getValueWithForm(dict, "LastSeen.HoursAgo", .two) - self._LastSeen_HoursAgo_few = getValueWithForm(dict, "LastSeen.HoursAgo", .few) - self._LastSeen_HoursAgo_many = getValueWithForm(dict, "LastSeen.HoursAgo", .many) - self._LastSeen_HoursAgo_other = getValueWithForm(dict, "LastSeen.HoursAgo", .other) - self._Map_ETAHours_zero = getValueWithForm(dict, "Map.ETAHours", .zero) - self._Map_ETAHours_one = getValueWithForm(dict, "Map.ETAHours", .one) - self._Map_ETAHours_two = getValueWithForm(dict, "Map.ETAHours", .two) - self._Map_ETAHours_few = getValueWithForm(dict, "Map.ETAHours", .few) - self._Map_ETAHours_many = getValueWithForm(dict, "Map.ETAHours", .many) - self._Map_ETAHours_other = getValueWithForm(dict, "Map.ETAHours", .other) - self._MessageTimer_ShortSeconds_zero = getValueWithForm(dict, "MessageTimer.ShortSeconds", .zero) - self._MessageTimer_ShortSeconds_one = getValueWithForm(dict, "MessageTimer.ShortSeconds", .one) - self._MessageTimer_ShortSeconds_two = getValueWithForm(dict, "MessageTimer.ShortSeconds", .two) - self._MessageTimer_ShortSeconds_few = getValueWithForm(dict, "MessageTimer.ShortSeconds", .few) - self._MessageTimer_ShortSeconds_many = getValueWithForm(dict, "MessageTimer.ShortSeconds", .many) - self._MessageTimer_ShortSeconds_other = getValueWithForm(dict, "MessageTimer.ShortSeconds", .other) - self._PasscodeSettings_FailedAttempts_zero = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .zero) - self._PasscodeSettings_FailedAttempts_one = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .one) - self._PasscodeSettings_FailedAttempts_two = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .two) - self._PasscodeSettings_FailedAttempts_few = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .few) - self._PasscodeSettings_FailedAttempts_many = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .many) - self._PasscodeSettings_FailedAttempts_other = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .other) - self._QuickSend_Photos_zero = getValueWithForm(dict, "QuickSend.Photos", .zero) - self._QuickSend_Photos_one = getValueWithForm(dict, "QuickSend.Photos", .one) - self._QuickSend_Photos_two = getValueWithForm(dict, "QuickSend.Photos", .two) - self._QuickSend_Photos_few = getValueWithForm(dict, "QuickSend.Photos", .few) - self._QuickSend_Photos_many = getValueWithForm(dict, "QuickSend.Photos", .many) - self._QuickSend_Photos_other = getValueWithForm(dict, "QuickSend.Photos", .other) self._Media_ShareVideo_zero = getValueWithForm(dict, "Media.ShareVideo", .zero) self._Media_ShareVideo_one = getValueWithForm(dict, "Media.ShareVideo", .one) self._Media_ShareVideo_two = getValueWithForm(dict, "Media.ShareVideo", .two) self._Media_ShareVideo_few = getValueWithForm(dict, "Media.ShareVideo", .few) self._Media_ShareVideo_many = getValueWithForm(dict, "Media.ShareVideo", .many) self._Media_ShareVideo_other = getValueWithForm(dict, "Media.ShareVideo", .other) - self._Call_ShortMinutes_zero = getValueWithForm(dict, "Call.ShortMinutes", .zero) - self._Call_ShortMinutes_one = getValueWithForm(dict, "Call.ShortMinutes", .one) - self._Call_ShortMinutes_two = getValueWithForm(dict, "Call.ShortMinutes", .two) - self._Call_ShortMinutes_few = getValueWithForm(dict, "Call.ShortMinutes", .few) - self._Call_ShortMinutes_many = getValueWithForm(dict, "Call.ShortMinutes", .many) - self._Call_ShortMinutes_other = getValueWithForm(dict, "Call.ShortMinutes", .other) - self._PrivacyLastSeenSettings_AddUsers_zero = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .zero) - self._PrivacyLastSeenSettings_AddUsers_one = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .one) - self._PrivacyLastSeenSettings_AddUsers_two = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .two) - self._PrivacyLastSeenSettings_AddUsers_few = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .few) - self._PrivacyLastSeenSettings_AddUsers_many = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .many) - self._PrivacyLastSeenSettings_AddUsers_other = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .other) - self._Watch_LastSeen_HoursAgo_zero = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .zero) - self._Watch_LastSeen_HoursAgo_one = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .one) - self._Watch_LastSeen_HoursAgo_two = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .two) - self._Watch_LastSeen_HoursAgo_few = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .few) - self._Watch_LastSeen_HoursAgo_many = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .many) - self._Watch_LastSeen_HoursAgo_other = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .other) + self._Call_Minutes_zero = getValueWithForm(dict, "Call.Minutes", .zero) + self._Call_Minutes_one = getValueWithForm(dict, "Call.Minutes", .one) + self._Call_Minutes_two = getValueWithForm(dict, "Call.Minutes", .two) + self._Call_Minutes_few = getValueWithForm(dict, "Call.Minutes", .few) + self._Call_Minutes_many = getValueWithForm(dict, "Call.Minutes", .many) + self._Call_Minutes_other = getValueWithForm(dict, "Call.Minutes", .other) + self._LiveLocation_MenuChatsCount_zero = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .zero) + self._LiveLocation_MenuChatsCount_one = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .one) + self._LiveLocation_MenuChatsCount_two = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .two) + self._LiveLocation_MenuChatsCount_few = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .few) + self._LiveLocation_MenuChatsCount_many = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .many) + self._LiveLocation_MenuChatsCount_other = getValueWithForm(dict, "LiveLocation.MenuChatsCount", .other) + self._SharedMedia_Photo_zero = getValueWithForm(dict, "SharedMedia.Photo", .zero) + self._SharedMedia_Photo_one = getValueWithForm(dict, "SharedMedia.Photo", .one) + self._SharedMedia_Photo_two = getValueWithForm(dict, "SharedMedia.Photo", .two) + self._SharedMedia_Photo_few = getValueWithForm(dict, "SharedMedia.Photo", .few) + self._SharedMedia_Photo_many = getValueWithForm(dict, "SharedMedia.Photo", .many) + self._SharedMedia_Photo_other = getValueWithForm(dict, "SharedMedia.Photo", .other) + self._AttachmentMenu_SendGif_zero = getValueWithForm(dict, "AttachmentMenu.SendGif", .zero) + self._AttachmentMenu_SendGif_one = getValueWithForm(dict, "AttachmentMenu.SendGif", .one) + self._AttachmentMenu_SendGif_two = getValueWithForm(dict, "AttachmentMenu.SendGif", .two) + self._AttachmentMenu_SendGif_few = getValueWithForm(dict, "AttachmentMenu.SendGif", .few) + self._AttachmentMenu_SendGif_many = getValueWithForm(dict, "AttachmentMenu.SendGif", .many) + self._AttachmentMenu_SendGif_other = getValueWithForm(dict, "AttachmentMenu.SendGif", .other) + self._MuteFor_Days_zero = getValueWithForm(dict, "MuteFor.Days", .zero) + self._MuteFor_Days_one = getValueWithForm(dict, "MuteFor.Days", .one) + self._MuteFor_Days_two = getValueWithForm(dict, "MuteFor.Days", .two) + self._MuteFor_Days_few = getValueWithForm(dict, "MuteFor.Days", .few) + self._MuteFor_Days_many = getValueWithForm(dict, "MuteFor.Days", .many) + self._MuteFor_Days_other = getValueWithForm(dict, "MuteFor.Days", .other) + self._ForwardedGifs_zero = getValueWithForm(dict, "ForwardedGifs", .zero) + self._ForwardedGifs_one = getValueWithForm(dict, "ForwardedGifs", .one) + self._ForwardedGifs_two = getValueWithForm(dict, "ForwardedGifs", .two) + self._ForwardedGifs_few = getValueWithForm(dict, "ForwardedGifs", .few) + self._ForwardedGifs_many = getValueWithForm(dict, "ForwardedGifs", .many) + self._ForwardedGifs_other = getValueWithForm(dict, "ForwardedGifs", .other) self._ForwardedVideoMessages_zero = getValueWithForm(dict, "ForwardedVideoMessages", .zero) self._ForwardedVideoMessages_one = getValueWithForm(dict, "ForwardedVideoMessages", .one) self._ForwardedVideoMessages_two = getValueWithForm(dict, "ForwardedVideoMessages", .two) self._ForwardedVideoMessages_few = getValueWithForm(dict, "ForwardedVideoMessages", .few) self._ForwardedVideoMessages_many = getValueWithForm(dict, "ForwardedVideoMessages", .many) self._ForwardedVideoMessages_other = getValueWithForm(dict, "ForwardedVideoMessages", .other) - self._ServiceMessage_GameScoreSelfSimple_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .zero) - self._ServiceMessage_GameScoreSelfSimple_one = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .one) - self._ServiceMessage_GameScoreSelfSimple_two = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .two) - self._ServiceMessage_GameScoreSelfSimple_few = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .few) - self._ServiceMessage_GameScoreSelfSimple_many = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .many) - self._ServiceMessage_GameScoreSelfSimple_other = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .other) - self._ServiceMessage_GameScoreSimple_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .zero) - self._ServiceMessage_GameScoreSimple_one = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .one) - self._ServiceMessage_GameScoreSimple_two = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .two) - self._ServiceMessage_GameScoreSimple_few = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .few) - self._ServiceMessage_GameScoreSimple_many = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .many) - self._ServiceMessage_GameScoreSimple_other = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .other) + self._Forward_ConfirmMultipleFiles_zero = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .zero) + self._Forward_ConfirmMultipleFiles_one = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .one) + self._Forward_ConfirmMultipleFiles_two = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .two) + self._Forward_ConfirmMultipleFiles_few = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .few) + self._Forward_ConfirmMultipleFiles_many = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .many) + self._Forward_ConfirmMultipleFiles_other = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .other) + self._MessageTimer_Months_zero = getValueWithForm(dict, "MessageTimer.Months", .zero) + self._MessageTimer_Months_one = getValueWithForm(dict, "MessageTimer.Months", .one) + self._MessageTimer_Months_two = getValueWithForm(dict, "MessageTimer.Months", .two) + self._MessageTimer_Months_few = getValueWithForm(dict, "MessageTimer.Months", .few) + self._MessageTimer_Months_many = getValueWithForm(dict, "MessageTimer.Months", .many) + self._MessageTimer_Months_other = getValueWithForm(dict, "MessageTimer.Months", .other) + self._Map_ETAHours_zero = getValueWithForm(dict, "Map.ETAHours", .zero) + self._Map_ETAHours_one = getValueWithForm(dict, "Map.ETAHours", .one) + self._Map_ETAHours_two = getValueWithForm(dict, "Map.ETAHours", .two) + self._Map_ETAHours_few = getValueWithForm(dict, "Map.ETAHours", .few) + self._Map_ETAHours_many = getValueWithForm(dict, "Map.ETAHours", .many) + self._Map_ETAHours_other = getValueWithForm(dict, "Map.ETAHours", .other) + self._StickerPack_RemoveStickerCount_zero = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .zero) + self._StickerPack_RemoveStickerCount_one = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .one) + self._StickerPack_RemoveStickerCount_two = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .two) + self._StickerPack_RemoveStickerCount_few = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .few) + self._StickerPack_RemoveStickerCount_many = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .many) + self._StickerPack_RemoveStickerCount_other = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .other) + self._MessageTimer_ShortWeeks_zero = getValueWithForm(dict, "MessageTimer.ShortWeeks", .zero) + self._MessageTimer_ShortWeeks_one = getValueWithForm(dict, "MessageTimer.ShortWeeks", .one) + self._MessageTimer_ShortWeeks_two = getValueWithForm(dict, "MessageTimer.ShortWeeks", .two) + self._MessageTimer_ShortWeeks_few = getValueWithForm(dict, "MessageTimer.ShortWeeks", .few) + self._MessageTimer_ShortWeeks_many = getValueWithForm(dict, "MessageTimer.ShortWeeks", .many) + self._MessageTimer_ShortWeeks_other = getValueWithForm(dict, "MessageTimer.ShortWeeks", .other) + self._Call_Seconds_zero = getValueWithForm(dict, "Call.Seconds", .zero) + self._Call_Seconds_one = getValueWithForm(dict, "Call.Seconds", .one) + self._Call_Seconds_two = getValueWithForm(dict, "Call.Seconds", .two) + self._Call_Seconds_few = getValueWithForm(dict, "Call.Seconds", .few) + self._Call_Seconds_many = getValueWithForm(dict, "Call.Seconds", .many) + self._Call_Seconds_other = getValueWithForm(dict, "Call.Seconds", .other) + self._ForwardedAudios_zero = getValueWithForm(dict, "ForwardedAudios", .zero) + self._ForwardedAudios_one = getValueWithForm(dict, "ForwardedAudios", .one) + self._ForwardedAudios_two = getValueWithForm(dict, "ForwardedAudios", .two) + self._ForwardedAudios_few = getValueWithForm(dict, "ForwardedAudios", .few) + self._ForwardedAudios_many = getValueWithForm(dict, "ForwardedAudios", .many) + self._ForwardedAudios_other = getValueWithForm(dict, "ForwardedAudios", .other) + self._Conversation_StatusMembers_zero = getValueWithForm(dict, "Conversation.StatusMembers", .zero) + self._Conversation_StatusMembers_one = getValueWithForm(dict, "Conversation.StatusMembers", .one) + self._Conversation_StatusMembers_two = getValueWithForm(dict, "Conversation.StatusMembers", .two) + self._Conversation_StatusMembers_few = getValueWithForm(dict, "Conversation.StatusMembers", .few) + self._Conversation_StatusMembers_many = getValueWithForm(dict, "Conversation.StatusMembers", .many) + self._Conversation_StatusMembers_other = getValueWithForm(dict, "Conversation.StatusMembers", .other) + self._PasscodeSettings_FailedAttempts_zero = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .zero) + self._PasscodeSettings_FailedAttempts_one = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .one) + self._PasscodeSettings_FailedAttempts_two = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .two) + self._PasscodeSettings_FailedAttempts_few = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .few) + self._PasscodeSettings_FailedAttempts_many = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .many) + self._PasscodeSettings_FailedAttempts_other = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .other) + self._MessageTimer_ShortDays_zero = getValueWithForm(dict, "MessageTimer.ShortDays", .zero) + self._MessageTimer_ShortDays_one = getValueWithForm(dict, "MessageTimer.ShortDays", .one) + self._MessageTimer_ShortDays_two = getValueWithForm(dict, "MessageTimer.ShortDays", .two) + self._MessageTimer_ShortDays_few = getValueWithForm(dict, "MessageTimer.ShortDays", .few) + self._MessageTimer_ShortDays_many = getValueWithForm(dict, "MessageTimer.ShortDays", .many) + self._MessageTimer_ShortDays_other = getValueWithForm(dict, "MessageTimer.ShortDays", .other) + self._Notification_GameScoreExtended_zero = getValueWithForm(dict, "Notification.GameScoreExtended", .zero) + self._Notification_GameScoreExtended_one = getValueWithForm(dict, "Notification.GameScoreExtended", .one) + self._Notification_GameScoreExtended_two = getValueWithForm(dict, "Notification.GameScoreExtended", .two) + self._Notification_GameScoreExtended_few = getValueWithForm(dict, "Notification.GameScoreExtended", .few) + self._Notification_GameScoreExtended_many = getValueWithForm(dict, "Notification.GameScoreExtended", .many) + self._Notification_GameScoreExtended_other = getValueWithForm(dict, "Notification.GameScoreExtended", .other) + self._MuteExpires_Hours_zero = getValueWithForm(dict, "MuteExpires.Hours", .zero) + self._MuteExpires_Hours_one = getValueWithForm(dict, "MuteExpires.Hours", .one) + self._MuteExpires_Hours_two = getValueWithForm(dict, "MuteExpires.Hours", .two) + self._MuteExpires_Hours_few = getValueWithForm(dict, "MuteExpires.Hours", .few) + self._MuteExpires_Hours_many = getValueWithForm(dict, "MuteExpires.Hours", .many) + self._MuteExpires_Hours_other = getValueWithForm(dict, "MuteExpires.Hours", .other) + self._InviteText_ContactsCount_zero = getValueWithForm(dict, "InviteText.ContactsCount", .zero) + self._InviteText_ContactsCount_one = getValueWithForm(dict, "InviteText.ContactsCount", .one) + self._InviteText_ContactsCount_two = getValueWithForm(dict, "InviteText.ContactsCount", .two) + self._InviteText_ContactsCount_few = getValueWithForm(dict, "InviteText.ContactsCount", .few) + self._InviteText_ContactsCount_many = getValueWithForm(dict, "InviteText.ContactsCount", .many) + self._InviteText_ContactsCount_other = getValueWithForm(dict, "InviteText.ContactsCount", .other) + self._StickerPack_AddStickerCount_zero = getValueWithForm(dict, "StickerPack.AddStickerCount", .zero) + self._StickerPack_AddStickerCount_one = getValueWithForm(dict, "StickerPack.AddStickerCount", .one) + self._StickerPack_AddStickerCount_two = getValueWithForm(dict, "StickerPack.AddStickerCount", .two) + self._StickerPack_AddStickerCount_few = getValueWithForm(dict, "StickerPack.AddStickerCount", .few) + self._StickerPack_AddStickerCount_many = getValueWithForm(dict, "StickerPack.AddStickerCount", .many) + self._StickerPack_AddStickerCount_other = getValueWithForm(dict, "StickerPack.AddStickerCount", .other) + self._ServiceMessage_GameScoreSelfExtended_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .zero) + self._ServiceMessage_GameScoreSelfExtended_one = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .one) + self._ServiceMessage_GameScoreSelfExtended_two = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .two) + self._ServiceMessage_GameScoreSelfExtended_few = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .few) + self._ServiceMessage_GameScoreSelfExtended_many = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .many) + self._ServiceMessage_GameScoreSelfExtended_other = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .other) + self._Notification_GameScoreSelfExtended_zero = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .zero) + self._Notification_GameScoreSelfExtended_one = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .one) + self._Notification_GameScoreSelfExtended_two = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .two) + self._Notification_GameScoreSelfExtended_few = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .few) + self._Notification_GameScoreSelfExtended_many = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .many) + self._Notification_GameScoreSelfExtended_other = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .other) self._AttachmentMenu_SendItem_zero = getValueWithForm(dict, "AttachmentMenu.SendItem", .zero) self._AttachmentMenu_SendItem_one = getValueWithForm(dict, "AttachmentMenu.SendItem", .one) self._AttachmentMenu_SendItem_two = getValueWithForm(dict, "AttachmentMenu.SendItem", .two) self._AttachmentMenu_SendItem_few = getValueWithForm(dict, "AttachmentMenu.SendItem", .few) self._AttachmentMenu_SendItem_many = getValueWithForm(dict, "AttachmentMenu.SendItem", .many) self._AttachmentMenu_SendItem_other = getValueWithForm(dict, "AttachmentMenu.SendItem", .other) - self._StickerPack_RemoveMaskCount_zero = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .zero) - self._StickerPack_RemoveMaskCount_one = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .one) - self._StickerPack_RemoveMaskCount_two = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .two) - self._StickerPack_RemoveMaskCount_few = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .few) - self._StickerPack_RemoveMaskCount_many = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .many) - self._StickerPack_RemoveMaskCount_other = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .other) + self._MessageTimer_ShortHours_zero = getValueWithForm(dict, "MessageTimer.ShortHours", .zero) + self._MessageTimer_ShortHours_one = getValueWithForm(dict, "MessageTimer.ShortHours", .one) + self._MessageTimer_ShortHours_two = getValueWithForm(dict, "MessageTimer.ShortHours", .two) + self._MessageTimer_ShortHours_few = getValueWithForm(dict, "MessageTimer.ShortHours", .few) + self._MessageTimer_ShortHours_many = getValueWithForm(dict, "MessageTimer.ShortHours", .many) + self._MessageTimer_ShortHours_other = getValueWithForm(dict, "MessageTimer.ShortHours", .other) self._ForwardedContacts_zero = getValueWithForm(dict, "ForwardedContacts", .zero) self._ForwardedContacts_one = getValueWithForm(dict, "ForwardedContacts", .one) self._ForwardedContacts_two = getValueWithForm(dict, "ForwardedContacts", .two) self._ForwardedContacts_few = getValueWithForm(dict, "ForwardedContacts", .few) self._ForwardedContacts_many = getValueWithForm(dict, "ForwardedContacts", .many) self._ForwardedContacts_other = getValueWithForm(dict, "ForwardedContacts", .other) - self._Conversation_StatusOnline_zero = getValueWithForm(dict, "Conversation.StatusOnline", .zero) - self._Conversation_StatusOnline_one = getValueWithForm(dict, "Conversation.StatusOnline", .one) - self._Conversation_StatusOnline_two = getValueWithForm(dict, "Conversation.StatusOnline", .two) - self._Conversation_StatusOnline_few = getValueWithForm(dict, "Conversation.StatusOnline", .few) - self._Conversation_StatusOnline_many = getValueWithForm(dict, "Conversation.StatusOnline", .many) - self._Conversation_StatusOnline_other = getValueWithForm(dict, "Conversation.StatusOnline", .other) - self._Call_ShortSeconds_zero = getValueWithForm(dict, "Call.ShortSeconds", .zero) - self._Call_ShortSeconds_one = getValueWithForm(dict, "Call.ShortSeconds", .one) - self._Call_ShortSeconds_two = getValueWithForm(dict, "Call.ShortSeconds", .two) - self._Call_ShortSeconds_few = getValueWithForm(dict, "Call.ShortSeconds", .few) - self._Call_ShortSeconds_many = getValueWithForm(dict, "Call.ShortSeconds", .many) - self._Call_ShortSeconds_other = getValueWithForm(dict, "Call.ShortSeconds", .other) - self._ForwardedLocations_zero = getValueWithForm(dict, "ForwardedLocations", .zero) - self._ForwardedLocations_one = getValueWithForm(dict, "ForwardedLocations", .one) - self._ForwardedLocations_two = getValueWithForm(dict, "ForwardedLocations", .two) - self._ForwardedLocations_few = getValueWithForm(dict, "ForwardedLocations", .few) - self._ForwardedLocations_many = getValueWithForm(dict, "ForwardedLocations", .many) - self._ForwardedLocations_other = getValueWithForm(dict, "ForwardedLocations", .other) - self._SharedMedia_DeleteItemsConfirmation_zero = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .zero) - self._SharedMedia_DeleteItemsConfirmation_one = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .one) - self._SharedMedia_DeleteItemsConfirmation_two = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .two) - self._SharedMedia_DeleteItemsConfirmation_few = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .few) - self._SharedMedia_DeleteItemsConfirmation_many = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .many) - self._SharedMedia_DeleteItemsConfirmation_other = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .other) - self._ForwardedMessages_zero = getValueWithForm(dict, "ForwardedMessages", .zero) - self._ForwardedMessages_one = getValueWithForm(dict, "ForwardedMessages", .one) - self._ForwardedMessages_two = getValueWithForm(dict, "ForwardedMessages", .two) - self._ForwardedMessages_few = getValueWithForm(dict, "ForwardedMessages", .few) - self._ForwardedMessages_many = getValueWithForm(dict, "ForwardedMessages", .many) - self._ForwardedMessages_other = getValueWithForm(dict, "ForwardedMessages", .other) - self._ForwardedPhotos_zero = getValueWithForm(dict, "ForwardedPhotos", .zero) - self._ForwardedPhotos_one = getValueWithForm(dict, "ForwardedPhotos", .one) - self._ForwardedPhotos_two = getValueWithForm(dict, "ForwardedPhotos", .two) - self._ForwardedPhotos_few = getValueWithForm(dict, "ForwardedPhotos", .few) - self._ForwardedPhotos_many = getValueWithForm(dict, "ForwardedPhotos", .many) - self._ForwardedPhotos_other = getValueWithForm(dict, "ForwardedPhotos", .other) - self._AttachmentMenu_SendVideo_zero = getValueWithForm(dict, "AttachmentMenu.SendVideo", .zero) - self._AttachmentMenu_SendVideo_one = getValueWithForm(dict, "AttachmentMenu.SendVideo", .one) - self._AttachmentMenu_SendVideo_two = getValueWithForm(dict, "AttachmentMenu.SendVideo", .two) - self._AttachmentMenu_SendVideo_few = getValueWithForm(dict, "AttachmentMenu.SendVideo", .few) - self._AttachmentMenu_SendVideo_many = getValueWithForm(dict, "AttachmentMenu.SendVideo", .many) - self._AttachmentMenu_SendVideo_other = getValueWithForm(dict, "AttachmentMenu.SendVideo", .other) - self._AttachmentMenu_SendPhoto_zero = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .zero) - self._AttachmentMenu_SendPhoto_one = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .one) - self._AttachmentMenu_SendPhoto_two = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .two) - self._AttachmentMenu_SendPhoto_few = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .few) - self._AttachmentMenu_SendPhoto_many = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .many) - self._AttachmentMenu_SendPhoto_other = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .other) self._SharedMedia_Generic_zero = getValueWithForm(dict, "SharedMedia.Generic", .zero) self._SharedMedia_Generic_one = getValueWithForm(dict, "SharedMedia.Generic", .one) self._SharedMedia_Generic_two = getValueWithForm(dict, "SharedMedia.Generic", .two) self._SharedMedia_Generic_few = getValueWithForm(dict, "SharedMedia.Generic", .few) self._SharedMedia_Generic_many = getValueWithForm(dict, "SharedMedia.Generic", .many) self._SharedMedia_Generic_other = getValueWithForm(dict, "SharedMedia.Generic", .other) + self._LiveLocationUpdated_MinutesAgo_zero = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .zero) + self._LiveLocationUpdated_MinutesAgo_one = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .one) + self._LiveLocationUpdated_MinutesAgo_two = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .two) + self._LiveLocationUpdated_MinutesAgo_few = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .few) + self._LiveLocationUpdated_MinutesAgo_many = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .many) + self._LiveLocationUpdated_MinutesAgo_other = getValueWithForm(dict, "LiveLocationUpdated.MinutesAgo", .other) + self._Notification_GameScoreSelfSimple_zero = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .zero) + self._Notification_GameScoreSelfSimple_one = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .one) + self._Notification_GameScoreSelfSimple_two = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .two) + self._Notification_GameScoreSelfSimple_few = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .few) + self._Notification_GameScoreSelfSimple_many = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .many) + self._Notification_GameScoreSelfSimple_other = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .other) + self._ServiceMessage_GameScoreSimple_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .zero) + self._ServiceMessage_GameScoreSimple_one = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .one) + self._ServiceMessage_GameScoreSimple_two = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .two) + self._ServiceMessage_GameScoreSimple_few = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .few) + self._ServiceMessage_GameScoreSimple_many = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .many) + self._ServiceMessage_GameScoreSimple_other = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .other) + self._QuickSend_Photos_zero = getValueWithForm(dict, "QuickSend.Photos", .zero) + self._QuickSend_Photos_one = getValueWithForm(dict, "QuickSend.Photos", .one) + self._QuickSend_Photos_two = getValueWithForm(dict, "QuickSend.Photos", .two) + self._QuickSend_Photos_few = getValueWithForm(dict, "QuickSend.Photos", .few) + self._QuickSend_Photos_many = getValueWithForm(dict, "QuickSend.Photos", .many) + self._QuickSend_Photos_other = getValueWithForm(dict, "QuickSend.Photos", .other) + self._Media_SharePhoto_zero = getValueWithForm(dict, "Media.SharePhoto", .zero) + self._Media_SharePhoto_one = getValueWithForm(dict, "Media.SharePhoto", .one) + self._Media_SharePhoto_two = getValueWithForm(dict, "Media.SharePhoto", .two) + self._Media_SharePhoto_few = getValueWithForm(dict, "Media.SharePhoto", .few) + self._Media_SharePhoto_many = getValueWithForm(dict, "Media.SharePhoto", .many) + self._Media_SharePhoto_other = getValueWithForm(dict, "Media.SharePhoto", .other) self._MuteExpires_Minutes_zero = getValueWithForm(dict, "MuteExpires.Minutes", .zero) self._MuteExpires_Minutes_one = getValueWithForm(dict, "MuteExpires.Minutes", .one) self._MuteExpires_Minutes_two = getValueWithForm(dict, "MuteExpires.Minutes", .two) self._MuteExpires_Minutes_few = getValueWithForm(dict, "MuteExpires.Minutes", .few) self._MuteExpires_Minutes_many = getValueWithForm(dict, "MuteExpires.Minutes", .many) self._MuteExpires_Minutes_other = getValueWithForm(dict, "MuteExpires.Minutes", .other) - self._Map_ETAMinutes_zero = getValueWithForm(dict, "Map.ETAMinutes", .zero) - self._Map_ETAMinutes_one = getValueWithForm(dict, "Map.ETAMinutes", .one) - self._Map_ETAMinutes_two = getValueWithForm(dict, "Map.ETAMinutes", .two) - self._Map_ETAMinutes_few = getValueWithForm(dict, "Map.ETAMinutes", .few) - self._Map_ETAMinutes_many = getValueWithForm(dict, "Map.ETAMinutes", .many) - self._Map_ETAMinutes_other = getValueWithForm(dict, "Map.ETAMinutes", .other) - self._Conversation_StatusSubscribers_zero = getValueWithForm(dict, "Conversation.StatusSubscribers", .zero) - self._Conversation_StatusSubscribers_one = getValueWithForm(dict, "Conversation.StatusSubscribers", .one) - self._Conversation_StatusSubscribers_two = getValueWithForm(dict, "Conversation.StatusSubscribers", .two) - self._Conversation_StatusSubscribers_few = getValueWithForm(dict, "Conversation.StatusSubscribers", .few) - self._Conversation_StatusSubscribers_many = getValueWithForm(dict, "Conversation.StatusSubscribers", .many) - self._Conversation_StatusSubscribers_other = getValueWithForm(dict, "Conversation.StatusSubscribers", .other) + self._Conversation_LiveLocationMembersCount_zero = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .zero) + self._Conversation_LiveLocationMembersCount_one = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .one) + self._Conversation_LiveLocationMembersCount_two = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .two) + self._Conversation_LiveLocationMembersCount_few = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .few) + self._Conversation_LiveLocationMembersCount_many = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .many) + self._Conversation_LiveLocationMembersCount_other = getValueWithForm(dict, "Conversation.LiveLocationMembersCount", .other) + self._MessageTimer_Days_zero = getValueWithForm(dict, "MessageTimer.Days", .zero) + self._MessageTimer_Days_one = getValueWithForm(dict, "MessageTimer.Days", .one) + self._MessageTimer_Days_two = getValueWithForm(dict, "MessageTimer.Days", .two) + self._MessageTimer_Days_few = getValueWithForm(dict, "MessageTimer.Days", .few) + self._MessageTimer_Days_many = getValueWithForm(dict, "MessageTimer.Days", .many) + self._MessageTimer_Days_other = getValueWithForm(dict, "MessageTimer.Days", .other) + self._MessageTimer_ShortSeconds_zero = getValueWithForm(dict, "MessageTimer.ShortSeconds", .zero) + self._MessageTimer_ShortSeconds_one = getValueWithForm(dict, "MessageTimer.ShortSeconds", .one) + self._MessageTimer_ShortSeconds_two = getValueWithForm(dict, "MessageTimer.ShortSeconds", .two) + self._MessageTimer_ShortSeconds_few = getValueWithForm(dict, "MessageTimer.ShortSeconds", .few) + self._MessageTimer_ShortSeconds_many = getValueWithForm(dict, "MessageTimer.ShortSeconds", .many) + self._MessageTimer_ShortSeconds_other = getValueWithForm(dict, "MessageTimer.ShortSeconds", .other) + self._Conversation_StatusOnline_zero = getValueWithForm(dict, "Conversation.StatusOnline", .zero) + self._Conversation_StatusOnline_one = getValueWithForm(dict, "Conversation.StatusOnline", .one) + self._Conversation_StatusOnline_two = getValueWithForm(dict, "Conversation.StatusOnline", .two) + self._Conversation_StatusOnline_few = getValueWithForm(dict, "Conversation.StatusOnline", .few) + self._Conversation_StatusOnline_many = getValueWithForm(dict, "Conversation.StatusOnline", .many) + self._Conversation_StatusOnline_other = getValueWithForm(dict, "Conversation.StatusOnline", .other) + self._ForwardedMessages_zero = getValueWithForm(dict, "ForwardedMessages", .zero) + self._ForwardedMessages_one = getValueWithForm(dict, "ForwardedMessages", .one) + self._ForwardedMessages_two = getValueWithForm(dict, "ForwardedMessages", .two) + self._ForwardedMessages_few = getValueWithForm(dict, "ForwardedMessages", .few) + self._ForwardedMessages_many = getValueWithForm(dict, "ForwardedMessages", .many) + self._ForwardedMessages_other = getValueWithForm(dict, "ForwardedMessages", .other) + self._MessageTimer_Seconds_zero = getValueWithForm(dict, "MessageTimer.Seconds", .zero) + self._MessageTimer_Seconds_one = getValueWithForm(dict, "MessageTimer.Seconds", .one) + self._MessageTimer_Seconds_two = getValueWithForm(dict, "MessageTimer.Seconds", .two) + self._MessageTimer_Seconds_few = getValueWithForm(dict, "MessageTimer.Seconds", .few) + self._MessageTimer_Seconds_many = getValueWithForm(dict, "MessageTimer.Seconds", .many) + self._MessageTimer_Seconds_other = getValueWithForm(dict, "MessageTimer.Seconds", .other) + self._SharedMedia_Link_zero = getValueWithForm(dict, "SharedMedia.Link", .zero) + self._SharedMedia_Link_one = getValueWithForm(dict, "SharedMedia.Link", .one) + self._SharedMedia_Link_two = getValueWithForm(dict, "SharedMedia.Link", .two) + self._SharedMedia_Link_few = getValueWithForm(dict, "SharedMedia.Link", .few) + self._SharedMedia_Link_many = getValueWithForm(dict, "SharedMedia.Link", .many) + self._SharedMedia_Link_other = getValueWithForm(dict, "SharedMedia.Link", .other) + self._Invitation_Members_zero = getValueWithForm(dict, "Invitation.Members", .zero) + self._Invitation_Members_one = getValueWithForm(dict, "Invitation.Members", .one) + self._Invitation_Members_two = getValueWithForm(dict, "Invitation.Members", .two) + self._Invitation_Members_few = getValueWithForm(dict, "Invitation.Members", .few) + self._Invitation_Members_many = getValueWithForm(dict, "Invitation.Members", .many) + self._Invitation_Members_other = getValueWithForm(dict, "Invitation.Members", .other) + self._Contacts_ImportersCount_zero = getValueWithForm(dict, "Contacts.ImportersCount", .zero) + self._Contacts_ImportersCount_one = getValueWithForm(dict, "Contacts.ImportersCount", .one) + self._Contacts_ImportersCount_two = getValueWithForm(dict, "Contacts.ImportersCount", .two) + self._Contacts_ImportersCount_few = getValueWithForm(dict, "Contacts.ImportersCount", .few) + self._Contacts_ImportersCount_many = getValueWithForm(dict, "Contacts.ImportersCount", .many) + self._Contacts_ImportersCount_other = getValueWithForm(dict, "Contacts.ImportersCount", .other) + self._ForwardedFiles_zero = getValueWithForm(dict, "ForwardedFiles", .zero) + self._ForwardedFiles_one = getValueWithForm(dict, "ForwardedFiles", .one) + self._ForwardedFiles_two = getValueWithForm(dict, "ForwardedFiles", .two) + self._ForwardedFiles_few = getValueWithForm(dict, "ForwardedFiles", .few) + self._ForwardedFiles_many = getValueWithForm(dict, "ForwardedFiles", .many) + self._ForwardedFiles_other = getValueWithForm(dict, "ForwardedFiles", .other) + self._Watch_UserInfo_Mute_zero = getValueWithForm(dict, "Watch.UserInfo.Mute", .zero) + self._Watch_UserInfo_Mute_one = getValueWithForm(dict, "Watch.UserInfo.Mute", .one) + self._Watch_UserInfo_Mute_two = getValueWithForm(dict, "Watch.UserInfo.Mute", .two) + self._Watch_UserInfo_Mute_few = getValueWithForm(dict, "Watch.UserInfo.Mute", .few) + self._Watch_UserInfo_Mute_many = getValueWithForm(dict, "Watch.UserInfo.Mute", .many) + self._Watch_UserInfo_Mute_other = getValueWithForm(dict, "Watch.UserInfo.Mute", .other) + self._Watch_LastSeen_HoursAgo_zero = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .zero) + self._Watch_LastSeen_HoursAgo_one = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .one) + self._Watch_LastSeen_HoursAgo_two = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .two) + self._Watch_LastSeen_HoursAgo_few = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .few) + self._Watch_LastSeen_HoursAgo_many = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .many) + self._Watch_LastSeen_HoursAgo_other = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .other) + self._SharedMedia_Video_zero = getValueWithForm(dict, "SharedMedia.Video", .zero) + self._SharedMedia_Video_one = getValueWithForm(dict, "SharedMedia.Video", .one) + self._SharedMedia_Video_two = getValueWithForm(dict, "SharedMedia.Video", .two) + self._SharedMedia_Video_few = getValueWithForm(dict, "SharedMedia.Video", .few) + self._SharedMedia_Video_many = getValueWithForm(dict, "SharedMedia.Video", .many) + self._SharedMedia_Video_other = getValueWithForm(dict, "SharedMedia.Video", .other) + self._AttachmentMenu_SendVideo_zero = getValueWithForm(dict, "AttachmentMenu.SendVideo", .zero) + self._AttachmentMenu_SendVideo_one = getValueWithForm(dict, "AttachmentMenu.SendVideo", .one) + self._AttachmentMenu_SendVideo_two = getValueWithForm(dict, "AttachmentMenu.SendVideo", .two) + self._AttachmentMenu_SendVideo_few = getValueWithForm(dict, "AttachmentMenu.SendVideo", .few) + self._AttachmentMenu_SendVideo_many = getValueWithForm(dict, "AttachmentMenu.SendVideo", .many) + self._AttachmentMenu_SendVideo_other = getValueWithForm(dict, "AttachmentMenu.SendVideo", .other) + self._ForwardedStickers_zero = getValueWithForm(dict, "ForwardedStickers", .zero) + self._ForwardedStickers_one = getValueWithForm(dict, "ForwardedStickers", .one) + self._ForwardedStickers_two = getValueWithForm(dict, "ForwardedStickers", .two) + self._ForwardedStickers_few = getValueWithForm(dict, "ForwardedStickers", .few) + self._ForwardedStickers_many = getValueWithForm(dict, "ForwardedStickers", .many) + self._ForwardedStickers_other = getValueWithForm(dict, "ForwardedStickers", .other) + self._MessageTimer_Years_zero = getValueWithForm(dict, "MessageTimer.Years", .zero) + self._MessageTimer_Years_one = getValueWithForm(dict, "MessageTimer.Years", .one) + self._MessageTimer_Years_two = getValueWithForm(dict, "MessageTimer.Years", .two) + self._MessageTimer_Years_few = getValueWithForm(dict, "MessageTimer.Years", .few) + self._MessageTimer_Years_many = getValueWithForm(dict, "MessageTimer.Years", .many) + self._MessageTimer_Years_other = getValueWithForm(dict, "MessageTimer.Years", .other) + self._SharedMedia_DeleteItemsConfirmation_zero = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .zero) + self._SharedMedia_DeleteItemsConfirmation_one = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .one) + self._SharedMedia_DeleteItemsConfirmation_two = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .two) + self._SharedMedia_DeleteItemsConfirmation_few = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .few) + self._SharedMedia_DeleteItemsConfirmation_many = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .many) + self._SharedMedia_DeleteItemsConfirmation_other = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .other) + self._MessageTimer_ShortMinutes_zero = getValueWithForm(dict, "MessageTimer.ShortMinutes", .zero) + self._MessageTimer_ShortMinutes_one = getValueWithForm(dict, "MessageTimer.ShortMinutes", .one) + self._MessageTimer_ShortMinutes_two = getValueWithForm(dict, "MessageTimer.ShortMinutes", .two) + self._MessageTimer_ShortMinutes_few = getValueWithForm(dict, "MessageTimer.ShortMinutes", .few) + self._MessageTimer_ShortMinutes_many = getValueWithForm(dict, "MessageTimer.ShortMinutes", .many) + self._MessageTimer_ShortMinutes_other = getValueWithForm(dict, "MessageTimer.ShortMinutes", .other) + self._AttachmentMenu_SendPhoto_zero = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .zero) + self._AttachmentMenu_SendPhoto_one = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .one) + self._AttachmentMenu_SendPhoto_two = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .two) + self._AttachmentMenu_SendPhoto_few = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .few) + self._AttachmentMenu_SendPhoto_many = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .many) + self._AttachmentMenu_SendPhoto_other = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .other) + self._GroupInfo_ParticipantCount_zero = getValueWithForm(dict, "GroupInfo.ParticipantCount", .zero) + self._GroupInfo_ParticipantCount_one = getValueWithForm(dict, "GroupInfo.ParticipantCount", .one) + self._GroupInfo_ParticipantCount_two = getValueWithForm(dict, "GroupInfo.ParticipantCount", .two) + self._GroupInfo_ParticipantCount_few = getValueWithForm(dict, "GroupInfo.ParticipantCount", .few) + self._GroupInfo_ParticipantCount_many = getValueWithForm(dict, "GroupInfo.ParticipantCount", .many) + self._GroupInfo_ParticipantCount_other = getValueWithForm(dict, "GroupInfo.ParticipantCount", .other) + self._Call_ShortSeconds_zero = getValueWithForm(dict, "Call.ShortSeconds", .zero) + self._Call_ShortSeconds_one = getValueWithForm(dict, "Call.ShortSeconds", .one) + self._Call_ShortSeconds_two = getValueWithForm(dict, "Call.ShortSeconds", .two) + self._Call_ShortSeconds_few = getValueWithForm(dict, "Call.ShortSeconds", .few) + self._Call_ShortSeconds_many = getValueWithForm(dict, "Call.ShortSeconds", .many) + self._Call_ShortSeconds_other = getValueWithForm(dict, "Call.ShortSeconds", .other) + self._Notification_GameScoreSimple_zero = getValueWithForm(dict, "Notification.GameScoreSimple", .zero) + self._Notification_GameScoreSimple_one = getValueWithForm(dict, "Notification.GameScoreSimple", .one) + self._Notification_GameScoreSimple_two = getValueWithForm(dict, "Notification.GameScoreSimple", .two) + self._Notification_GameScoreSimple_few = getValueWithForm(dict, "Notification.GameScoreSimple", .few) + self._Notification_GameScoreSimple_many = getValueWithForm(dict, "Notification.GameScoreSimple", .many) + self._Notification_GameScoreSimple_other = getValueWithForm(dict, "Notification.GameScoreSimple", .other) + self._StickerPack_StickerCount_zero = getValueWithForm(dict, "StickerPack.StickerCount", .zero) + self._StickerPack_StickerCount_one = getValueWithForm(dict, "StickerPack.StickerCount", .one) + self._StickerPack_StickerCount_two = getValueWithForm(dict, "StickerPack.StickerCount", .two) + self._StickerPack_StickerCount_few = getValueWithForm(dict, "StickerPack.StickerCount", .few) + self._StickerPack_StickerCount_many = getValueWithForm(dict, "StickerPack.StickerCount", .many) + self._StickerPack_StickerCount_other = getValueWithForm(dict, "StickerPack.StickerCount", .other) + self._StickerPack_RemoveMaskCount_zero = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .zero) + self._StickerPack_RemoveMaskCount_one = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .one) + self._StickerPack_RemoveMaskCount_two = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .two) + self._StickerPack_RemoveMaskCount_few = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .few) + self._StickerPack_RemoveMaskCount_many = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .many) + self._StickerPack_RemoveMaskCount_other = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .other) + self._UserCount_zero = getValueWithForm(dict, "UserCount", .zero) + self._UserCount_one = getValueWithForm(dict, "UserCount", .one) + self._UserCount_two = getValueWithForm(dict, "UserCount", .two) + self._UserCount_few = getValueWithForm(dict, "UserCount", .few) + self._UserCount_many = getValueWithForm(dict, "UserCount", .many) + self._UserCount_other = getValueWithForm(dict, "UserCount", .other) + self._SharedMedia_File_zero = getValueWithForm(dict, "SharedMedia.File", .zero) + self._SharedMedia_File_one = getValueWithForm(dict, "SharedMedia.File", .one) + self._SharedMedia_File_two = getValueWithForm(dict, "SharedMedia.File", .two) + self._SharedMedia_File_few = getValueWithForm(dict, "SharedMedia.File", .few) + self._SharedMedia_File_many = getValueWithForm(dict, "SharedMedia.File", .many) + self._SharedMedia_File_other = getValueWithForm(dict, "SharedMedia.File", .other) + self._Call_ShortMinutes_zero = getValueWithForm(dict, "Call.ShortMinutes", .zero) + self._Call_ShortMinutes_one = getValueWithForm(dict, "Call.ShortMinutes", .one) + self._Call_ShortMinutes_two = getValueWithForm(dict, "Call.ShortMinutes", .two) + self._Call_ShortMinutes_few = getValueWithForm(dict, "Call.ShortMinutes", .few) + self._Call_ShortMinutes_many = getValueWithForm(dict, "Call.ShortMinutes", .many) + self._Call_ShortMinutes_other = getValueWithForm(dict, "Call.ShortMinutes", .other) + self._MuteExpires_Days_zero = getValueWithForm(dict, "MuteExpires.Days", .zero) + self._MuteExpires_Days_one = getValueWithForm(dict, "MuteExpires.Days", .one) + self._MuteExpires_Days_two = getValueWithForm(dict, "MuteExpires.Days", .two) + self._MuteExpires_Days_few = getValueWithForm(dict, "MuteExpires.Days", .few) + self._MuteExpires_Days_many = getValueWithForm(dict, "MuteExpires.Days", .many) + self._MuteExpires_Days_other = getValueWithForm(dict, "MuteExpires.Days", .other) + self._Watch_LastSeen_MinutesAgo_zero = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .zero) + self._Watch_LastSeen_MinutesAgo_one = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .one) + self._Watch_LastSeen_MinutesAgo_two = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .two) + self._Watch_LastSeen_MinutesAgo_few = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .few) + self._Watch_LastSeen_MinutesAgo_many = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .many) + self._Watch_LastSeen_MinutesAgo_other = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .other) + self._LastSeen_MinutesAgo_zero = getValueWithForm(dict, "LastSeen.MinutesAgo", .zero) + self._LastSeen_MinutesAgo_one = getValueWithForm(dict, "LastSeen.MinutesAgo", .one) + self._LastSeen_MinutesAgo_two = getValueWithForm(dict, "LastSeen.MinutesAgo", .two) + self._LastSeen_MinutesAgo_few = getValueWithForm(dict, "LastSeen.MinutesAgo", .few) + self._LastSeen_MinutesAgo_many = getValueWithForm(dict, "LastSeen.MinutesAgo", .many) + self._LastSeen_MinutesAgo_other = getValueWithForm(dict, "LastSeen.MinutesAgo", .other) self._ForwardedVideos_zero = getValueWithForm(dict, "ForwardedVideos", .zero) self._ForwardedVideos_one = getValueWithForm(dict, "ForwardedVideos", .one) self._ForwardedVideos_two = getValueWithForm(dict, "ForwardedVideos", .two) self._ForwardedVideos_few = getValueWithForm(dict, "ForwardedVideos", .few) self._ForwardedVideos_many = getValueWithForm(dict, "ForwardedVideos", .many) self._ForwardedVideos_other = getValueWithForm(dict, "ForwardedVideos", .other) + self._DialogList_LiveLocationChatsCount_zero = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .zero) + self._DialogList_LiveLocationChatsCount_one = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .one) + self._DialogList_LiveLocationChatsCount_two = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .two) + self._DialogList_LiveLocationChatsCount_few = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .few) + self._DialogList_LiveLocationChatsCount_many = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .many) + self._DialogList_LiveLocationChatsCount_other = getValueWithForm(dict, "DialogList.LiveLocationChatsCount", .other) + self._MessageTimer_Minutes_zero = getValueWithForm(dict, "MessageTimer.Minutes", .zero) + self._MessageTimer_Minutes_one = getValueWithForm(dict, "MessageTimer.Minutes", .one) + self._MessageTimer_Minutes_two = getValueWithForm(dict, "MessageTimer.Minutes", .two) + self._MessageTimer_Minutes_few = getValueWithForm(dict, "MessageTimer.Minutes", .few) + self._MessageTimer_Minutes_many = getValueWithForm(dict, "MessageTimer.Minutes", .many) + self._MessageTimer_Minutes_other = getValueWithForm(dict, "MessageTimer.Minutes", .other) + self._Map_ETAMinutes_zero = getValueWithForm(dict, "Map.ETAMinutes", .zero) + self._Map_ETAMinutes_one = getValueWithForm(dict, "Map.ETAMinutes", .one) + self._Map_ETAMinutes_two = getValueWithForm(dict, "Map.ETAMinutes", .two) + self._Map_ETAMinutes_few = getValueWithForm(dict, "Map.ETAMinutes", .few) + self._Map_ETAMinutes_many = getValueWithForm(dict, "Map.ETAMinutes", .many) + self._Map_ETAMinutes_other = getValueWithForm(dict, "Map.ETAMinutes", .other) + self._LastSeen_HoursAgo_zero = getValueWithForm(dict, "LastSeen.HoursAgo", .zero) + self._LastSeen_HoursAgo_one = getValueWithForm(dict, "LastSeen.HoursAgo", .one) + self._LastSeen_HoursAgo_two = getValueWithForm(dict, "LastSeen.HoursAgo", .two) + self._LastSeen_HoursAgo_few = getValueWithForm(dict, "LastSeen.HoursAgo", .few) + self._LastSeen_HoursAgo_many = getValueWithForm(dict, "LastSeen.HoursAgo", .many) + self._LastSeen_HoursAgo_other = getValueWithForm(dict, "LastSeen.HoursAgo", .other) + self._StickerPack_AddMaskCount_zero = getValueWithForm(dict, "StickerPack.AddMaskCount", .zero) + self._StickerPack_AddMaskCount_one = getValueWithForm(dict, "StickerPack.AddMaskCount", .one) + self._StickerPack_AddMaskCount_two = getValueWithForm(dict, "StickerPack.AddMaskCount", .two) + self._StickerPack_AddMaskCount_few = getValueWithForm(dict, "StickerPack.AddMaskCount", .few) + self._StickerPack_AddMaskCount_many = getValueWithForm(dict, "StickerPack.AddMaskCount", .many) + self._StickerPack_AddMaskCount_other = getValueWithForm(dict, "StickerPack.AddMaskCount", .other) self._MessageTimer_Weeks_zero = getValueWithForm(dict, "MessageTimer.Weeks", .zero) self._MessageTimer_Weeks_one = getValueWithForm(dict, "MessageTimer.Weeks", .one) self._MessageTimer_Weeks_two = getValueWithForm(dict, "MessageTimer.Weeks", .two) self._MessageTimer_Weeks_few = getValueWithForm(dict, "MessageTimer.Weeks", .few) self._MessageTimer_Weeks_many = getValueWithForm(dict, "MessageTimer.Weeks", .many) self._MessageTimer_Weeks_other = getValueWithForm(dict, "MessageTimer.Weeks", .other) + self._ForwardedAuthorsOthers_zero = getValueWithForm(dict, "ForwardedAuthorsOthers", .zero) + self._ForwardedAuthorsOthers_one = getValueWithForm(dict, "ForwardedAuthorsOthers", .one) + self._ForwardedAuthorsOthers_two = getValueWithForm(dict, "ForwardedAuthorsOthers", .two) + self._ForwardedAuthorsOthers_few = getValueWithForm(dict, "ForwardedAuthorsOthers", .few) + self._ForwardedAuthorsOthers_many = getValueWithForm(dict, "ForwardedAuthorsOthers", .many) + self._ForwardedAuthorsOthers_other = getValueWithForm(dict, "ForwardedAuthorsOthers", .other) + self._ServiceMessage_GameScoreExtended_zero = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .zero) + self._ServiceMessage_GameScoreExtended_one = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .one) + self._ServiceMessage_GameScoreExtended_two = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .two) + self._ServiceMessage_GameScoreExtended_few = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .few) + self._ServiceMessage_GameScoreExtended_many = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .many) + self._ServiceMessage_GameScoreExtended_other = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .other) self._MessageTimer_Hours_zero = getValueWithForm(dict, "MessageTimer.Hours", .zero) self._MessageTimer_Hours_one = getValueWithForm(dict, "MessageTimer.Hours", .one) self._MessageTimer_Hours_two = getValueWithForm(dict, "MessageTimer.Hours", .two) self._MessageTimer_Hours_few = getValueWithForm(dict, "MessageTimer.Hours", .few) self._MessageTimer_Hours_many = getValueWithForm(dict, "MessageTimer.Hours", .many) self._MessageTimer_Hours_other = getValueWithForm(dict, "MessageTimer.Hours", .other) + self._ForwardedLocations_zero = getValueWithForm(dict, "ForwardedLocations", .zero) + self._ForwardedLocations_one = getValueWithForm(dict, "ForwardedLocations", .one) + self._ForwardedLocations_two = getValueWithForm(dict, "ForwardedLocations", .two) + self._ForwardedLocations_few = getValueWithForm(dict, "ForwardedLocations", .few) + self._ForwardedLocations_many = getValueWithForm(dict, "ForwardedLocations", .many) + self._ForwardedLocations_other = getValueWithForm(dict, "ForwardedLocations", .other) + self._PrivacyLastSeenSettings_AddUsers_zero = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .zero) + self._PrivacyLastSeenSettings_AddUsers_one = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .one) + self._PrivacyLastSeenSettings_AddUsers_two = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .two) + self._PrivacyLastSeenSettings_AddUsers_few = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .few) + self._PrivacyLastSeenSettings_AddUsers_many = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .many) + self._PrivacyLastSeenSettings_AddUsers_other = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .other) + self._MuteFor_Hours_zero = getValueWithForm(dict, "MuteFor.Hours", .zero) + self._MuteFor_Hours_one = getValueWithForm(dict, "MuteFor.Hours", .one) + self._MuteFor_Hours_two = getValueWithForm(dict, "MuteFor.Hours", .two) + self._MuteFor_Hours_few = getValueWithForm(dict, "MuteFor.Hours", .few) + self._MuteFor_Hours_many = getValueWithForm(dict, "MuteFor.Hours", .many) + self._MuteFor_Hours_other = getValueWithForm(dict, "MuteFor.Hours", .other) + self._ForwardedPhotos_zero = getValueWithForm(dict, "ForwardedPhotos", .zero) + self._ForwardedPhotos_one = getValueWithForm(dict, "ForwardedPhotos", .one) + self._ForwardedPhotos_two = getValueWithForm(dict, "ForwardedPhotos", .two) + self._ForwardedPhotos_few = getValueWithForm(dict, "ForwardedPhotos", .few) + self._ForwardedPhotos_many = getValueWithForm(dict, "ForwardedPhotos", .many) + self._ForwardedPhotos_other = getValueWithForm(dict, "ForwardedPhotos", .other) + self._ServiceMessage_GameScoreSelfSimple_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .zero) + self._ServiceMessage_GameScoreSelfSimple_one = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .one) + self._ServiceMessage_GameScoreSelfSimple_two = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .two) + self._ServiceMessage_GameScoreSelfSimple_few = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .few) + self._ServiceMessage_GameScoreSelfSimple_many = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .many) + self._ServiceMessage_GameScoreSelfSimple_other = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .other) + self._Conversation_StatusSubscribers_zero = getValueWithForm(dict, "Conversation.StatusSubscribers", .zero) + self._Conversation_StatusSubscribers_one = getValueWithForm(dict, "Conversation.StatusSubscribers", .one) + self._Conversation_StatusSubscribers_two = getValueWithForm(dict, "Conversation.StatusSubscribers", .two) + self._Conversation_StatusSubscribers_few = getValueWithForm(dict, "Conversation.StatusSubscribers", .few) + self._Conversation_StatusSubscribers_many = getValueWithForm(dict, "Conversation.StatusSubscribers", .many) + self._Conversation_StatusSubscribers_other = getValueWithForm(dict, "Conversation.StatusSubscribers", .other) } } diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index 39f1ce5368..93e1d83e79 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -38,6 +38,15 @@ public enum PresentationThemeStatusBarStyle: Int32 { case black = 0 case white = 1 + init(_ style: StatusBarStyle) { + switch style { + case .White: + self = .white + default: + self = .black + } + } + var style: StatusBarStyle { switch self { case .black: diff --git a/TelegramUI/PrivacyAndSecurityController.swift b/TelegramUI/PrivacyAndSecurityController.swift index 8bd734e6c8..a27bce62d0 100644 --- a/TelegramUI/PrivacyAndSecurityController.swift +++ b/TelegramUI/PrivacyAndSecurityController.swift @@ -14,13 +14,9 @@ private final class PrivacyAndSecurityControllerArguments { let openTwoStepVerification: () -> Void let openActiveSessions: () -> Void let setupAccountAutoremove: () -> Void - let clearPaymentInfo: () -> Void - let updateSecretChatLinkPreviews: (Bool) -> Void - let deleteContacts: () -> Void - let updateSyncContacts: (Bool) -> Void - let updateSuggestFrequentContacts: (Bool) -> Void + let openDataSettings: () -> Void - init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping () -> Void, openActiveSessions: @escaping () -> Void, setupAccountAutoremove: @escaping () -> Void, clearPaymentInfo: @escaping () -> Void, updateSecretChatLinkPreviews: @escaping (Bool) -> Void, deleteContacts: @escaping () -> Void, updateSyncContacts: @escaping (Bool) -> Void, updateSuggestFrequentContacts: @escaping (Bool) -> Void) { + init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping () -> Void, openActiveSessions: @escaping () -> Void, setupAccountAutoremove: @escaping () -> Void, openDataSettings: @escaping () -> Void) { self.account = account self.openBlockedUsers = openBlockedUsers self.openLastSeenPrivacy = openLastSeenPrivacy @@ -30,11 +26,7 @@ private final class PrivacyAndSecurityControllerArguments { self.openTwoStepVerification = openTwoStepVerification self.openActiveSessions = openActiveSessions self.setupAccountAutoremove = setupAccountAutoremove - self.clearPaymentInfo = clearPaymentInfo - self.updateSecretChatLinkPreviews = updateSecretChatLinkPreviews - self.deleteContacts = deleteContacts - self.updateSyncContacts = updateSyncContacts - self.updateSuggestFrequentContacts = updateSuggestFrequentContacts + self.openDataSettings = openDataSettings } } @@ -42,18 +34,16 @@ private enum PrivacyAndSecuritySection: Int32 { case privacy case security case account - case payment - case secretChatLinkPreviews - case contacts - case frequentContacts + case dataSettings } private enum PrivacyAndSecurityEntry: ItemListNodeEntry { case privacyHeader(PresentationTheme, String) case blockedPeers(PresentationTheme, String) case lastSeenPrivacy(PresentationTheme, String, String) - case groupPrivacy(PresentationTheme, String, String) case voiceCallPrivacy(PresentationTheme, String, String) + case groupPrivacy(PresentationTheme, String, String) + case selectivePrivacyInfo(PresentationTheme, String) case securityHeader(PresentationTheme, String) case passcode(PresentationTheme, String) case twoStepVerification(PresentationTheme, String) @@ -61,36 +51,19 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { case accountHeader(PresentationTheme, String) case accountTimeout(PresentationTheme, String, String) case accountInfo(PresentationTheme, String) - case paymentHeader(PresentationTheme, String) - case clearPaymentInfo(PresentationTheme, String, Bool) - case paymentInfo(PresentationTheme, String) - case secretChatLinkPreviewsHeader(PresentationTheme, String) - case secretChatLinkPreviews(PresentationTheme, String, Bool) - case secretChatLinkPreviewsInfo(PresentationTheme, String) - case contactsHeader(PresentationTheme, String) - case deleteContacts(PresentationTheme, String, Bool) - case syncContacts(PresentationTheme, String, Bool) - case syncContactsInfo(PresentationTheme, String) - case frequentContactsHeader(PresentationTheme, String) - case frequentContacts(PresentationTheme, String, Bool) - case frequentContactsInfo(PresentationTheme, String) + case dataSettings(PresentationTheme, String) + case dataSettingsInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { - case .privacyHeader, .blockedPeers, .lastSeenPrivacy, .groupPrivacy, .voiceCallPrivacy: + case .privacyHeader, .blockedPeers, .lastSeenPrivacy, .groupPrivacy, .selectivePrivacyInfo, .voiceCallPrivacy: return PrivacyAndSecuritySection.privacy.rawValue case .securityHeader, .passcode, .twoStepVerification, .activeSessions: return PrivacyAndSecuritySection.security.rawValue case .accountHeader, .accountTimeout, .accountInfo: return PrivacyAndSecuritySection.account.rawValue - case .paymentHeader, .clearPaymentInfo, .paymentInfo: - return PrivacyAndSecuritySection.payment.rawValue - case .secretChatLinkPreviewsHeader, .secretChatLinkPreviews, .secretChatLinkPreviewsInfo: - return PrivacyAndSecuritySection.secretChatLinkPreviews.rawValue - case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo: - return PrivacyAndSecuritySection.contacts.rawValue - case .frequentContactsHeader, .frequentContacts, .frequentContactsInfo: - return PrivacyAndSecuritySection.frequentContacts.rawValue + case .dataSettings, .dataSettingsInfo: + return PrivacyAndSecuritySection.dataSettings.rawValue } } @@ -102,50 +75,30 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return 1 case .lastSeenPrivacy: return 2 - case .groupPrivacy: - return 3 case .voiceCallPrivacy: + return 3 + case .groupPrivacy: return 4 - case .securityHeader: + case .selectivePrivacyInfo: return 5 - case .passcode: + case .securityHeader: return 6 - case .twoStepVerification: + case .passcode: return 7 - case .activeSessions: + case .twoStepVerification: return 8 - case .accountHeader: + case .activeSessions: return 9 - case .accountTimeout: + case .accountHeader: return 10 - case .accountInfo: + case .accountTimeout: return 11 - case .paymentHeader: + case .accountInfo: return 12 - case .clearPaymentInfo: + case .dataSettings: return 13 - case .paymentInfo: + case .dataSettingsInfo: return 14 - case .secretChatLinkPreviewsHeader: - return 15 - case .secretChatLinkPreviews: - return 16 - case .secretChatLinkPreviewsInfo: - return 17 - case .contactsHeader: - return 18 - case .deleteContacts: - return 19 - case .syncContacts: - return 20 - case .syncContactsInfo: - return 21 - case .frequentContactsHeader: - return 22 - case .frequentContacts: - return 23 - case .frequentContactsInfo: - return 24 } } @@ -175,6 +128,12 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { } else { return false } + case let .selectivePrivacyInfo(lhsTheme, lhsText): + if case let .selectivePrivacyInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .voiceCallPrivacy(lhsTheme, lhsText, lhsValue): if case let .voiceCallPrivacy(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true @@ -223,80 +182,14 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { } else { return false } - case let .paymentHeader(lhsTheme, lhsText): - if case let .paymentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .dataSettings(lhsTheme, lhsText): + if case let .dataSettings(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .clearPaymentInfo(lhsTheme, lhsText, lhsEnabled): - if case let .clearPaymentInfo(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { - return true - } else { - return false - } - case let .paymentInfo(lhsTheme, lhsText): - if case let .paymentInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .secretChatLinkPreviewsHeader(lhsTheme, lhsText): - if case let .secretChatLinkPreviewsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .secretChatLinkPreviews(lhsTheme, lhsText, lhsEnabled): - if case let .secretChatLinkPreviews(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { - return true - } else { - return false - } - case let .secretChatLinkPreviewsInfo(lhsTheme, lhsText): - if case let .secretChatLinkPreviewsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .contactsHeader(lhsTheme, lhsText): - if case let .contactsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .deleteContacts(lhsTheme, lhsText, lhsEnabled): - if case let .deleteContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { - return true - } else { - return false - } - case let .syncContacts(lhsTheme, lhsText, lhsEnabled): - if case let .syncContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { - return true - } else { - return false - } - case let .syncContactsInfo(lhsTheme, lhsText): - if case let .syncContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .frequentContactsHeader(lhsTheme, lhsText): - if case let .frequentContactsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .frequentContacts(lhsTheme, lhsText, lhsEnabled): - if case let .frequentContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled { - return true - } else { - return false - } - case let .frequentContactsInfo(lhsTheme, lhsText): - if case let .frequentContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .dataSettingsInfo(lhsTheme, lhsText): + if case let .dataSettingsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -324,6 +217,8 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openGroupsPrivacy() }) + case let .selectivePrivacyInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) case let .voiceCallPrivacy(theme, text, value): return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openVoiceCallPrivacy() @@ -350,100 +245,19 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { }) case let .accountInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) - case let .paymentHeader(theme, text): + case let .dataSettings(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { + arguments.openDataSettings() + }) return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) - case let .clearPaymentInfo(theme, text, enabled): - return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { - arguments.clearPaymentInfo() - }) - case let .paymentInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) - case let .secretChatLinkPreviewsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) - case let .secretChatLinkPreviews(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in - arguments.updateSecretChatLinkPreviews(updatedValue) - }) - case let .secretChatLinkPreviewsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) - case let .contactsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) - case let .deleteContacts(theme, text, value): - return ItemListActionItem(theme: theme, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { - arguments.deleteContacts() - }) - case let .syncContacts(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in - arguments.updateSyncContacts(updatedValue) - }) - case let .syncContactsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) - case let .frequentContactsHeader(theme, text): - return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) - case let .frequentContacts(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: !value, sectionId: self.section, style: .blocks, updated: { updatedValue in - arguments.updateSuggestFrequentContacts(updatedValue) - }) - case let .frequentContactsInfo(theme, text): + case let .dataSettingsInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } private struct PrivacyAndSecurityControllerState: Equatable { - let updatingAccountTimeoutValue: Int32? - let clearingPaymentInfo: Bool - let clearedPaymentInfo: Bool - let deletingContacts: Bool - let updatedSuggestFrequentContacts: Bool? - - init(updatingAccountTimeoutValue: Int32? = nil, clearingPaymentInfo: Bool = false, clearedPaymentInfo: Bool = false, deletingContacts: Bool = false, updatedSuggestFrequentContacts: Bool? = nil) { - self.updatingAccountTimeoutValue = updatingAccountTimeoutValue - self.clearingPaymentInfo = clearingPaymentInfo - self.clearedPaymentInfo = clearedPaymentInfo - self.deletingContacts = deletingContacts - self.updatedSuggestFrequentContacts = updatedSuggestFrequentContacts - } - - static func ==(lhs: PrivacyAndSecurityControllerState, rhs: PrivacyAndSecurityControllerState) -> Bool { - if lhs.updatingAccountTimeoutValue != rhs.updatingAccountTimeoutValue { - return false - } - if lhs.clearingPaymentInfo != rhs.clearingPaymentInfo { - return false - } - if lhs.clearedPaymentInfo != rhs.clearedPaymentInfo { - return false - } - if lhs.deletingContacts != rhs.deletingContacts { - return false - } - if lhs.updatedSuggestFrequentContacts != rhs.updatedSuggestFrequentContacts { - return false - } - - return true - } - - func withUpdatedUpdatingAccountTimeoutValue(_ updatingAccountTimeoutValue: Int32?) -> PrivacyAndSecurityControllerState { - return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: updatingAccountTimeoutValue, clearingPaymentInfo: self.clearingPaymentInfo, clearedPaymentInfo: self.clearedPaymentInfo, deletingContacts: self.deletingContacts, updatedSuggestFrequentContacts: self.updatedSuggestFrequentContacts) - } - - func withUpdatedClearingPaymentInfo(_ clearingPaymentInfo: Bool) -> PrivacyAndSecurityControllerState { - return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: self.updatingAccountTimeoutValue, clearingPaymentInfo: clearingPaymentInfo, clearedPaymentInfo: self.clearedPaymentInfo, deletingContacts: self.deletingContacts, updatedSuggestFrequentContacts: self.updatedSuggestFrequentContacts) - } - - func withUpdatedClearedPaymentInfo(_ clearedPaymentInfo: Bool) -> PrivacyAndSecurityControllerState { - return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: self.updatingAccountTimeoutValue, clearingPaymentInfo: self.clearingPaymentInfo, clearedPaymentInfo: clearedPaymentInfo, deletingContacts: self.deletingContacts, updatedSuggestFrequentContacts: self.updatedSuggestFrequentContacts) - } - - func withUpdatedDeletingContacts(_ deletingContacts: Bool) -> PrivacyAndSecurityControllerState { - return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: self.updatingAccountTimeoutValue, clearingPaymentInfo: self.clearingPaymentInfo, clearedPaymentInfo: self.clearedPaymentInfo, deletingContacts: deletingContacts, updatedSuggestFrequentContacts: self.updatedSuggestFrequentContacts) - } - - func withUpdatedUpdatedSuggestFrequentContacts(_ updatedSuggestFrequentContacts: Bool?) -> PrivacyAndSecurityControllerState { - return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: self.updatingAccountTimeoutValue, clearingPaymentInfo: self.clearingPaymentInfo, clearedPaymentInfo: self.clearedPaymentInfo, deletingContacts: self.deletingContacts, updatedSuggestFrequentContacts: updatedSuggestFrequentContacts) - } + var updatingAccountTimeoutValue: Int32? = nil } private func stringForSelectiveSettings(strings: PresentationStrings, settings: SelectivePrivacySettings) -> String { @@ -473,19 +287,22 @@ private func stringForSelectiveSettings(strings: PresentationStrings, settings: } } -private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool) -> [PrivacyAndSecurityEntry] { +private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?) -> [PrivacyAndSecurityEntry] { var entries: [PrivacyAndSecurityEntry] = [] entries.append(.privacyHeader(presentationData.theme, presentationData.strings.PrivacySettings_PrivacyTitle)) entries.append(.blockedPeers(presentationData.theme, presentationData.strings.Settings_BlockedUsers)) if let privacySettings = privacySettings { entries.append(.lastSeenPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_LastSeen, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.presence))) - entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.groupInvitations))) entries.append(.voiceCallPrivacy(presentationData.theme, presentationData.strings.Privacy_Calls, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.voiceCalls))) + entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.groupInvitations))) + + entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp)) } else { entries.append(.lastSeenPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_LastSeen, presentationData.strings.Channel_NotificationLoading)) - entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, presentationData.strings.Channel_NotificationLoading)) entries.append(.voiceCallPrivacy(presentationData.theme, presentationData.strings.Privacy_Calls, presentationData.strings.Channel_NotificationLoading)) + entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, presentationData.strings.Channel_NotificationLoading)) + entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp)) } entries.append(.securityHeader(presentationData.theme, presentationData.strings.PrivacySettings_SecurityTitle)) @@ -515,26 +332,8 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD } entries.append(.accountInfo(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountHelp)) - entries.append(.paymentHeader(presentationData.theme, presentationData.strings.Privacy_PaymentsTitle)) - entries.append(.clearPaymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfo, !state.clearingPaymentInfo && !state.clearedPaymentInfo)) - if state.clearedPaymentInfo { - entries.append(.paymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfoDoneHelp)) - } else { - entries.append(.paymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfoHelp)) - } - - entries.append(.secretChatLinkPreviewsHeader(presentationData.theme, presentationData.strings.PrivacySettings_SecretChats)) - entries.append(.secretChatLinkPreviews(presentationData.theme, presentationData.strings.PrivacySettings_LinkPreviews, secretChatLinkPreviews ?? true)) - entries.append(.secretChatLinkPreviewsInfo(presentationData.theme, presentationData.strings.PrivacySettings_LinkPreviewsInfo)) - - entries.append(.contactsHeader(presentationData.theme, presentationData.strings.PrivacySettings_Contacts)) - entries.append(.deleteContacts(presentationData.theme, presentationData.strings.PrivacySettings_DeleteContacts, !state.deletingContacts)) - entries.append(.syncContacts(presentationData.theme, presentationData.strings.PrivacySettings_SyncContacts, synchronizeDeviceContacts)) - entries.append(.syncContactsInfo(presentationData.theme, presentationData.strings.PrivacySettings_SyncContactsInfo)) - - entries.append(.frequentContactsHeader(presentationData.theme, presentationData.strings.PrivacySettings_FrequentContacts)) - entries.append(.frequentContacts(presentationData.theme, presentationData.strings.PrivacySettings_SuggestFrequentContacts, frequentContacts)) - entries.append(.frequentContactsInfo(presentationData.theme, presentationData.strings.PrivacySettings_SuggestFrequentContactsInfo)) + entries.append(.dataSettings(presentationData.theme, presentationData.strings.PrivacySettings_DataSettings)) + entries.append(.dataSettingsInfo(presentationData.theme, presentationData.strings.PrivacySettings_DataSettingsHelp)) return entries } @@ -558,9 +357,6 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign let updateAccountTimeoutDisposable = MetaDisposable() actionsDisposable.add(updateAccountTimeoutDisposable) - let clearPaymentInfoDisposable = MetaDisposable() - actionsDisposable.add(clearPaymentInfoDisposable) - let privacySettingsPromise = Promise() privacySettingsPromise.set(initialSettings) @@ -664,8 +460,10 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign } let timeoutAction: (Int32) -> Void = { timeout in if let updateAccountTimeoutDisposable = updateAccountTimeoutDisposable { - updateState { - return $0.withUpdatedUpdatingAccountTimeoutValue(timeout) + updateState { state in + var state = state + state.updatingAccountTimeoutValue = timeout + return state } let applyTimeout: Signal = privacySettingsPromise.get() |> filter { $0 != nil } @@ -680,8 +478,10 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign updateAccountTimeoutDisposable.set((updateAccountRemovalTimeout(account: account, timeout: timeout) |> then(applyTimeout) |> deliverOnMainQueue).start(completed: { - updateState { - return $0.withUpdatedUpdatingAccountTimeoutValue(nil) + updateState { state in + var state = state + state.updatingAccountTimeoutValue = nil + return state } })) } @@ -705,104 +505,8 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign presentControllerImpl?(controller) } })) - }, clearPaymentInfo: { - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationTheme: presentationData.theme) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Privacy_PaymentsClearInfo, color: .destructive, action: { - var clear = false - updateState { current in - if !current.clearingPaymentInfo && !current.clearedPaymentInfo { - clear = true - return current.withUpdatedClearingPaymentInfo(true) - } else { - return current - } - } - if clear { - clearPaymentInfoDisposable.set((clearBotPaymentInfo(network: account.network) - |> deliverOnMainQueue).start(completed: { - updateState { current in - return current.withUpdatedClearingPaymentInfo(false).withUpdatedClearedPaymentInfo(true) - } - })) - } - dismissAction() - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller) - }, updateSecretChatLinkPreviews: { value in - let _ = ApplicationSpecificNotice.setSecretChatLinkPreviews(postbox: account.postbox, value: value).start() - }, deleteContacts: { - var canBegin = false - updateState { state in - if !state.deletingContacts { - canBegin = true - } - return state - } - if canBegin { - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "This will remove your contacts from the Telegram servers.", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { - var begin = false - updateState { state in - var state = state - if !state.deletingContacts { - state = state.withUpdatedDeletingContacts(true) - begin = true - } - return state - } - - if !begin { - return - } - - let _ = updateContactSynchronizationSettingsInteractively(postbox: account.postbox, { settings in - var settings = settings - settings.synchronizeDeviceContacts = false - return settings - }) - - actionsDisposable.add((deleteAllContacts(postbox: account.postbox, network: account.network) - |> deliverOnMainQueue).start(completed: { - updateState { state in - var state = state - state = state.withUpdatedDeletingContacts(false) - return state - } - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.PrivacySettings_DeleteContactsSuccess, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) - })) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})])) - } - }, updateSyncContacts: { value in - let _ = updateContactSynchronizationSettingsInteractively(postbox: account.postbox, { settings in - var settings = settings - settings.synchronizeDeviceContacts = value - return settings - }).start() - }, updateSuggestFrequentContacts: { value in - let apply: () -> Void = { - updateState { state in - return state.withUpdatedUpdatedSuggestFrequentContacts(value) - } - let _ = updateRecentPeersEnabled(postbox: account.postbox, network: account.network, enabled: value).start() - } - if !value { - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.PrivacySettings_SuggestFrequentContactsDisableNotice, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - apply() - })])) - } else { - apply() - } + }, openDataSettings: { + pushControllerImpl?(dataPrivacyController(account: account)) }) let previousState = Atomic(value: nil) @@ -813,22 +517,6 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, privacySettingsPromise.get(), account.postbox.combinedView(keys: [.noticeEntry(ApplicationSpecificNotice.secretChatLinkPreviewsKey()), preferencesKey]), recentPeers(account: account)) |> map { presentationData, state, privacySettings, combined, recentPeers -> (ItemListControllerState, (ItemListNodeState, PrivacyAndSecurityEntry.ItemGenerationArguments)) in - let secretChatLinkPreviews = (combined.views[.noticeEntry(ApplicationSpecificNotice.secretChatLinkPreviewsKey())] as? NoticeEntryView)?.value.flatMap({ ApplicationSpecificNotice.getSecretChatLinkPreviews($0) }) - - let synchronizeDeviceContacts: Bool = ((combined.views[preferencesKey] as? PreferencesView)?.values[ApplicationSpecificPreferencesKeys.contactSynchronizationSettings] as? ContactSynchronizationSettings)?.synchronizeDeviceContacts ?? true - - let suggestRecentPeers: Bool - if let updatedSuggestFrequentContacts = state.updatedSuggestFrequentContacts { - suggestRecentPeers = updatedSuggestFrequentContacts - } else { - switch recentPeers { - case .peers: - suggestRecentPeers = true - case .disabled: - suggestRecentPeers = false - } - } - var rightNavigationButton: ItemListNavigationButton? if privacySettings == nil || state.updatingAccountTimeoutValue != nil { rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) @@ -837,14 +525,9 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PrivacySettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let previousStateValue = previousState.swap(state) - var animateChanges = false - if let previousStateValue = previousStateValue { - if previousStateValue.clearedPaymentInfo != state.clearedPaymentInfo { - animateChanges = true - } - } + let animateChanges = false - let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts, frequentContacts: suggestRecentPeers), style: .blocks, animateChanges: animateChanges) + let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings), style: .blocks, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/SaveToCameraRoll.swift b/TelegramUI/SaveToCameraRoll.swift index a9d90a88dc..76cbc6df7b 100644 --- a/TelegramUI/SaveToCameraRoll.swift +++ b/TelegramUI/SaveToCameraRoll.swift @@ -48,7 +48,7 @@ func saveToCameraRoll(applicationContext: TelegramApplicationContext, postbox: P |> mapToSignal { data -> Signal in if data.complete { return Signal { subscriber in - authorizeDeviceAccess(to: .mediaLibrary(.save), presentationData: applicationContext.currentPresentationData.with { $0 }, present: { c, a in + DeviceAccess.authorizeAccess(to: .mediaLibrary(.save), presentationData: applicationContext.currentPresentationData.with { $0 }, present: { c, a in applicationContext.presentGlobalController(c, a) }, openSettings: applicationContext.applicationBindings.openSettings, { authorized in if !authorized { diff --git a/TelegramUI/SearchBarNode.swift b/TelegramUI/SearchBarNode.swift index af5d6ecb8d..3fd515be13 100644 --- a/TelegramUI/SearchBarNode.swift +++ b/TelegramUI/SearchBarNode.swift @@ -104,6 +104,42 @@ private class SearchBarTextField: UITextField { } } +final class SearchBarNodeTheme { + let background: UIColor + let separator: UIColor + let inputFill: UIColor + let placeholder: UIColor + let primaryText: UIColor + let inputIcon: UIColor + let inputClear: UIColor + let accent: UIColor + let keyboard: PresentationThemeKeyboardColor + + init(background: UIColor, separator: UIColor, inputFill: UIColor, primaryText: UIColor, placeholder: UIColor, inputIcon: UIColor, inputClear: UIColor, accent: UIColor, keyboard: PresentationThemeKeyboardColor) { + self.background = background + self.separator = separator + self.inputFill = inputFill + self.primaryText = primaryText + self.placeholder = placeholder + self.inputIcon = inputIcon + self.inputClear = inputClear + self.accent = accent + self.keyboard = keyboard + } + + init(theme: PresentationTheme) { + self.background = theme.rootController.activeNavigationSearchBar.backgroundColor + self.separator = theme.rootController.navigationBar.separatorColor + self.inputFill = theme.rootController.activeNavigationSearchBar.inputFillColor + self.placeholder = theme.rootController.activeNavigationSearchBar.inputPlaceholderTextColor + self.primaryText = theme.rootController.activeNavigationSearchBar.inputTextColor + self.inputIcon = theme.rootController.activeNavigationSearchBar.inputIconColor + self.inputClear = theme.rootController.activeNavigationSearchBar.inputClearButtonColor + self.accent = theme.rootController.activeNavigationSearchBar.accentColor + self.keyboard = theme.chatList.searchBarKeyboardColor + } +} + class SearchBarNode: ASDisplayNode, UITextFieldDelegate { var cancel: (() -> Void)? var textUpdated: ((String) -> Void)? @@ -161,7 +197,7 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { if self.activity != oldValue { if self.activity { if self.activityIndicator == nil { - let activityIndicator = ActivityIndicator(type: .custom(self.theme.rootController.activeNavigationSearchBar.inputIconColor, 13.0, 1.0)) + let activityIndicator = ActivityIndicator(type: .custom(self.theme.inputIcon, 13.0, 1.0)) self.activityIndicator = activityIndicator self.addSubnode(activityIndicator) if let (boundingSize, leftInset, rightInset) = self.validLayout { @@ -179,47 +215,47 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { private var validLayout: (CGSize, CGFloat, CGFloat)? - private var theme: PresentationTheme + private var theme: SearchBarNodeTheme private var strings: PresentationStrings - init(theme: PresentationTheme, strings: PresentationStrings) { + init(theme: SearchBarNodeTheme, strings: PresentationStrings) { self.theme = theme self.strings = strings self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = theme.rootController.activeNavigationSearchBar.backgroundColor + self.backgroundNode.backgroundColor = theme.background self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true - self.separatorNode.backgroundColor = theme.rootController.activeNavigationSearchBar.separatorColor + self.separatorNode.backgroundColor = theme.separator self.textBackgroundNode = ASImageNode() self.textBackgroundNode.isLayerBacked = false self.textBackgroundNode.displaysAsynchronously = false self.textBackgroundNode.displayWithoutProcessing = true - self.textBackgroundNode.image = generateBackground(backgroundColor: theme.rootController.activeNavigationSearchBar.backgroundColor, foregroundColor: theme.rootController.activeNavigationSearchBar.inputFillColor) + self.textBackgroundNode.image = generateBackground(backgroundColor: theme.background, foregroundColor: theme.inputFill) self.iconNode = ASImageNode() self.iconNode.isLayerBacked = true self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true - self.iconNode.image = generateLoupeIcon(color: theme.rootController.activeNavigationSearchBar.inputIconColor) + self.iconNode.image = generateLoupeIcon(color: theme.inputIcon) self.textField = SearchBarTextField() self.textField.autocorrectionType = .no self.textField.returnKeyType = .done self.textField.font = Font.regular(14.0) - self.textField.textColor = theme.rootController.activeNavigationSearchBar.inputTextColor + self.textField.textColor = theme.primaryText self.clearButton = HighlightableButtonNode() self.clearButton.imageNode.displaysAsynchronously = false self.clearButton.imageNode.displayWithoutProcessing = true self.clearButton.displaysAsynchronously = false - self.clearButton.setImage(generateClearIcon(color: theme.rootController.activeNavigationSearchBar.inputClearButtonColor), for: []) + self.clearButton.setImage(generateClearIcon(color: theme.inputClear), for: []) self.clearButton.isHidden = true - switch theme.chatList.searchBarKeyboardColor { + switch theme.keyboard { case .light: self.textField.keyboardAppearance = .default case .dark: @@ -228,7 +264,7 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.cancelButton = ASButtonNode() self.cancelButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) - self.cancelButton.setAttributedTitle(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.activeNavigationSearchBar.accentColor), for: []) + self.cancelButton.setAttributedTitle(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.accent), for: []) self.cancelButton.displaysAsynchronously = false super.init() @@ -253,12 +289,12 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) } - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + func updateThemeAndStrings(theme: SearchBarNodeTheme, strings: PresentationStrings) { if self.theme !== theme { - self.cancelButton.setAttributedTitle(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.activeNavigationSearchBar.accentColor), for: []) - self.backgroundNode.backgroundColor = theme.rootController.activeNavigationSearchBar.backgroundColor - self.separatorNode.backgroundColor = theme.rootController.activeNavigationSearchBar.separatorColor - self.textBackgroundNode.image = generateBackground(backgroundColor: theme.rootController.activeNavigationSearchBar.backgroundColor, foregroundColor: theme.rootController.activeNavigationSearchBar.inputFillColor) + self.cancelButton.setAttributedTitle(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.accent), for: []) + self.backgroundNode.backgroundColor = theme.background + self.separatorNode.backgroundColor = theme.separator + self.textBackgroundNode.image = generateBackground(backgroundColor: theme.background, foregroundColor: theme.inputFill) } diff --git a/TelegramUI/SearchDisplayController.swift b/TelegramUI/SearchDisplayController.swift index 076bf5bd69..dbb7883640 100644 --- a/TelegramUI/SearchDisplayController.swift +++ b/TelegramUI/SearchDisplayController.swift @@ -14,7 +14,7 @@ final class SearchDisplayController { private var isSearchingDisposable: Disposable? init(theme: PresentationTheme, strings: PresentationStrings, contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) { - self.searchBar = SearchBarNode(theme: theme, strings: strings) + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme), strings: strings) self.contentNode = contentNode self.searchBar.textUpdated = { [weak contentNode] text in @@ -39,7 +39,7 @@ final class SearchDisplayController { } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - self.searchBar.updateThemeAndStrings(theme: theme, strings: strings) + self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: theme), strings: strings) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { diff --git a/TelegramUI/SecretChatKeyControllerNode.swift b/TelegramUI/SecretChatKeyControllerNode.swift index 4dc1a64026..1c1e2f86e2 100644 --- a/TelegramUI/SecretChatKeyControllerNode.swift +++ b/TelegramUI/SecretChatKeyControllerNode.swift @@ -152,7 +152,9 @@ final class SecretChatKeyControllerNode: ViewControllerTracingNode { let point = recognizer.location(in: recognizer.view) if let attributes = self.infoNode.attributesAtPoint(point)?.1 { if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { - openExternalUrl(account: self.account, url: url, presentationData: self.presentationData, applicationContext: self.account.telegramApplicationContext, navigationController: self.getNavigationController()) + openExternalUrl(account: self.account, url: url, presentationData: self.presentationData, applicationContext: self.account.telegramApplicationContext, navigationController: self.getNavigationController(), dismissInput: { [weak self] in + self?.view.endEditing(true) + }) } } } diff --git a/TelegramUI/SecureIdAuthController.swift b/TelegramUI/SecureIdAuthController.swift index f10dc004bf..3806c83391 100644 --- a/TelegramUI/SecureIdAuthController.swift +++ b/TelegramUI/SecureIdAuthController.swift @@ -73,7 +73,7 @@ final class SecureIdAuthController: ViewController { if let strongSelf = self { strongSelf.updateState { state in var state = state - if data.currentSalt != nil { + if data.currentPasswordDerivation != nil { state.verificationState = .passwordChallenge(data.currentHint ?? "", .none) } else { state.verificationState = .noChallenge @@ -233,7 +233,9 @@ final class SecureIdAuthController: ViewController { self?.grantAccess() }, openUrl: { [weak self] url in if let strongSelf = self { - openExternalUrl(account: strongSelf.account, url: url, presentationData: strongSelf.presentationData, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: strongSelf.navigationController as? NavigationController) + openExternalUrl(account: strongSelf.account, url: url, presentationData: strongSelf.presentationData, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: { + self?.view.endEditing(true) + }) } }, openMention: { [weak self] mention in guard let strongSelf = self else { diff --git a/TelegramUI/SecureIdDocumentFormControllerNode.swift b/TelegramUI/SecureIdDocumentFormControllerNode.swift index 1ac119cb65..a7bae0611a 100644 --- a/TelegramUI/SecureIdDocumentFormControllerNode.swift +++ b/TelegramUI/SecureIdDocumentFormControllerNode.swift @@ -1508,7 +1508,7 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode Signal) } @@ -197,6 +198,18 @@ public final class ShareController: ViewController { self?.saveToCameraRoll(image: representations) }) } + case let .media(mediaReference): + var canSave = false + if mediaReference.media is TelegramMediaImage { + canSave = true + } else if mediaReference.media is TelegramMediaFile { + canSave = true + } + if saveToCameraRoll && canSave { + self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Preview_SaveToCameraRoll, action: { [weak self] in + self?.saveToCameraRoll(mediaReference: mediaReference) + }) + } case let .messages(messages): if saveToCameraRoll { self.defaultAction = ShareControllerAction(title: self.presentationData.strings.Preview_SaveToCameraRoll, action: { [weak self] in @@ -266,9 +279,9 @@ public final class ShareController: ViewController { for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) } - messages.append(.message(text: url, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: url, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() } return .complete() @@ -276,9 +289,9 @@ public final class ShareController: ViewController { for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) } - messages.append(.message(text: string, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: string, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() } return .complete() @@ -286,12 +299,12 @@ public final class ShareController: ViewController { for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) } let attributedText = NSMutableAttributedString(string: string, attributes: [ChatTextInputAttributes.italic: true as NSNumber]) attributedText.append(NSAttributedString(string: "\n\n\(url)")) let entities = generateChatInputTextEntities(attributedText) - messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: attributedText.string, attributes: [TextEntitiesMessageAttribute(entities: entities)], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() } return .complete() @@ -299,9 +312,19 @@ public final class ShareController: ViewController { for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) } - messages.append(.message(text: "", attributes: [], media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: representations, reference: nil), replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: representations, reference: nil, partialReference: nil)), replyToMessageId: nil, localGroupingKey: nil)) + let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() + } + return .complete() + case let .media(mediaReference): + for peerId in peerIds { + var messages: [EnqueueMessage] = [] + if !text.isEmpty { + messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + } + messages.append(.message(text: "", attributes: [], mediaReference: mediaReference, replyToMessageId: nil, localGroupingKey: nil)) let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() } return .complete() @@ -309,9 +332,9 @@ public final class ShareController: ViewController { for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) } - messages.append(.message(text: "", attributes: [], media: media, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil)) let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messages).start() } return .complete() @@ -319,7 +342,7 @@ public final class ShareController: ViewController { for peerId in peerIds { var messagesToEnqueue: [EnqueueMessage] = [] if !text.isEmpty { - messagesToEnqueue.append(.message(text: text, attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)) + messagesToEnqueue.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) } for message in messages { messagesToEnqueue.append(.forward(source: message.id, grouping: .auto)) @@ -347,8 +370,10 @@ public final class ShareController: ViewController { case let .quote(text, url): collectableItems.append(CollectableExternalShareItem(url: "", text: "\"\(text)\"\n\n\(url)", mediaReference: nil)) case let .image(representations): - let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: representations, reference: nil) + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()), representations: representations, reference: nil, partialReference: nil) collectableItems.append(CollectableExternalShareItem(url: "", text: "", mediaReference: .standalone(media: media))) + case let .media(mediaReference): + collectableItems.append(CollectableExternalShareItem(url: "", text: "", mediaReference: mediaReference)) case let .mapMedia(media): let latLong = "\(media.latitude),\(media.longitude)" collectableItems.append(CollectableExternalShareItem(url: "https://media: maps.apple.com/maps?ll=\(latLong)&q=\(latLong)&t=m", text: "", mediaReference: nil)) @@ -468,7 +493,11 @@ public final class ShareController: ViewController { } private func saveToCameraRoll(image: [TelegramMediaImageRepresentation]) { - let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image, reference: nil) + let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image, reference: nil, partialReference: nil) self.controllerNode.transitionToProgress(signal: TelegramUI.saveToCameraRoll(applicationContext: self.account.telegramApplicationContext, postbox: self.account.postbox, mediaReference: .standalone(media: media))) } + + private func saveToCameraRoll(mediaReference: AnyMediaReference) { + self.controllerNode.transitionToProgress(signal: TelegramUI.saveToCameraRoll(applicationContext: self.account.telegramApplicationContext, postbox: self.account.postbox, mediaReference: mediaReference)) + } } diff --git a/TelegramUI/ShareControllerNode.swift b/TelegramUI/ShareControllerNode.swift index 88a20c38e4..35281e123d 100644 --- a/TelegramUI/ShareControllerNode.swift +++ b/TelegramUI/ShareControllerNode.swift @@ -467,7 +467,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate |> take(1) |> deliverOnMainQueue).start(next: { peers in if let strongSelf = self { - let searchContentNode = ShareSearchContainerNode(account: strongSelf.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction!, recentPeers: peers) + let searchContentNode = ShareSearchContainerNode(account: strongSelf.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction!, recentPeers: peers.map({ $0.peer })) searchContentNode.cancel = { if let strongSelf = self, let peersContentNode = strongSelf.peersContentNode { strongSelf.transitionToContentNode(peersContentNode) diff --git a/TelegramUI/SinglePhoneInputNode.swift b/TelegramUI/SinglePhoneInputNode.swift index bd03a9bcc4..af75e5b15c 100644 --- a/TelegramUI/SinglePhoneInputNode.swift +++ b/TelegramUI/SinglePhoneInputNode.swift @@ -79,6 +79,7 @@ final class SinglePhoneInputNode: ASDisplayNode, UITextFieldDelegate { private let fontSize: CGFloat var numberField: TextFieldNode? + var numberFieldText: String? var enableEditing: Bool = true @@ -107,6 +108,7 @@ final class SinglePhoneInputNode: ASDisplayNode, UITextFieldDelegate { let numberField = TextFieldNode() numberField.textField.font = Font.regular(self.fontSize) numberField.textField.keyboardType = .numberPad + numberField.textField.text = self.numberFieldText self.addSubnode(numberField) @@ -140,6 +142,7 @@ final class SinglePhoneInputNode: ASDisplayNode, UITextFieldDelegate { private func updateNumber(_ inputText: String) { let (_, numberText) = self.phoneFormatter.updateText(inputText) guard let numberField = self.numberField else { + self.numberFieldText = numberText return } diff --git a/TelegramUI/StickerPackPreviewGridItem.swift b/TelegramUI/StickerPackPreviewGridItem.swift index ab5e2eb4b4..8bf1efddc6 100644 --- a/TelegramUI/StickerPackPreviewGridItem.swift +++ b/TelegramUI/StickerPackPreviewGridItem.swift @@ -121,7 +121,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { if let (_, _, mediaDimensions) = self.currentState { let imageSize = mediaDimensions.aspectFitted(boundingSize) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) let boundingFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - boundingSize.width) / 2.0), y: (bounds.size.height - boundingSize.height) / 2.0), size: boundingSize) let textSize = CGSize(width: 32.0, height: 24.0) diff --git a/TelegramUI/StickerPaneSearchStickerItem.swift b/TelegramUI/StickerPaneSearchStickerItem.swift index bf5ef14486..44c9d25a5a 100644 --- a/TelegramUI/StickerPaneSearchStickerItem.swift +++ b/TelegramUI/StickerPaneSearchStickerItem.swift @@ -149,7 +149,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode { if let (_, _, mediaDimensions) = self.currentState { let imageSize = mediaDimensions.aspectFitted(boundingSize) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) } } diff --git a/TelegramUI/StickerPreviewControllerNode.swift b/TelegramUI/StickerPreviewControllerNode.swift index adc0284541..8e32912988 100644 --- a/TelegramUI/StickerPreviewControllerNode.swift +++ b/TelegramUI/StickerPreviewControllerNode.swift @@ -53,7 +53,7 @@ final class StickerPreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0)) let imageSize = dimensitons.aspectFitted(boundingSize) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: (layout.size.height - imageSize.height - textSpacing - textSize.height) / 4.0), size: imageSize) self.imageNode.frame = imageFrame diff --git a/TelegramUI/StickerPreviewPeekContent.swift b/TelegramUI/StickerPreviewPeekContent.swift index aa804e4927..d047cccc7f 100644 --- a/TelegramUI/StickerPreviewPeekContent.swift +++ b/TelegramUI/StickerPreviewPeekContent.swift @@ -92,7 +92,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0)) let imageSize = dimensitons.aspectFitted(boundingSize) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: textSize.height + textSpacing), size: imageSize) self.imageNode.frame = imageFrame diff --git a/TelegramUI/StickerResources.swift b/TelegramUI/StickerResources.swift index d10ce2b139..242fdfe4a3 100644 --- a/TelegramUI/StickerResources.swift +++ b/TelegramUI/StickerResources.swift @@ -145,10 +145,10 @@ func chatMessageSticker(account: Account, file: TelegramMediaFile, small: Bool, return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) - /*let drawingRect = arguments.drawingRect + let drawingRect = arguments.drawingRect let fittedSize = arguments.imageSize - let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize)*/ - let fittedRect = arguments.drawingRect + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + //let fittedRect = arguments.drawingRect var fullSizeImage: (UIImage, UIImage)? if let fullSizeData = fullSizeData, fullSizeComplete { diff --git a/TelegramUI/StringWithAppliedEntities.swift b/TelegramUI/StringWithAppliedEntities.swift index 781e2308e4..a777289b2e 100644 --- a/TelegramUI/StringWithAppliedEntities.swift +++ b/TelegramUI/StringWithAppliedEntities.swift @@ -44,6 +44,10 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba let string = NSMutableAttributedString(string: text, attributes: [NSAttributedStringKey.font: baseFont, NSAttributedStringKey.foregroundColor: baseColor]) var skipEntity = false let stringLength = string.length + var underlineAllLinks = false + if linkColor.isEqual(baseColor) { + underlineAllLinks = true + } for i in 0 ..< entities.count { if skipEntity { skipEntity = false @@ -71,18 +75,27 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba if nsString == nil { nsString = text as NSString } + if underlineAllLinks { + string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) + } string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Url), value: "mailto:\(nsString!.substring(with: range))", range: range) case .PhoneNumber: string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) if nsString == nil { nsString = text as NSString } + if underlineAllLinks { + string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) + } string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Url), value: "tel:\(nsString!.substring(with: range))", range: range) case let .TextUrl(url): string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) if nsString == nil { nsString = text as NSString } + if underlineAllLinks { + string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) + } string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Url), value: url, range: range) case .Bold: string.addAttribute(NSAttributedStringKey.font, value: boldFont, range: range) diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index 9ebf3fcb8a..d47c6c44b7 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -53,6 +53,7 @@ public final class TelegramApplicationContext { public let liveLocationManager: LiveLocationManager? public let contactsManager = DeviceContactsManager() + public let contactDataManager = DeviceContactDataManager() let peerChannelMemberCategoriesContextsManager = PeerChannelMemberCategoriesContextsManager() diff --git a/TelegramUI/TelegramInitializeLegacyComponents.swift b/TelegramUI/TelegramInitializeLegacyComponents.swift index 719a422f95..fa3607f3df 100644 --- a/TelegramUI/TelegramInitializeLegacyComponents.swift +++ b/TelegramUI/TelegramInitializeLegacyComponents.swift @@ -69,7 +69,7 @@ private final class LegacyComponentsAccessCheckerImpl: NSObject, LegacyComponent subject = .send } if let applicationContext = self.applicationContext { - authorizeDeviceAccess(to: .location(subject), presentationData: applicationContext.currentPresentationData.with { $0 }, present: applicationContext.presentGlobalController, openSettings: applicationContext.applicationBindings.openSettings, { value in + DeviceAccess.authorizeAccess(to: .location(subject), presentationData: applicationContext.currentPresentationData.with { $0 }, present: applicationContext.presentGlobalController, openSettings: applicationContext.applicationBindings.openSettings, { value in if !value { alertDismissCompletion?() } diff --git a/TelegramUI/TransformOutgoingMessageMedia.swift b/TelegramUI/TransformOutgoingMessageMedia.swift index f4846dd271..a8e33d48ee 100644 --- a/TelegramUI/TransformOutgoingMessageMedia.swift +++ b/TelegramUI/TransformOutgoingMessageMedia.swift @@ -122,6 +122,19 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me return result |> mapToSignal { data -> Signal in if data.complete { + if let smallest = smallestImageRepresentation(image.representations), smallest.dimensions.width > 100.0 || smallest.dimensions.height > 100.0 { + let smallestSize = smallest.dimensions.fitted(CGSize(width: 90.0, height: 90.0)) + if let fullImage = UIImage(contentsOfFile: data.path), let smallestImage = generateScaledImage(image: fullImage, size: smallestSize, scale: 1.0), let smallestData = compressImageToJPEG(smallestImage, quality: 0.7) { + var representations = image.representations + + let thumbnailResource = LocalFileMediaResource(fileId: arc4random64()) + postbox.mediaBox.storeResourceData(thumbnailResource.id, data: smallestData) + representations.append(TelegramMediaImageRepresentation(dimensions: smallestSize, resource: thumbnailResource)) + let updatedImage = TelegramMediaImage(imageId: image.imageId, representations: representations, reference: image.reference, partialReference: image.partialReference) + return .single(.standalone(media: updatedImage)) + } + } + return .single(nil) } else if opportunistic { return .single(nil) diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index d31383f518..9f2d0d87aa 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -25,8 +25,12 @@ private final class UserInfoControllerArguments { let openCallMenu: (String) -> Void let displayAboutContextMenu: (String) -> Void let openEncryptionKey: (SecretChatKeyFingerprint) -> Void + let addBotToGroup: () -> Void + let botSettings: () -> Void + let botHelp: () -> Void + let report: () -> Void - init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, addContact: @escaping () -> Void, shareContact: @escaping () -> Void, startSecretChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayCopyContextMenu: @escaping (UserInfoEntryTag, String) -> Void, call: @escaping () -> Void, openCallMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, openEncryptionKey: @escaping (SecretChatKeyFingerprint) -> Void) { + init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, addContact: @escaping () -> Void, shareContact: @escaping () -> Void, startSecretChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayCopyContextMenu: @escaping (UserInfoEntryTag, String) -> Void, call: @escaping () -> Void, openCallMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, openEncryptionKey: @escaping (SecretChatKeyFingerprint) -> Void, addBotToGroup: @escaping () -> Void, botSettings: @escaping () -> Void, botHelp: @escaping () -> Void, report: @escaping () -> Void) { self.account = account self.avatarAndNameInfoContext = avatarAndNameInfoContext self.updateEditingName = updateEditingName @@ -47,6 +51,10 @@ private final class UserInfoControllerArguments { self.openCallMenu = openCallMenu self.displayAboutContextMenu = displayAboutContextMenu self.openEncryptionKey = openEncryptionKey + self.addBotToGroup = addBotToGroup + self.botSettings = botSettings + self.botHelp = botHelp + self.report = report } } @@ -54,6 +62,7 @@ private enum UserInfoSection: ItemListSectionId { case info case actions case sharedMediaAndNotifications + case bot case block } @@ -77,6 +86,10 @@ private enum UserInfoEntry: ItemListNodeEntry { case notificationSound(PresentationTheme, String, String) case groupsInCommon(PresentationTheme, String, Int32) case secretEncryptionKey(PresentationTheme, String, SecretChatKeyFingerprint) + case botAddToGroup(PresentationTheme, String) + case botSettings(PresentationTheme, String) + case botHelp(PresentationTheme, String) + case botReport(PresentationTheme, String) case block(PresentationTheme, String, DestructiveUserInfoAction) var section: ItemListSectionId { @@ -87,6 +100,8 @@ private enum UserInfoEntry: ItemListNodeEntry { return UserInfoSection.actions.rawValue case .sharedMedia, .notifications, .notificationSound, .secretEncryptionKey, .groupsInCommon: return UserInfoSection.sharedMediaAndNotifications.rawValue + case .botAddToGroup, .botSettings, .botHelp, .botReport: + return UserInfoSection.bot.rawValue case .block: return UserInfoSection.block.rawValue } @@ -210,6 +225,30 @@ private enum UserInfoEntry: ItemListNodeEntry { } else { return false } + case let .botAddToGroup(lhsTheme, lhsText): + if case let .botAddToGroup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .botSettings(lhsTheme, lhsText): + if case let .botSettings(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .botHelp(lhsTheme, lhsText): + if case let .botHelp(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .botReport(lhsTheme, lhsText): + if case let .botReport(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .block(lhsTheme, lhsText, lhsAction): if case let .block(rhsTheme, rhsText, rhsAction) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsAction == rhsAction { return true @@ -247,8 +286,16 @@ private enum UserInfoEntry: ItemListNodeEntry { return 1008 case .secretEncryptionKey: return 1009 - case .block: + case .botAddToGroup: return 1010 + case .botSettings: + return 1011 + case .botHelp: + return 1012 + case .botReport: + return 1013 + case .block: + return 1014 } } @@ -271,13 +318,13 @@ private enum UserInfoEntry: ItemListNodeEntry { arguments.displayAboutContextMenu(value) }, tag: UserInfoEntryTag.about) case let .phoneNumber(theme, _, label, value, isMain): - return ItemListTextWithLabelItem(theme: theme, label: label, text: value, textColor: isMain ? .accent : .primary, enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: { + return ItemListTextWithLabelItem(theme: theme, label: label, text: value, textColor: .accent, enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: { arguments.openCallMenu(value) }, longTapAction: { arguments.displayCopyContextMenu(.phoneNumber, value) }, tag: UserInfoEntryTag.phoneNumber) case let .userName(theme, text, value): - return ItemListTextWithLabelItem(theme: theme, label: text, text: "@\(value)", enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: { + return ItemListTextWithLabelItem(theme: theme, label: text, text: "@\(value)", textColor: .accent, enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: { arguments.displayUsernameContextMenu("@\(value)") }, longTapAction: { arguments.displayCopyContextMenu(.username, "@\(value)") @@ -318,6 +365,22 @@ private enum UserInfoEntry: ItemListNodeEntry { return ItemListSecretChatKeyItem(theme: theme, title: text, fingerprint: fingerprint, sectionId: self.section, style: .plain, action: { arguments.openEncryptionKey(fingerprint) }) + case let .botAddToGroup(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + arguments.addBotToGroup() + }) + case let .botSettings(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + arguments.botSettings() + }) + case let .botHelp(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + arguments.botHelp() + }) + case let .botReport(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + arguments.report() + }) case let .block(theme, text, action): return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { switch action { @@ -402,16 +465,6 @@ private func stringForBlockAction(strings: PresentationStrings, action: Destruct } } -private func localizedPhoneNumberLabel(label: String, strings: PresentationStrings) -> String { - if label == "_$!!$_" { - return "mobile" - } else if label == "_$!!$_" { - return "home" - } else { - return label - } -} - private func userInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, deviceContacts: [DeviceContact], state: UserInfoState, peerChatState: PostboxCoding?, globalNotificationSettings: GlobalNotificationSettings) -> [UserInfoEntry] { var entries: [UserInfoEntry] = [] @@ -518,6 +571,15 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat entries.append(UserInfoEntry.secretEncryptionKey(presentationData.theme, presentationData.strings.Profile_EncryptionKey, keyFingerprint)) } + if let peer = peer as? TelegramUser, let botInfo = peer.botInfo { + if botInfo.flags.contains(.worksWithGroups) { + entries.append(UserInfoEntry.botAddToGroup(presentationData.theme, presentationData.strings.UserInfo_InviteBotToGroup)) + } + entries.append(UserInfoEntry.botSettings(presentationData.theme, presentationData.strings.UserInfo_BotSettings)) + entries.append(UserInfoEntry.botHelp(presentationData.theme, presentationData.strings.UserInfo_BotHelp)) + entries.append(UserInfoEntry.botReport(presentationData.theme, presentationData.strings.ReportPeer_Report)) + } + if let cachedData = view.cachedData as? CachedUserData { if cachedData.isBlocked { entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .unblock, peer: user), .unblock)) @@ -555,6 +617,8 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll var openChatImpl: (() -> Void)? var shareContactImpl: (() -> Void)? var startSecretChatImpl: (() -> Void)? + var botAddToGroupImpl: (() -> Void)? + var dismissInputImpl: (() -> Void)? let actionsDisposable = DisposableSet() @@ -730,7 +794,8 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll if let user = peer as? TelegramUser, user.botInfo != nil { updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: value).start()) if !value { - let _ = enqueueMessages(account: account, peerId: peer.id, messages: [.message(text: "/start", attributes: [], media: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() + let _ = enqueueMessages(account: account, peerId: peer.id, messages: [.message(text: "/start", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() + openChatImpl?() } } else { let text: String @@ -809,6 +874,24 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll pushControllerImpl?(SecretChatKeyController(account: account, fingerprint: fingerprint, peer: peer)) } }) + }, addBotToGroup: { + botAddToGroupImpl?() + }, botSettings: { + let _ = (account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { peer in + let _ = enqueueMessages(account: account, peerId: peer.id, messages: [.message(text: "/settings", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() + openChatImpl?() + }) + }, botHelp: { + let _ = (account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { peer in + let _ = enqueueMessages(account: account, peerId: peer.id, messages: [.message(text: "/help", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() + openChatImpl?() + }) + }, report: { + presentControllerImpl?(peerReportOptionsController(account: account, subject: .peer(peerId), present: { c, a in + presentControllerImpl?(c, a) + }), nil) }) let peerView = Promise() @@ -926,6 +1009,9 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll presentControllerImpl = { [weak controller] value, presentationArguments in controller?.present(value, in: .window(.root), with: presentationArguments) } + dismissInputImpl = { [weak controller] in + controller?.view.endEditing(true) + } openChatImpl = { [weak controller] in if let navigationController = (controller?.navigationController as? NavigationController) { navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) @@ -937,7 +1023,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll if let peer = peer as? TelegramUser, let phone = peer.phone { let selectionController = PeerSelectionController(account: account) selectionController.peerSelected = { [weak selectionController] peerId in - let _ = (enqueueMessages(account: account, peerId: peerId, messages: [.message(text: "", attributes: [], media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil), replyToMessageId: nil, localGroupingKey: nil)]) |> deliverOnMainQueue).start(completed: { + let _ = (enqueueMessages(account: account, peerId: peerId, messages: [.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), replyToMessageId: nil, localGroupingKey: nil)]) |> deliverOnMainQueue).start(completed: { if let controller = controller { let ready = ValuePromise() let _ = (ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in @@ -992,6 +1078,18 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll } }) } + botAddToGroupImpl = { [weak controller] in + guard let controller = controller else { + return + } + openResolvedUrl(.groupBotStart(peerId: peerId, payload: ""), account: account, navigationController: controller.navigationController as? NavigationController, openPeer: { id, navigation in + + }, present: { c, a in + presentControllerImpl?(c, a) + }, dismissInput: { + dismissInputImpl?() + }) + } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { var result: ((ASDisplayNode, () -> UIView?), CGRect)? diff --git a/TelegramUI/UserInfoEditingPhoneItem.swift b/TelegramUI/UserInfoEditingPhoneItem.swift index a85f0e04c1..e47ff5086a 100644 --- a/TelegramUI/UserInfoEditingPhoneItem.swift +++ b/TelegramUI/UserInfoEditingPhoneItem.swift @@ -20,8 +20,9 @@ class UserInfoEditingPhoneItem: ListViewItem, ItemListItem { let updated: (String) -> Void let selectLabel: () -> Void let delete: () -> Void + let tag: ItemListItemTag? - init(theme: PresentationTheme, strings: PresentationStrings, id: Int64, label: String, value: String, editing: UserInfoEditingPhoneItemEditing, sectionId: ItemListSectionId, setPhoneIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, updated: @escaping (String) -> Void, selectLabel: @escaping () -> Void, delete: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, id: Int64, label: String, value: String, editing: UserInfoEditingPhoneItemEditing, sectionId: ItemListSectionId, setPhoneIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, updated: @escaping (String) -> Void, selectLabel: @escaping () -> Void, delete: @escaping () -> Void, tag: ItemListItemTag?) { self.theme = theme self.strings = strings self.id = id @@ -33,6 +34,7 @@ class UserInfoEditingPhoneItem: ListViewItem, ItemListItem { self.updated = updated self.selectLabel = selectLabel self.delete = delete + self.tag = tag } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { @@ -71,7 +73,7 @@ class UserInfoEditingPhoneItem: ListViewItem, ItemListItem { private let titleFont = Font.regular(15.0) -class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode { +class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, ItemListItemFocusableNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -85,7 +87,7 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode { private var item: UserInfoEditingPhoneItem? private var layoutParams: ListViewItemLayoutParams? - var tag: Any? { + var tag: ItemListItemTag? { return self.item?.tag } @@ -289,4 +291,8 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode { @objc func labelPressed() { self.item?.selectLabel() } + + func focus() { + self.phoneNode.numberField?.becomeFirstResponder() + } } diff --git a/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift b/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift index af061389e2..204f260a55 100644 --- a/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift @@ -166,7 +166,7 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { var imageResource: TelegramMediaResource? switch item.result { - case let .externalReference(_, _, title, _, url, content, thumbnail, _): + case let .externalReference(_, _, _, title, _, url, content, thumbnail, _): if let thumbnail = thumbnail { imageResource = thumbnail.resource } @@ -185,7 +185,7 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { iconText = NSAttributedString(string: host.substring(to: host.index(after: host.startIndex)).uppercased(), font: iconFont, textColor: UIColor.white) } } - case let .internalReference(_, _, title, _, image, file, _): + case let .internalReference(_, _, _, title, _, image, file, _): if let image = image { imageResource = imageRepresentationLargerThan(image.representations, size: CGSize(width: 200.0, height: 200.0))?.resource } else if let file = file { @@ -220,7 +220,7 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { if updatedIconImageResource { if let imageResource = imageResource { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 55.0, height: 55.0), resource: imageResource) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil, partialReference: nil) updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photoReference: .standalone(media: tmpImage)) } else { updateIconImageSignal = .complete()