diff --git a/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionReport.imageset/Contents.json b/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionReport.imageset/Contents.json new file mode 100644 index 0000000000..24bbabc041 --- /dev/null +++ b/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionReport.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_report.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionReport.imageset/ic_report.pdf b/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionReport.imageset/ic_report.pdf new file mode 100644 index 0000000000..92a7a593e4 Binary files /dev/null and b/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionReport.imageset/ic_report.pdf differ diff --git a/Images.xcassets/Chat/Input/Media/GridSetupIcon.imageset/Contents.json b/Images.xcassets/Chat/Input/Media/GridSetupIcon.imageset/Contents.json new file mode 100644 index 0000000000..842d8fb7a1 --- /dev/null +++ b/Images.xcassets/Chat/Input/Media/GridSetupIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "StickersGroupSettings@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "StickersGroupSettings@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Media/GridSetupIcon.imageset/StickersGroupSettings@2x.png b/Images.xcassets/Chat/Input/Media/GridSetupIcon.imageset/StickersGroupSettings@2x.png new file mode 100644 index 0000000000..774c3013a4 Binary files /dev/null and b/Images.xcassets/Chat/Input/Media/GridSetupIcon.imageset/StickersGroupSettings@2x.png differ diff --git a/Images.xcassets/Chat/Input/Media/GridSetupIcon.imageset/StickersGroupSettings@3x.png b/Images.xcassets/Chat/Input/Media/GridSetupIcon.imageset/StickersGroupSettings@3x.png new file mode 100644 index 0000000000..17c303914e Binary files /dev/null and b/Images.xcassets/Chat/Input/Media/GridSetupIcon.imageset/StickersGroupSettings@3x.png differ diff --git a/Images.xcassets/Chat/Input/Media/StickersNotFoundIcon.imageset/Contents.json b/Images.xcassets/Chat/Input/Media/StickersNotFoundIcon.imageset/Contents.json new file mode 100644 index 0000000000..c7c5e24a31 --- /dev/null +++ b/Images.xcassets/Chat/Input/Media/StickersNotFoundIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "StickersPlaceholderIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "StickersPlaceholderIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Media/StickersNotFoundIcon.imageset/StickersPlaceholderIcon@2x.png b/Images.xcassets/Chat/Input/Media/StickersNotFoundIcon.imageset/StickersPlaceholderIcon@2x.png new file mode 100644 index 0000000000..62c00b234e Binary files /dev/null and b/Images.xcassets/Chat/Input/Media/StickersNotFoundIcon.imageset/StickersPlaceholderIcon@2x.png differ diff --git a/Images.xcassets/Chat/Input/Media/StickersNotFoundIcon.imageset/StickersPlaceholderIcon@3x.png b/Images.xcassets/Chat/Input/Media/StickersNotFoundIcon.imageset/StickersPlaceholderIcon@3x.png new file mode 100644 index 0000000000..f9e6a6bb7b Binary files /dev/null and b/Images.xcassets/Chat/Input/Media/StickersNotFoundIcon.imageset/StickersPlaceholderIcon@3x.png differ diff --git a/Images.xcassets/Peer Info/GroupStickerPackNotFound.imageset/Contents.json b/Images.xcassets/Peer Info/GroupStickerPackNotFound.imageset/Contents.json new file mode 100644 index 0000000000..ba61303252 --- /dev/null +++ b/Images.xcassets/Peer Info/GroupStickerPackNotFound.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "StickerPackNotFoundIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "StickerPackNotFoundIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Peer Info/GroupStickerPackNotFound.imageset/StickerPackNotFoundIcon@2x.png b/Images.xcassets/Peer Info/GroupStickerPackNotFound.imageset/StickerPackNotFoundIcon@2x.png new file mode 100644 index 0000000000..7dc666fc30 Binary files /dev/null and b/Images.xcassets/Peer Info/GroupStickerPackNotFound.imageset/StickerPackNotFoundIcon@2x.png differ diff --git a/Images.xcassets/Peer Info/GroupStickerPackNotFound.imageset/StickerPackNotFoundIcon@3x.png b/Images.xcassets/Peer Info/GroupStickerPackNotFound.imageset/StickerPackNotFoundIcon@3x.png new file mode 100644 index 0000000000..825ff31209 Binary files /dev/null and b/Images.xcassets/Peer Info/GroupStickerPackNotFound.imageset/StickerPackNotFoundIcon@3x.png differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index d40a5b68bb..d61153abc7 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -191,6 +191,7 @@ D07E413D208A494D00FCA8F0 /* ProxyServerActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07E413C208A494D00FCA8F0 /* ProxyServerActionSheetController.swift */; }; D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */; }; D083491C209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D083491B209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift */; }; + D084023420E295F000065674 /* GroupStickerPackSetupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D084023320E295F000065674 /* GroupStickerPackSetupController.swift */; }; D087BFAD1F741B9D003FD209 /* ShareContentContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFAC1F741B9D003FD209 /* ShareContentContainerNode.swift */; }; D087BFAF1F741BB7003FD209 /* ShareLoadingContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFAE1F741BB7003FD209 /* ShareLoadingContainerNode.swift */; }; D087BFB11F745483003FD209 /* ShareSearchBarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFB01F745483003FD209 /* ShareSearchBarNode.swift */; }; @@ -223,6 +224,8 @@ D0943B001FDAE852001522CC /* ChatFeedNavigationInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943AFF1FDAE852001522CC /* ChatFeedNavigationInputPanelNode.swift */; }; D0943B051FDDFDA0001522CC /* OverlayInstantVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */; }; D0943B071FDEC529001522CC /* InstantVideoRadialStatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943B061FDEC528001522CC /* InstantVideoRadialStatusNode.swift */; }; + D097C26820DD0A1D007BB4B8 /* PeerReportController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D097C26720DD0A1D007BB4B8 /* PeerReportController.swift */; }; + D097C26C20DD1EA5007BB4B8 /* OverlayStatusController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D097C26B20DD1EA5007BB4B8 /* OverlayStatusController.swift */; }; D099D74D1EEFEE1500A3128C /* GameController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D74C1EEFEE1500A3128C /* GameController.swift */; }; D099D74F1EEFEE6A00A3128C /* GameControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D74E1EEFEE6A00A3128C /* GameControllerNode.swift */; }; D099D7511EEFF91E00A3128C /* GameControllerTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D7501EEFF91E00A3128C /* GameControllerTitleView.swift */; }; @@ -942,6 +945,8 @@ D0F0AAE21EC20EF8005EE2A5 /* CallControllerStatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0AAE11EC20EF8005EE2A5 /* CallControllerStatusNode.swift */; }; D0F0AAE41EC21AAA005EE2A5 /* CallControllerButtonsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0AAE31EC21AAA005EE2A5 /* CallControllerButtonsNode.swift */; }; 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 */; }; 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 */; }; @@ -1422,6 +1427,7 @@ D07E413C208A494D00FCA8F0 /* ProxyServerActionSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyServerActionSheetController.swift; sourceTree = ""; }; D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageManagedMediaId.swift; sourceTree = ""; }; D083491B209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarGalleryItemFooterContentNode.swift; sourceTree = ""; }; + D084023320E295F000065674 /* GroupStickerPackSetupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStickerPackSetupController.swift; sourceTree = ""; }; D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListEditableDeleteControlNode.swift; sourceTree = ""; }; D08774F91E3E2A5600A97350 /* ItemListCheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListCheckboxItem.swift; sourceTree = ""; }; D08775081E3E59DE00A97350 /* PeerNotificationSoundStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerNotificationSoundStrings.swift; sourceTree = ""; }; @@ -1475,6 +1481,8 @@ D096A4611EA681A90000A7AE /* PresentationsResourceCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationsResourceCache.swift; sourceTree = ""; }; D096A4631EA683C90000A7AE /* PresentationTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationTheme.swift; sourceTree = ""; }; D096A47A1EA6A2F00000A7AE /* PresentationStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationStrings.swift; sourceTree = ""; }; + D097C26720DD0A1D007BB4B8 /* PeerReportController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerReportController.swift; sourceTree = ""; }; + D097C26B20DD1EA5007BB4B8 /* OverlayStatusController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayStatusController.swift; sourceTree = ""; }; D099261E1E69791E00D95539 /* GroupsInCommonController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupsInCommonController.swift; sourceTree = ""; }; D099D74C1EEFEE1500A3128C /* GameController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameController.swift; sourceTree = ""; }; D099D74E1EEFEE6A00A3128C /* GameControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameControllerNode.swift; sourceTree = ""; }; @@ -1867,6 +1875,8 @@ D0F0AAE11EC20EF8005EE2A5 /* CallControllerStatusNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallControllerStatusNode.swift; sourceTree = ""; }; D0F0AAE31EC21AAA005EE2A5 /* CallControllerButtonsNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallControllerButtonsNode.swift; sourceTree = ""; }; D0F0AAE51EC21B68005EE2A5 /* CallControllerButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallControllerButton.swift; sourceTree = ""; }; + D0F19F6120E5694D00EEC860 /* GroupStickerPackCurrentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStickerPackCurrentItem.swift; sourceTree = ""; }; + D0F19F6320E5A15B00EEC860 /* ChatMediaInputPeerSpecificItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMediaInputPeerSpecificItem.swift; sourceTree = ""; }; D0F3A8AA1E82D83E00B4C64C /* TelegramAccountAuxiliaryMethods.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramAccountAuxiliaryMethods.swift; sourceTree = ""; }; 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 = ""; }; @@ -2193,6 +2203,7 @@ D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */, D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */, D049EAE51E44AD5600A2CD3A /* ChatMediaInputMetaSectionItemNode.swift */, + D0F19F6320E5A15B00EEC860 /* ChatMediaInputPeerSpecificItem.swift */, D002A0DC1E9CD52A00A81812 /* ChatMediaInputRecentGifsItem.swift */, D0575AEE1E9FF881006F2541 /* ChatMediaInputTrendingItem.swift */, D01C06B41FBB7720001561AB /* ChatMediaInputSettingsItem.swift */, @@ -2695,6 +2706,7 @@ D07ABBA4202A14BC003671DE /* LegacyImagePicker.swift */, D07ABBAA202A1BD1003671DE /* LegacyWallpaperEditor.swift */, D0380DAC204ED434000414AB /* LegacyLiveUploadInterface.swift */, + D097C26B20DD1EA5007BB4B8 /* OverlayStatusController.swift */, ); name = "Legacy Components"; sourceTree = ""; @@ -3547,6 +3559,9 @@ D0E8B8BC204479A500605593 /* SecretChatKeyController.swift */, D0E8B8BE20447A4600605593 /* SecretChatKeyControllerNode.swift */, D0BFAE4F20AB2A1300793CF2 /* PeerBanTimeoutController.swift */, + D097C26720DD0A1D007BB4B8 /* PeerReportController.swift */, + D084023320E295F000065674 /* GroupStickerPackSetupController.swift */, + D0F19F6120E5694D00EEC860 /* GroupStickerPackCurrentItem.swift */, ); name = "Peer Info"; sourceTree = ""; @@ -4695,6 +4710,7 @@ D0EC6D401EB9F58800EBF1C3 /* MediaNavigationAccessoryContainerNode.swift in Sources */, D0E266FD1F66706500BFC79F /* ChatBubbleVideoDecoration.swift in Sources */, D0EC6D411EB9F58800EBF1C3 /* MediaNavigationAccessoryHeaderNode.swift in Sources */, + D097C26820DD0A1D007BB4B8 /* PeerReportController.swift in Sources */, D0471B491EFD59170074D609 /* BotCheckoutControllerNode.swift in Sources */, D0EC6D421EB9F58800EBF1C3 /* MediaNavigationAccessoryItemListNode.swift in Sources */, D01BAA181ECC8E0000295217 /* CallListController.swift in Sources */, @@ -4811,6 +4827,7 @@ D01776BC1F1E21AF0044446D /* RadialStatusBackgroundNode.swift in Sources */, D0FE4DE61F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift in Sources */, D0E8B8A72044339500605593 /* PresentationCallToneData.swift in Sources */, + D0F19F6420E5A15B00EEC860 /* ChatMediaInputPeerSpecificItem.swift in Sources */, D0AEAE252080D6830013176E /* StickerPaneSearchContainerNode.swift in Sources */, D01DBA9B209CC6AD00C64E64 /* ChatLinkPreview.swift in Sources */, D044A0FB20BDC40C00326FAC /* CachedChannelAdmins.swift in Sources */, @@ -4850,6 +4867,7 @@ D0EC6D9D1EB9F58900EBF1C3 /* ChatMessageTextBubbleContentNode.swift in Sources */, D0E9BA2B1F0557A600F079A4 /* STPFormEncoder.m in Sources */, D01BAA1C1ECC92F700295217 /* CallListViewTransition.swift in Sources */, + D097C26C20DD1EA5007BB4B8 /* OverlayStatusController.swift in Sources */, D0EC6D9E1EB9F58900EBF1C3 /* ChatMessageWebpageBubbleContentNode.swift in Sources */, D06CF82720D0080200AC4CFF /* SecureIdAuthListContentNode.swift in Sources */, D0C0B5901EDB505E000F4D2C /* ActivityIndicator.swift in Sources */, @@ -5087,6 +5105,7 @@ D0EC6E2C1EB9F58900EBF1C3 /* ComposeControllerNode.swift in Sources */, D0EC6E2D1EB9F58900EBF1C3 /* CounterContollerTitleView.swift in Sources */, D0AEAE292080FD660013176E /* StickerPaneSearchGlobaltem.swift in Sources */, + D0F19F6220E5694D00EEC860 /* GroupStickerPackCurrentItem.swift in Sources */, D0EC6E2E1EB9F58900EBF1C3 /* ContactMultiselectionController.swift in Sources */, D0EC6E2F1EB9F58900EBF1C3 /* ContactMultiselectionControllerNode.swift in Sources */, D0EC6E301EB9F58900EBF1C3 /* ContactSelectionController.swift in Sources */, @@ -5120,6 +5139,7 @@ D0EC6E431EB9F58900EBF1C3 /* ItemListEditableDeleteControlNode.swift in Sources */, D0EC6E441EB9F58900EBF1C3 /* ItemListSingleLineInputItem.swift in Sources */, D01776B31F1D69A80044446D /* RadialStatusNode.swift in Sources */, + D084023420E295F000065674 /* GroupStickerPackSetupController.swift in Sources */, D01C06BE1FBCAF06001561AB /* ChatMessageBubbleMosaicLayout.swift in Sources */, D0EC6E451EB9F58900EBF1C3 /* ItemListMultilineTextItem.swift in Sources */, D02F4AE91FCF370B004DFBAE /* ChatMessageInteractiveMediaBadge.swift in Sources */, @@ -5438,6 +5458,7 @@ "-DTGVOIP_USE_CUSTOM_CRYPTO", "-DWEBRTC_APM_DEBUG_DUMP=0", "-DWEBRTC_POSIX", + "-DMINIMAL_ASDK=1", ); OTHER_LDFLAGS = "-ObjC"; OTHER_SWIFT_FLAGS = ""; @@ -5548,6 +5569,7 @@ "-DTGVOIP_USE_CUSTOM_CRYPTO", "-DWEBRTC_APM_DEBUG_DUMP=0", "-DWEBRTC_POSIX", + "-DMINIMAL_ASDK=1", ); OTHER_LDFLAGS = "-ObjC"; OTHER_SWIFT_FLAGS = ""; @@ -5586,6 +5608,7 @@ "-DTGVOIP_USE_CUSTOM_CRYPTO", "-DWEBRTC_APM_DEBUG_DUMP=0", "-DWEBRTC_POSIX", + "-DMINIMAL_ASDK=1", ); OTHER_LDFLAGS = "-ObjC"; OTHER_SWIFT_FLAGS = ""; @@ -5623,6 +5646,7 @@ "-DTGVOIP_USE_CUSTOM_CRYPTO", "-DWEBRTC_APM_DEBUG_DUMP=0", "-DWEBRTC_POSIX", + "-DMINIMAL_ASDK=1", ); OTHER_LDFLAGS = "-ObjC"; OTHER_SWIFT_FLAGS = ""; @@ -5658,6 +5682,7 @@ "-DTGVOIP_USE_CUSTOM_CRYPTO", "-DWEBRTC_APM_DEBUG_DUMP=0", "-DWEBRTC_POSIX", + "-DMINIMAL_ASDK=1", ); OTHER_LDFLAGS = "-ObjC"; OTHER_SWIFT_FLAGS = ""; diff --git a/TelegramUI/ArhivedStickerPacksController.swift b/TelegramUI/ArhivedStickerPacksController.swift index cdb3ed583a..afa7ffc71a 100644 --- a/TelegramUI/ArhivedStickerPacksController.swift +++ b/TelegramUI/ArhivedStickerPacksController.swift @@ -195,10 +195,6 @@ private struct ArchivedStickerPacksControllerState: Equatable { } } -private func stringForStickerCount(_ count: Int32, strings: PresentationStrings) -> String { - return strings.StickerPack_StickerCount(count) -} - private func archivedStickerPacksControllerEntries(presentationData: PresentationData, state: ArchivedStickerPacksControllerState, packs: [ArchivedStickerPackItem]?, installedView: CombinedView) -> [ArchivedStickerPacksEntry] { var entries: [ArchivedStickerPacksEntry] = [] @@ -213,7 +209,7 @@ private func archivedStickerPacksControllerEntries(presentationData: Presentatio var index: Int32 = 0 for item in packs { if !installedIds.contains(item.info.id) { - entries.append(.pack(index, presentationData.theme, presentationData.strings, item.info, item.topItems.first, stringForStickerCount(item.info.count, strings: presentationData.strings), !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == item.info.id, reorderable: false))) + entries.append(.pack(index, presentationData.theme, presentationData.strings, item.info, item.topItems.first, presentationData.strings.StickerPack_StickerCount(item.info.count), !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == item.info.id, reorderable: false))) index += 1 } } diff --git a/TelegramUI/AuthorizationSequenceController.swift b/TelegramUI/AuthorizationSequenceController.swift index 6ec1830ed2..f939acb3a1 100644 --- a/TelegramUI/AuthorizationSequenceController.swift +++ b/TelegramUI/AuthorizationSequenceController.swift @@ -16,7 +16,7 @@ public final class AuthorizationSequenceController: NavigationController { private var account: UnauthorizedAccount private let apiId: Int32 private let apiHash: String - private let strings: PresentationStrings + private var strings: PresentationStrings public let theme: AuthorizationTheme private let openUrl: (String) -> Void @@ -59,9 +59,12 @@ public final class AuthorizationSequenceController: NavigationController { if let currentController = currentController { controller = currentController } else { - controller = AuthorizationSequenceSplashController(theme: self.theme) - controller.nextPressed = { [weak self] in + controller = AuthorizationSequenceSplashController(postbox: self.account.postbox, network: self.account.network, theme: self.theme) + controller.nextPressed = { [weak self] strings in if let strongSelf = self { + if let strings = strings { + strongSelf.strings = strings + } let masterDatacenterId = strongSelf.account.masterDatacenterId var countryId: String? = nil @@ -150,7 +153,9 @@ public final class AuthorizationSequenceController: NavigationController { var currentController: AuthorizationSequenceCodeEntryController? for c in self.viewControllers { if let c = c as? AuthorizationSequenceCodeEntryController { - currentController = c + if c.data?.1 == type { + currentController = c + } break } } diff --git a/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift b/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift index e6a3712819..1ccdb923f9 100644 --- a/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift @@ -80,6 +80,8 @@ private final class PhoneAndCountryNode: ASDisplayNode { self.countryButton = ASButtonNode() self.countryButton.displaysAsynchronously = false self.countryButton.setBackgroundImage(countryButtonBackground, for: []) + self.countryButton.titleNode.maximumNumberOfLines = 1 + self.countryButton.titleNode.truncationMode = .byTruncatingTail self.countryButton.setBackgroundImage(countryButtonHighlightedBackground, for: .highlighted) self.phoneBackground = ASImageNode() diff --git a/TelegramUI/AuthorizationSequenceSplashController.swift b/TelegramUI/AuthorizationSequenceSplashController.swift index 084cbe6f77..5547313724 100644 --- a/TelegramUI/AuthorizationSequenceSplashController.swift +++ b/TelegramUI/AuthorizationSequenceSplashController.swift @@ -1,30 +1,77 @@ import Foundation import Display import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit import TelegramUIPrivateModule +import LegacyComponents final class AuthorizationSequenceSplashController: ViewController { private var controllerNode: AuthorizationSequenceSplashControllerNode { return self.displayNode as! AuthorizationSequenceSplashControllerNode } + private let postbox: Postbox + private let network: Network private let theme: AuthorizationTheme private let controller: RMIntroViewController - var nextPressed: (() -> Void)? + var nextPressed: ((PresentationStrings?) -> Void)? - init(theme: AuthorizationTheme) { + private let activateLocalizationDisposable = MetaDisposable() + + init(postbox: Postbox, network: Network, theme: AuthorizationTheme) { + self.postbox = postbox + self.network = network self.theme = theme - self.controller = RMIntroViewController(backroundColor: theme.backgroundColor, primaryColor: theme.primaryColor, accentColor: theme.accentColor, regularDotColor: theme.disclosureControlColor, highlightedDotColor: theme.accentColor) + + let localizationSignal = SSignal(generator: { subscriber in + let disposable = currentlySuggestedLocalization(network: network, extractKeys: ["Login.ContinueWithLocalization"]).start(next: { localization in + guard let localization = localization else { + return + } + + var continueWithLanguageString: String = "Continue" + for entry in localization.extractedEntries { + switch entry { + case let .string(key, value): + if key == "Login.ContinueWithLocalization" { + continueWithLanguageString = value + } + default: + break + } + } + + if let available = localization.availableLocalizations.first, available.languageCode != "en" { + let value = TGSuggestedLocalization(info: TGAvailableLocalization(title: available.title, localizedTitle: available.localizedTitle, code: available.languageCode), continueWithLanguageString: continueWithLanguageString, chooseLanguageString: "Choose Language", chooseLanguageOtherString: "Choose Language", englishLanguageNameString: "English") + subscriber?.putNext(value) + } + }, completed: { + subscriber?.putCompletion() + }) + + return SBlockDisposable(block: { + disposable.dispose() + }) + }) + + self.controller = RMIntroViewController(backroundColor: theme.backgroundColor, primaryColor: theme.primaryColor, accentColor: theme.accentColor, regularDotColor: theme.disclosureControlColor, highlightedDotColor: theme.accentColor, suggestedLocalizationSignal: localizationSignal) super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = theme.statusBarStyle self.controller.startMessaging = { [weak self] in - self?.nextPressed?() + self?.activateLocalization("en") + } + self.controller.startMessagingInAlternativeLanguage = { [weak self] code in + if let code = code { + self?.activateLocalization(code) + } } } @@ -32,6 +79,10 @@ final class AuthorizationSequenceSplashController: ViewController { fatalError("init(coder:) has not been implemented") } + deinit { + self.activateLocalizationDisposable.dispose() + } + override public func loadDisplayNode() { self.displayNode = AuthorizationSequenceSplashControllerNode(theme: self.theme) self.displayNodeDidLoad() @@ -83,4 +134,48 @@ final class AuthorizationSequenceSplashController: ViewController { }) } } + + private func activateLocalization(_ code: String) { + let _ = (postbox.transaction { transaction -> String in + if let current = transaction.getPreferencesEntry(key: PreferencesKeys.localizationSettings) as? LocalizationSettings { + return current.languageCode + } else { + return "en" + } + } |> deliverOnMainQueue).start(next: { [weak self] currentCode in + guard let strongSelf = self else { + return + } + + if currentCode == code { + strongSelf.nextPressed?(nil) + return + } + + strongSelf.controller.isEnabled = false + let postbox = strongSelf.postbox + + strongSelf.activateLocalizationDisposable.set(downoadAndApplyLocalization(postbox: postbox, network: strongSelf.network, languageCode: code).start(completed: { + let _ = (postbox.transaction { transaction -> PresentationStrings? in + let localizationSettings: LocalizationSettings? + if let current = transaction.getPreferencesEntry(key: PreferencesKeys.localizationSettings) as? LocalizationSettings { + localizationSettings = current + } else { + localizationSettings = nil + } + let stringsValue: PresentationStrings + if let localizationSettings = localizationSettings { + stringsValue = PresentationStrings(languageCode: localizationSettings.languageCode, dict: dictFromLocalization(localizationSettings.localization)) + } else { + stringsValue = defaultPresentationStrings + } + return stringsValue + } + |> deliverOnMainQueue).start(next: { strings in + self?.controller.isEnabled = true + self?.nextPressed?(strings) + }) + })) + }) + } } diff --git a/TelegramUI/AutodownloadSizeLimitItem.swift b/TelegramUI/AutodownloadSizeLimitItem.swift index 0045da1e0f..a2b6d69a99 100644 --- a/TelegramUI/AutodownloadSizeLimitItem.swift +++ b/TelegramUI/AutodownloadSizeLimitItem.swift @@ -117,6 +117,7 @@ class AutodownloadSizeLimitItemNode: ListViewItemNode { let sliderView = TGPhotoEditorSliderView() sliderView.enablePanHandling = true + sliderView.enablePanHandling = true sliderView.trackCornerRadius = 1.0 sliderView.lineSize = 2.0 sliderView.dotSize = 5.0 diff --git a/TelegramUI/AutomaticMediaDownloadSettings.swift b/TelegramUI/AutomaticMediaDownloadSettings.swift index a34eb86d19..137f5b4fc2 100644 --- a/TelegramUI/AutomaticMediaDownloadSettings.swift +++ b/TelegramUI/AutomaticMediaDownloadSettings.swift @@ -98,8 +98,8 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable { photo: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: Int32.max), video: AutomaticMediaDownloadCategory(cellular: false, wifi: false, sizeLimit: 1 * 1024 * 1024), file: AutomaticMediaDownloadCategory(cellular: false, wifi: false, sizeLimit: 1 * 1024 * 1024), - voiceMessage: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: Int32.max), - videoMessage: AutomaticMediaDownloadCategory(cellular: false, wifi: false, sizeLimit: Int32.max) + voiceMessage: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: 1 * 1024 * 1024), + videoMessage: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: 1 * 1024 * 1024) ) return AutomaticMediaDownloadSettings(masterEnabled: true, peers: AutomaticMediaDownloadPeers( contacts: defaultCategory, @@ -167,24 +167,44 @@ func updateMediaDownloadSettingsInteractively(postbox: Postbox, _ f: @escaping ( } } -private func categoriesForPeer(_ peer: Peer, settings: AutomaticMediaDownloadSettings) -> AutomaticMediaDownloadCategories { +public enum AutomaticMediaDownloadPeerType { + case contact + case otherPrivate + case group + case channel +} + +private func tempPeerTypeForPeer(_ peer: Peer) -> AutomaticMediaDownloadPeerType { if let _ = peer as? TelegramUser { - return settings.peers.contacts + return .contact } else if let _ = peer as? TelegramSecretChat { - return settings.peers.contacts + return .contact } else if let channel = peer as? TelegramChannel { if case .broadcast = channel.info { - return settings.peers.channels + return .channel } else { - return settings.peers.groups + return .group } } else { - return settings.peers.channels + return .channel + } +} + +private func categoriesForPeerType(_ type: AutomaticMediaDownloadPeerType, settings: AutomaticMediaDownloadSettings) -> AutomaticMediaDownloadCategories { + switch type { + case .contact: + return settings.peers.contacts + case .otherPrivate: + return settings.peers.otherPrivate + case .group: + return settings.peers.groups + case .channel: + return settings.peers.channels } } private func categoryForPeerAndMedia(settings: AutomaticMediaDownloadSettings, peer: Peer, media: Media) -> (AutomaticMediaDownloadCategory, Int32?)? { - let categories = categoriesForPeer(peer, settings: settings) + let categories = categoriesForPeerType(tempPeerTypeForPeer(peer), settings: settings) if let _ = media as? TelegramMediaImage { return (categories.photo, nil) } else if let file = media as? TelegramMediaFile { @@ -194,7 +214,11 @@ private func categoryForPeerAndMedia(settings: AutomaticMediaDownloadSettings, p if flags.contains(.instantRoundVideo) { return (categories.videoMessage, file.size.flatMap(Int32.init)) } else { - return (categories.video, file.size.flatMap(Int32.init)) + if file.isAnimated { + return (categories.videoMessage, file.size.flatMap(Int32.init)) + } else { + return (categories.video, file.size.flatMap(Int32.init)) + } } case let .Audio(isVoice, _, _, _, _): if isVoice { diff --git a/TelegramUI/ChannelAdminController.swift b/TelegramUI/ChannelAdminController.swift index ff877c33c3..4f74f54839 100644 --- a/TelegramUI/ChannelAdminController.swift +++ b/TelegramUI/ChannelAdminController.swift @@ -329,7 +329,7 @@ private func canEditAdminRights(accountPeerId: PeerId, channelView: PeerView, in if let adminInfo = adminInfo { return adminInfo.canBeEditedByAccountPeer || adminInfo.promotedBy == accountPeerId } else { - return false + return channel.hasAdminRights(.canAddAdmins) } } } else { @@ -361,6 +361,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s .canPostMessages, .canEditMessages, .canDeleteMessages, + .canInviteUsers, .canAddAdmins ] case .group: diff --git a/TelegramUI/ChannelAdminsController.swift b/TelegramUI/ChannelAdminsController.swift index 392c79b54e..a4095d5150 100644 --- a/TelegramUI/ChannelAdminsController.swift +++ b/TelegramUI/ChannelAdminsController.swift @@ -194,7 +194,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { } case let .adminPeerItem(_, _, _, index, _, _, _): switch rhs { - case .recentActions, .administrationType, .administrationInfo, .adminsHeader: + case .recentActions, .administrationType, .administrationInfo, .adminsHeader, .addAdmin: return false case let .adminPeerItem(_, _, _, rhsIndex, _, _, _): return index < rhsIndex @@ -203,7 +203,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { } case .addAdmin: switch rhs { - case .recentActions, .administrationType, .administrationInfo, .adminsHeader, .adminPeerItem: + case .recentActions, .administrationType, .administrationInfo, .adminsHeader, .addAdmin: return false default: return true @@ -385,6 +385,10 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, if let participants = participants { entries.append(.adminsHeader(presentationData.theme, isGroup ? presentationData.strings.ChannelMembers_GroupAdminsTitle : presentationData.strings.ChannelMembers_ChannelAdminsTitle)) + if peer.hasAdminRights(.canAddAdmins) { + entries.append(.addAdmin(presentationData.theme, presentationData.strings.Channel_Management_AddModerator, state.editing)) + } + var combinedParticipants: [RenderedChannelParticipant] = participants var existingParticipantIds = Set() for participant in participants { @@ -439,7 +443,6 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, } if peer.hasAdminRights(.canAddAdmins) { - entries.append(.addAdmin(presentationData.theme, presentationData.strings.Channel_Management_AddModerator, state.editing)) entries.append(.adminsInfo(presentationData.theme, presentationData.strings.Channel_Management_AddModeratorHelp)) } } @@ -544,7 +547,7 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon } })) }, addAdmin: { - presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, openPeer: { peer, participant in + presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, excludeAccountPeer: true, openPeer: { peer, participant in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } if peer.id == account.peerId { return diff --git a/TelegramUI/ChannelBlacklistController.swift b/TelegramUI/ChannelBlacklistController.swift index c4d6dd17ed..795d904338 100644 --- a/TelegramUI/ChannelBlacklistController.swift +++ b/TelegramUI/ChannelBlacklistController.swift @@ -316,7 +316,7 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View } } }, addPeer: { - presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, openPeer: { peer, participant in + presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, excludeAccountPeer: true, openPeer: { peer, participant in if let participant = participant { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } switch participant.participant { @@ -329,8 +329,19 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View } } } - presentControllerImpl?(channelBannedMemberController(account: account, peerId: peerId, memberId: peer.id, initialParticipant: participant?.participant, updated: { _ in - }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let _ = (account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { channel in + guard let channel = channel as? TelegramChannel else { + return + } + if case .broadcast = channel.info { + removePeerDisposable.set((account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: account, peerId: peerId, memberId: peer.id, bannedRights: TelegramChannelBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) + |> deliverOnMainQueue).start()) + } else { + presentControllerImpl?(channelBannedMemberController(account: account, peerId: peerId, memberId: peer.id, initialParticipant: participant?.participant, updated: { _ in + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + }) }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, removePeer: { memberId in updateState { diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index 94d75ad33a..ff1dbeffce 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -583,7 +583,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr hasPhotos = true } - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: hasPhotos, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: hasPhotos, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false)! let _ = currentAvatarMixin.swap(mixin) mixin.didFinishWithImage = { image in if let image = image { @@ -594,7 +594,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr updateState { $0.withUpdatedUpdatingAvatar(.image(representation)) } - updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, resource: resource) |> deliverOnMainQueue).start(next: { result in + updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, photo: uploadedPeerPhoto(account: account, resource: resource)) |> deliverOnMainQueue).start(next: { result in switch result { case .complete: updateState { @@ -616,7 +616,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr return $0.withUpdatedUpdatingAvatar(.none) } } - updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, resource: nil) |> deliverOnMainQueue).start(next: { result in + updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, photo: nil) |> deliverOnMainQueue).start(next: { result in switch result { case .complete: updateState { @@ -732,7 +732,9 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr }, openBanned: { pushControllerImpl?(channelBlacklistController(account: account, peerId: peerId)) }, reportChannel: { - + presentControllerImpl?(peerReportOptionsController(account: account, subject: .peer(peerId), present: { c, a in + presentControllerImpl?(c, a) + }), nil) }, leaveChannel: { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationTheme: presentationData.theme) @@ -741,7 +743,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr } controller.setItemGroups([ ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Channel_LeaveChannel, action: { + ActionSheetButtonItem(title: presentationData.strings.Channel_LeaveChannel, color: .destructive, action: { let _ = removePeerChat(postbox: account.postbox, peerId: peerId, reportChatSpam: false).start() dismissAction() popToRootControllerImpl?() @@ -751,7 +753,25 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, deleteChannel: { - + 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.ChannelInfo_DeleteChannelConfirmation), + ActionSheetButtonItem(title: presentationData.strings.ChannelInfo_DeleteChannel, color: .destructive, action: { + actionsDisposable.add((deleteChannel(account: account, peerId: peerId) + |> deliverOnMainQueue).start(completed: { + popToRootControllerImpl?() + })) + dismissAction() + }), + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, displayAddressNameContextMenu: { text in let shareController = ShareController(account: account, subject: .url(text)) presentControllerImpl?(shareController, nil) diff --git a/TelegramUI/ChannelMembersSearchContainerNode.swift b/TelegramUI/ChannelMembersSearchContainerNode.swift index 2dd3e20ae9..3d1c8973f3 100644 --- a/TelegramUI/ChannelMembersSearchContainerNode.swift +++ b/TelegramUI/ChannelMembersSearchContainerNode.swift @@ -166,7 +166,6 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod case .searchMembers, .banAndPromoteActions: foundGroupMembers = Signal { subscriber in let (disposable, listControl) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.recent(postbox: account.postbox, network: account.network, peerId: peerId, searchQuery: query, updated: { state in - // FIXME: remove and test list expansion bug if case .ready = state.loadingState { subscriber.putNext(state.list) subscriber.putCompletion() @@ -198,6 +197,12 @@ final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNod var entries: [ChannelMembersSearchEntry] = [] var existingPeerIds = Set() + switch mode { + case .inviteActions, .banAndPromoteActions: + existingPeerIds.insert(account.peerId) + case .searchMembers: + break + } var index = 0 diff --git a/TelegramUI/ChannelMembersSearchController.swift b/TelegramUI/ChannelMembersSearchController.swift index b9d5c8b708..1c6b9524f9 100644 --- a/TelegramUI/ChannelMembersSearchController.swift +++ b/TelegramUI/ChannelMembersSearchController.swift @@ -9,6 +9,7 @@ final class ChannelMembersSearchController: ViewController { private let account: Account private let peerId: PeerId + private let excludeAccountPeer: Bool private let openPeer: (Peer, RenderedChannelParticipant?) -> Void private var presentationData: PresentationData @@ -19,9 +20,10 @@ final class ChannelMembersSearchController: ViewController { return self.displayNode as! ChannelMembersSearchControllerNode } - init(account: Account, peerId: PeerId, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void) { + init(account: Account, peerId: PeerId, excludeAccountPeer: Bool, openPeer: @escaping (Peer, RenderedChannelParticipant?) -> Void) { self.account = account self.peerId = peerId + self.excludeAccountPeer = excludeAccountPeer self.openPeer = openPeer self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -45,7 +47,7 @@ final class ChannelMembersSearchController: ViewController { } override func loadDisplayNode() { - self.displayNode = ChannelMembersSearchControllerNode(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings, peerId: peerId) + self.displayNode = ChannelMembersSearchControllerNode(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings, peerId: self.peerId, excludeAccountPeer: self.excludeAccountPeer) self.controllerNode.navigationBar = self.navigationBar self.controllerNode.requestActivateSearch = { [weak self] in self?.activateSearch() diff --git a/TelegramUI/ChannelMembersSearchControllerNode.swift b/TelegramUI/ChannelMembersSearchControllerNode.swift index 050d4c3323..0e5ad4b53e 100644 --- a/TelegramUI/ChannelMembersSearchControllerNode.swift +++ b/TelegramUI/ChannelMembersSearchControllerNode.swift @@ -101,6 +101,7 @@ private func preparedTransition(from fromEntries: [ChannelMembersSearchEntry]?, class ChannelMembersSearchControllerNode: ASDisplayNode { private let account: Account private let peerId: PeerId + private let excludeAccountPeer: Bool let listNode: ListView var navigationBar: NavigationBar? @@ -120,10 +121,11 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { private var disposable: Disposable? private var listControl: PeerChannelMemberCategoryControl? - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerId: PeerId) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerId: PeerId, excludeAccountPeer: Bool) { self.account = account self.listNode = ListView() self.peerId = peerId + self.excludeAccountPeer = excludeAccountPeer self.themeAndStrings = (theme, strings) @@ -153,6 +155,11 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { var index = 0 for participant in state.list { + if excludeAccountPeer { + if participant.peer.id == account.peerId { + continue + } + } entries.append(.peer(index, participant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false))) index += 1 } diff --git a/TelegramUI/ChatBotStartInputPanelNode.swift b/TelegramUI/ChatBotStartInputPanelNode.swift index 3cc128485f..d987628632 100644 --- a/TelegramUI/ChatBotStartInputPanelNode.swift +++ b/TelegramUI/ChatBotStartInputPanelNode.swift @@ -101,10 +101,10 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { let indicatorSize = self.activityIndicator.bounds.size self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) - return 47.0 + return 45.0 } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return 47.0 + return 45.0 } } diff --git a/TelegramUI/ChatButtonKeyboardInputNode.swift b/TelegramUI/ChatButtonKeyboardInputNode.swift index efac3122ed..1af6b4154e 100644 --- a/TelegramUI/ChatButtonKeyboardInputNode.swift +++ b/TelegramUI/ChatButtonKeyboardInputNode.swift @@ -64,7 +64,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))) if self.theme !== interfaceState.theme { @@ -115,6 +115,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { buttonNode.updateTheme(theme: interfaceState.theme) } else { buttonNode = ChatButtonKeyboardInputButtonNode(theme: interfaceState.theme) + buttonNode.titleNode.maximumNumberOfLines = 2 buttonNode.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: [.touchUpInside]) self.scrollNode.addSubnode(buttonNode) self.buttonNodes.append(buttonNode) @@ -122,7 +123,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { buttonIndex += 1 if buttonNode.button != button { buttonNode.button = button - buttonNode.setTitle(button.title, with: Font.regular(16.0), with: interfaceState.theme.chat.inputButtonPanel.buttonTextColor, for: []) + buttonNode.setAttributedTitle(NSAttributedString(string: button.title, font: Font.regular(16.0), textColor: interfaceState.theme.chat.inputButtonPanel.buttonTextColor, paragraphAlignment: .center), for: []) } buttonNode.frame = CGRect(origin: CGPoint(x: sideInset + CGFloat(columnIndex) * (buttonWidth + columnSpacing), y: verticalOffset), size: CGSize(width: buttonWidth, height: buttonHeight)) columnIndex += 1 diff --git a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift index 6b5caee32a..b7977478f5 100644 --- a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift +++ b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift @@ -137,10 +137,10 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { let indicatorSize = self.activityIndicator.bounds.size self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) - return 47.0 + return 45.0 } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return 47.0 + return 45.0 } } diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 2e48c491e9..b23baefa62 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -96,6 +96,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin private let editMessageDisposable = MetaDisposable() private let enqueueMediaMessageDisposable = MetaDisposable() private var resolvePeerByNameDisposable: MetaDisposable? + private var shareStatusDisposable: MetaDisposable? private let editingMessage = ValuePromise(nil, ignoreRepeated: true) private let startingBot = ValuePromise(false, ignoreRepeated: true) @@ -444,6 +445,15 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin })) } } + }, activateSwitchInline: { [weak self] peerId, inputString in + guard let strongSelf = self else { + return + } + if let botStart = strongSelf.botStart, case let .automatic(returnToPeerId) = botStart.behavior { + strongSelf.openPeer(peerId: returnToPeerId, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), messageId: nil), fromMessage: nil) + } else { + strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), messageId: nil), fromMessage: nil) + } }, openUrl: { [weak self] url in if let strongSelf = self { strongSelf.openUrl(url) @@ -479,7 +489,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin })]), in: .window(.root)) } }, sendBotCommand: { [weak self] messageId, command in - if let strongSelf = self { + if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}) var postAsReply = false if !command.contains("@") { @@ -686,19 +696,21 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.present(actionSheet, in: .window(.root)) case let .command(command): let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: command), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { [weak actionSheet] in + var items: [ActionSheetItem] = [] + items.append(ActionSheetTextItem(title: command)) + if canSendMessagesToChat(strongSelf.presentationInterfaceState) { + 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)]) } - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = command - }) - ]), ActionSheetItemGroup(items: [ + })) + } + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = command + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) @@ -773,6 +785,9 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) } + }, cancelInteractiveKeyboardGestures: { [weak self] in + (self?.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() + self?.chatDisplayNode.cancelInteractiveKeyboardGestures() }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings) self.controllerInteraction = controllerInteraction @@ -1143,6 +1158,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.editMessageDisposable.dispose() self.enqueueMediaMessageDisposable.dispose() self.resolvePeerByNameDisposable?.dispose() + self.shareStatusDisposable?.dispose() self.botCallbackAlertMessageDisposable?.dispose() for (_, info) in self.contextQueryStates { info.1.dispose() @@ -1167,6 +1183,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.networkStateDisposable?.dispose() self.screenCaptureEventsDisposable?.dispose() self.chatAdditionalDataDisposable.dispose() + self.shareStatusDisposable?.dispose() } public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { @@ -1217,7 +1234,17 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin pinnedMessage = cachedDataMessages[pinnedMessageId] } } - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedInterfaceState({ _ in return interfaceState }).updatedKeyboardButtonsMessage(combinedInitialData.buttonKeyboardMessage).updatedPinnedMessageId(pinnedMessageId).updatedPinnedMessage(pinnedMessage).updatedPeerIsBlocked(peerIsBlocked).updatedCanReportPeer(canReport).updatedTitlePanelContext({ context in + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { updated in + var updated = updated + + updated = updated.updatedInterfaceState({ _ in return interfaceState }) + + updated = updated.updatedKeyboardButtonsMessage(combinedInitialData.buttonKeyboardMessage) + updated = updated.updatedPinnedMessageId(pinnedMessageId) + updated = updated.updatedPinnedMessage(pinnedMessage) + updated = updated.updatedPeerIsBlocked(peerIsBlocked) + updated = updated.updatedCanReportPeer(canReport) + updated = updated.updatedTitlePanelContext({ context in if pinnedMessageId != nil { if !context.contains(where: { switch $0 { @@ -1250,6 +1277,10 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } } }) + if let editMessage = interfaceState.editMessage, let message = combinedInitialData.initialData?.associatedMessages[editMessage.messageId] { + updated = updatedChatEditInterfaceMessagetState(state: updated, message: message) + } + return updated }) } if let readStateData = combinedInitialData.readStateData { @@ -1477,12 +1508,26 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self?.updateChatPresentationInterfaceState(animated: animated, interactive: true, { $0.updatedInterfaceState(f) }) } - self.chatDisplayNode.requestUpdateInterfaceState = { [weak self] animated, f in - self?.updateChatPresentationInterfaceState(animated: animated, interactive: true, f) + self.chatDisplayNode.requestUpdateInterfaceState = { [weak self] transition, interactive, f in + self?.updateChatPresentationInterfaceState(transition: transition, interactive: interactive, f) } self.chatDisplayNode.displayAttachmentMenu = { [weak self] in - self?.presentAttachmentMenu(editingMessage: false) + guard let strongSelf = self else { + return + } + if case .peer = strongSelf.chatLocation, let messageId = strongSelf.presentationInterfaceState.interfaceState.editMessage?.messageId { + let _ = (strongSelf.account.postbox.transaction { transaction -> Message? in + return transaction.getMessage(messageId) + } |> deliverOnMainQueue).start(next: { message in + guard let strongSelf = self else { + return + } + strongSelf.presentAttachmentMenu(editingMessage: true) + }) + } else { + strongSelf.presentAttachmentMenu(editingMessage: false) + } } let postbox = self.account.postbox @@ -1592,15 +1637,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } return $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(message.text, entities: entities)), disableUrlPreview: nil)) } - var hasOriginalMedia = false - for media in message.media { - if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - updated = updated.updatedEditingUrlPreview((content.url, webpage)) - } else if media is TelegramMediaImage || media is TelegramMediaFile { - hasOriginalMedia = true - } - } - updated = updated.updatedEditMessageState(ChatEditInterfaceMessageState(hasOriginalMedia: hasOriginalMedia, media: nil)) + + updated = updatedChatEditInterfaceMessagetState(state: updated, message: message) updated = updated.updatedInputMode({ _ in return .text }) @@ -1609,25 +1647,6 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }) } } - }, setupEditMessageMedia: { [weak self] in - if let strongSelf = self, case .peer = strongSelf.chatLocation, let messageId = strongSelf.presentationInterfaceState.interfaceState.editMessage?.messageId { - let _ = (strongSelf.account.postbox.transaction { transaction -> Message? in - return transaction.getMessage(messageId) - } |> deliverOnMainQueue).start(next: { message in - guard let strongSelf = self, let message = message else { - return - } - /*var isFile = false - for media in message.media { - if let file = media as? TelegramMediaFile { - if !(file.isVideo || file.isInstantVideo) { - isFile = true - } - } - }*/ - strongSelf.presentAttachmentMenu(editingMessage: true) - }) - } }, beginMessageSelection: { [weak self] messageIds in if let strongSelf = self, strongSelf.isNodeLoaded { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true,{ $0.updatedInterfaceState { $0.withUpdatedSelectedMessages(messageIds) } }) @@ -1646,6 +1665,12 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin })) } } + }, reportSelectedMessages: { [weak self] in + if let strongSelf = self, let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { + strongSelf.present(peerReportOptionsController(account: strongSelf.account, subject: .messages(Array(messageIds).sorted()), present: { c, a in + self?.present(c, in: .window(.root), with: a) + }), in: .window(.root)) + } }, deleteMessages: { [weak self] messages in if let strongSelf = self, !messages.isEmpty { let messageIds = Set(messages.map { $0.id }) @@ -1663,95 +1688,13 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if let strongSelf = self { if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { let forwardMessageIds = Array(forwardMessageIdsSet).sorted() - - let controller = PeerSelectionController(account: strongSelf.account) - controller.peerSelected = { [weak controller] peerId in - if let strongSelf = self, let strongController = controller { - if case .peer(peerId) = strongSelf.chatLocation { - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds).withoutSelectionState() }) }) - strongController.dismiss() - } else if peerId == strongSelf.account.peerId { - let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: forwardMessageIds.map { id -> EnqueueMessage in - return .forward(source: id, grouping: .auto) - }).start() - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) - strongController.dismiss() - } else { - let _ = (strongSelf.account.postbox.transaction({ transaction -> Void in - transaction.updatePeerChatInterfaceState(peerId, update: { currentState in - if let currentState = currentState as? ChatInterfaceState { - return currentState.withUpdatedForwardMessageIds(forwardMessageIds) - } else { - return ChatInterfaceState().withUpdatedForwardMessageIds(forwardMessageIds) - } - }) - }) |> deliverOnMainQueue).start(completed: { - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) - - let ready = ValuePromise() - - strongSelf.controllerNavigationDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in - if let strongController = controller { - strongController.dismiss() - } - })) - - (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready) - } - }) - } - } - } - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(controller, in: .window(.root)) + strongSelf.forwardMessages(messageIds: forwardMessageIds) } } }, forwardMessages: { [weak self] messages in if let strongSelf = self, !messages.isEmpty { let forwardMessageIds = messages.map { $0.id }.sorted() - - let controller = PeerSelectionController(account: strongSelf.account) - controller.peerSelected = { [weak controller] peerId in - if let strongSelf = self, let strongController = controller { - if case .peer(peerId) = strongSelf.chatLocation { - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds).withoutSelectionState() }) }) - strongController.dismiss() - } else if peerId == strongSelf.account.peerId { - let _ = enqueueMessages(account: strongSelf.account, peerId: peerId, messages: forwardMessageIds.map { id -> EnqueueMessage in - return .forward(source: id, grouping: .auto) - }).start() - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) - strongController.dismiss() - } else { - let _ = (strongSelf.account.postbox.transaction({ transaction -> Void in - transaction.updatePeerChatInterfaceState(peerId, update: { currentState in - if let currentState = currentState as? ChatInterfaceState { - return currentState.withUpdatedForwardMessageIds(forwardMessageIds) - } else { - return ChatInterfaceState().withUpdatedForwardMessageIds(forwardMessageIds) - } - }) - }) |> deliverOnMainQueue).start(completed: { - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) - - let ready = ValuePromise() - - strongSelf.controllerNavigationDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in - if let strongController = controller { - strongController.dismiss() - } - })) - - (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready) - } - }) - } - } - } - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(controller, in: .window(.root)) + strongSelf.forwardMessages(messageIds: forwardMessageIds) } }, shareSelectedMessages: { [weak self] in if let strongSelf = self, let selectedIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { @@ -1814,20 +1757,29 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin , entities: entitiesAttribute, disableUrlPreview: disableUrlPreview) |> deliverOnMainQueue |> afterDisposed({ editingMessage.set(nil) })).start(next: { result in - if let strongSelf = self { - switch result { - case let .progress(value): - editingMessage.set(value) - case .done: - editingMessage.set(nil) - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - var state = state - state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) }) - state = state.updatedEditMessageState(nil) - return state - }) - } + guard let strongSelf = self else { + return } + switch result { + case let .progress(value): + editingMessage.set(value) + case .done: + editingMessage.set(nil) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + var state = state + state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) }) + state = state.updatedEditMessageState(nil) + return state + }) + } + }, error: { _ in + guard let strongSelf = self else { + return + } + + editingMessage.set(nil) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Channel_EditMessageErrorGeneric, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) })) } }, beginMessageSearch: { [weak self] domain, query in @@ -1950,7 +1902,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }, sendContextResult: { [weak self] results, result in self?.enqueueChatContextResult(results, result) }, sendBotCommand: { [weak self] botPeer, command in - if let strongSelf = self { + if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let addressName = botPeer.addressName { let messageText: String if peer is TelegramUser { @@ -1975,7 +1927,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } } }, sendBotStart: { [weak self] payload in - if let strongSelf = self { + if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { strongSelf.startBot(payload) } }, botSwitchChatWithPayload: { [weak self] peerId, payload in @@ -2126,7 +2078,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } } }, sendSticker: { [weak self] file in - if let strongSelf = self { + if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { @@ -2593,6 +2545,10 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } func updateChatPresentationInterfaceState(animated: Bool = true, interactive: Bool, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) { + self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, interactive: interactive, f) + } + + func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, interactive: Bool, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) { var temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState) if self.presentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup { @@ -2761,7 +2717,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } } - let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.hasOriginalMedia ?? false + let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.content != .plaintext let editingUrlPreviewText: String? = isEditingMedia ? nil : updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText.string if let (updatedEditingUrlPreviewUrl, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, account: self.account, currentQuery: self.editingUrlPreviewQueryState?.0) { self.editingUrlPreviewQueryState?.1.dispose() @@ -2817,33 +2773,33 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.presentationInterfaceState = updatedChatPresentationInterfaceState if self.isNodeLoaded { - self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, animated: animated, interactive: interactive) + self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, transition: transition, interactive: interactive) } if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { if self.leftNavigationButton != button { - self.navigationItem.setLeftBarButton(button.buttonItem, animated: animated) + self.navigationItem.setLeftBarButton(button.buttonItem, animated: transition.isAnimated) self.leftNavigationButton = button } } else if let _ = self.leftNavigationButton { - self.navigationItem.setLeftBarButton(nil, animated: animated) + self.navigationItem.setLeftBarButton(nil, animated: transition.isAnimated) self.leftNavigationButton = nil } if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) { if self.rightNavigationButton != button { - self.navigationItem.setRightBarButton(button.buttonItem, animated: animated) + self.navigationItem.setRightBarButton(button.buttonItem, animated: transition.isAnimated) self.rightNavigationButton = button } } else if let _ = self.rightNavigationButton { - self.navigationItem.setRightBarButton(nil, animated: animated) + self.navigationItem.setRightBarButton(nil, animated: transition.isAnimated) self.rightNavigationButton = nil } if let controllerInteraction = self.controllerInteraction { if updatedChatPresentationInterfaceState.interfaceState.selectionState != controllerInteraction.selectionState { controllerInteraction.selectionState = updatedChatPresentationInterfaceState.interfaceState.selectionState - self.updateItemNodesSelectionStates(animated: animated) + self.updateItemNodesSelectionStates(animated: transition.isAnimated) } } @@ -2936,7 +2892,17 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if let message = messages.first, case let .message(desc) = message, let media = desc.media { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in var state = state - state = state.updatedEditMessageState(ChatEditInterfaceMessageState(hasOriginalMedia: true, media: media)) + if let editMessageState = state.editMessageState, case .media(true) = editMessageState.content { + state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, media: media)) + } + if !desc.text.isEmpty { + state = state.updatedInterfaceState { state in + if let editMessage = state.editMessage { + return state.withUpdatedEditMessage(ChatEditMessageState(messageId: editMessage.messageId, inputState: ChatTextInputState(inputText: NSAttributedString(string: desc.text)), disableUrlPreview: editMessage.disableUrlPreview)) + } + return state + } + } return state }) } @@ -2947,7 +2913,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin return } - let _ = (legacyAssetPickerEnqueueMessages(account: self.account, peerId: peerId, signals: signals) + let _ = (legacyAssetPickerEnqueueMessages(account: self.account, signals: signals) |> deliverOnMainQueue).start(next: { [weak self] messages in self?.editMessageMediaWithMessages(messages) }) @@ -3002,7 +2968,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin legacyController.bind(controller: navigationController) legacyController.enableSizeClassSignal = true - let controller = legacyAttachmentMenu(account: strongSelf.account, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, openGallery: { + let controller = legacyAttachmentMenu(account: strongSelf.account, peer: peer, editingMessage: editingMessage, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, openGallery: { self?.presentMediaPicker(fileMode: false, completion: { signals in if editingMessage { self?.editMessageMediaWithLegacySignals(signals) @@ -3279,7 +3245,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin private func enqueueMediaMessages(signals: [Any]?) { if case let .peer(peerId) = self.chatLocation { - self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(account: self.account, peerId: peerId, signals: signals!) |> deliverOnMainQueue).start(next: { [weak self] messages in + self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(account: self.account, signals: signals!) |> deliverOnMainQueue).start(next: { [weak self] messages in if let strongSelf = self { let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ @@ -3296,7 +3262,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult) { - if let message = outgoingMessageWithChatContextResult(results, result) { + if let message = outgoingMessageWithChatContextResult(results, result), canSendMessagesToChat(self.presentationInterfaceState) { let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { @@ -3729,6 +3695,80 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } } + private func forwardMessages(messageIds: [MessageId]) { + let controller = PeerSelectionController(account: self.account, filter: .onlyWriteable) + controller.peerSelected = { [weak self, weak controller] peerId in + guard let strongSelf = self, let strongController = controller else { + return + } + + if case .peer(peerId) = strongSelf.chatLocation { + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(messageIds).withoutSelectionState() }) }) + strongController.dismiss() + } else if peerId == strongSelf.account.peerId { + let _ = (enqueueMessages(account: strongSelf.account, peerId: peerId, messages: messageIds.map { id -> EnqueueMessage in + return .forward(source: id, grouping: .auto) + }) + |> deliverOnMainQueue).start(next: { messageIds in + if let strongSelf = self { + let signals: [Signal] = messageIds.compactMap({ id -> Signal? in + guard let id = id else { + return nil + } + return strongSelf.account.pendingMessageManager.pendingMessageStatus(id) + |> mapToSignal { status -> Signal in + if status != nil { + return .never() + } else { + return .single(true) + } + } + |> take(1) + }) + if strongSelf.shareStatusDisposable == nil { + strongSelf.shareStatusDisposable = MetaDisposable() + } + strongSelf.shareStatusDisposable?.set((combineLatest(signals) + |> deliverOnMainQueue).start(completed: { + guard let strongSelf = self else { + return + } + strongSelf.present(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .success), in: .window(.root)) + })) + } + }) + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + strongController.dismiss() + } else { + let _ = (strongSelf.account.postbox.transaction({ transaction -> Void in + transaction.updatePeerChatInterfaceState(peerId, update: { currentState in + if let currentState = currentState as? ChatInterfaceState { + return currentState.withUpdatedForwardMessageIds(messageIds) + } else { + return ChatInterfaceState().withUpdatedForwardMessageIds(messageIds) + } + }) + }) |> deliverOnMainQueue).start(completed: { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + + let ready = ValuePromise() + + strongSelf.controllerNavigationDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in + if let strongController = controller { + strongController.dismiss() + } + })) + + (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready) + } + }) + } + } + self.chatDisplayNode.dismissInput() + self.present(controller, in: .window(.root)) + } + private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: Message?) { if case let .peer(currentPeerId) = self.chatLocation, peerId == currentPeerId { switch navigation { diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 69ca530e63..2db50c1129 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -48,6 +48,7 @@ public final class ChatControllerInteraction { let sendSticker: (TelegramMediaFile) -> Void let sendGif: (TelegramMediaFile) -> Void let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool) -> Void + let activateSwitchInline: (PeerId?, String) -> Void let openUrl: (String) -> Void let shareCurrentLocation: () -> Void let shareAccountContact: () -> Void @@ -68,6 +69,7 @@ public final class ChatControllerInteraction { let canSetupReply: (Message) -> Bool let requestMessageUpdate: (MessageId) -> Void + let cancelInteractiveKeyboardGestures: () -> Void var hiddenMedia: [MessageId: [Media]] = [:] var selectionState: ChatInterfaceSelectionState? @@ -75,7 +77,7 @@ public final class ChatControllerInteraction { var contextHighlightedState: ChatInterfaceHighlightedState? var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings - init(openMessage: @escaping (Message) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { + init(openMessage: @escaping (Message) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention @@ -87,6 +89,7 @@ public final class ChatControllerInteraction { self.sendSticker = sendSticker self.sendGif = sendGif self.requestMessageActionCallback = requestMessageActionCallback + self.activateSwitchInline = activateSwitchInline self.openUrl = openUrl self.shareCurrentLocation = shareCurrentLocation self.shareAccountContact = shareAccountContact @@ -107,6 +110,7 @@ public final class ChatControllerInteraction { self.canSetupReply = canSetupReply self.requestMessageUpdate = requestMessageUpdate + self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures self.automaticMediaDownloadSettings = automaticMediaDownloadSettings } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index f6592fbd0f..c3b3adb2c8 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -31,6 +31,11 @@ private final class ScrollContainerNode: ASScrollNode { } } +private struct ChatControllerNodeDerivedLayoutState { + var inputNodeHeight: CGFloat? + var upperInputPositionBound: CGFloat? +} + class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let account: Account let chatLocation: ChatLocation @@ -104,7 +109,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } var requestUpdateChatInterfaceState: (Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _, _ in } - var requestUpdateInterfaceState: (Bool, (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void = { _, _ in } + var requestUpdateInterfaceState: (ContainedViewLayoutTransition, Bool, (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void = { _, _, _ in } var sendMessages: ([EnqueueMessage]) -> Void = { _ in } var displayAttachmentMenu: () -> Void = { } var displayPasteMenu: ([UIImage]) -> Void = { _ in } @@ -130,6 +135,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var scheduledLayoutTransitionRequestId: Int = 0 private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? + private var panRecognizer: WindowPanRecognizer? + private let keyboardGestureRecognizerDelegate = WindowKeyboardGestureRecognizerDelegate() + private var upperInputPositionBound: CGFloat? + private var keyboardGestureBeginLocation: CGPoint? + private var keyboardGestureAccessoryHeight: CGFloat? + + private var derivedLayoutState: ChatControllerNodeDerivedLayoutState? + private var isLoading: Bool = false { didSet { if self.isLoading != oldValue { @@ -302,6 +315,36 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + override func didLoad() { + super.didLoad() + + let recognizer = WindowPanRecognizer(target: nil, action: nil) + recognizer.cancelsTouchesInView = false + recognizer.delaysTouchesBegan = false + recognizer.delaysTouchesEnded = false + recognizer.delegate = self.keyboardGestureRecognizerDelegate + recognizer.began = { [weak self] point in + guard let strongSelf = self else { + return + } + strongSelf.panGestureBegan(location: point) + } + recognizer.moved = { [weak self] point in + guard let strongSelf = self else { + return + } + strongSelf.panGestureMoved(location: point) + } + recognizer.ended = { [weak self] point, velocity in + guard let strongSelf = self else { + return + } + strongSelf.panGestureEnded(location: point, velocity: velocity) + } + self.panRecognizer = recognizer + self.view.addGestureRecognizer(recognizer) + } + private func updateIsEmpty(_ isEmpty: Bool, animated: Bool) { if isEmpty && self.emptyNode == nil { let emptyNode = ChatEmptyNode() @@ -410,12 +453,27 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var dismissedInputByDragging = false if let (validLayout, _) = self.validLayout { - var wasDragging = false + var wasDraggingKeyboard = false if validLayout.inputHeight != nil && validLayout.inputHeightIsInteractivellyChanging { - wasDragging = true + wasDraggingKeyboard = true } - if wasDragging { + var wasDraggingInputNode = false + if let derivedLayoutState = self.derivedLayoutState, let inputNodeHeight = derivedLayoutState.inputNodeHeight, !inputNodeHeight.isZero, let upperInputPositionBound = derivedLayoutState.upperInputPositionBound { + let normalizedHeight = max(0.0, layout.size.height - upperInputPositionBound) + if normalizedHeight < inputNodeHeight { + wasDraggingInputNode = true + } + } + if wasDraggingKeyboard || wasDraggingInputNode { + var isDraggingKeyboard = wasDraggingKeyboard if layout.inputHeight == 0.0 && validLayout.inputHeightIsInteractivellyChanging && !layout.inputHeightIsInteractivellyChanging { + isDraggingKeyboard = false + } + var isDraggingInputNode = false + if self.upperInputPositionBound != nil { + isDraggingInputNode = true + } + if !isDraggingKeyboard && !isDraggingInputNode { dismissedInputByDragging = true } } @@ -506,17 +564,26 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.insertSubnode(inputNode, aboveSubnode: self.inputPanelBackgroundNode) } } - inputNodeHeightAndOverflow = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelNodeBaseHeight, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + inputNodeHeightAndOverflow = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelNodeBaseHeight, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) } else if let inputNode = self.inputNode { dismissedInputNode = inputNode self.inputNode = nil } + var effectiveInputNodeHeight: CGFloat? + if let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { + if let upperInputPositionBound = self.upperInputPositionBound { + effectiveInputNodeHeight = min(layout.size.height - max(0.0, upperInputPositionBound), inputNodeHeightAndOverflow.0) + } else { + effectiveInputNodeHeight = inputNodeHeightAndOverflow.0 + } + } + var insets: UIEdgeInsets var bottomOverflowOffset: CGFloat = 0.0 - if let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { + if let effectiveInputNodeHeight = effectiveInputNodeHeight, let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { insets = layout.insets(options: []) - insets.bottom = max(inputNodeHeightAndOverflow.0, insets.bottom) + insets.bottom = max(effectiveInputNodeHeight, insets.bottom) bottomOverflowOffset = inputNodeHeightAndOverflow.1 } else { insets = layout.insets(options: [.input]) @@ -571,7 +638,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } if let inputMediaNode = self.inputMediaNode, inputMediaNode != self.inputNode { - let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelSize?.height ?? 0.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelSize?.height ?? 0.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 56.0))) @@ -977,8 +1044,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - if let inputNode = self.inputNode, let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { - let inputNodeHeight = inputNodeHeightAndOverflow.0 + inputNodeHeightAndOverflow.1 + if let inputNode = self.inputNode, let effectiveInputNodeHeight = effectiveInputNodeHeight, let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { + let inputNodeHeight = effectiveInputNodeHeight + inputNodeHeightAndOverflow.1 let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - inputNodeHeight), size: CGSize(width: layout.size.width, height: inputNodeHeight)) if immediatelyLayoutInputNodeAndAnimateAppearance { var adjustedForPreviousInputHeightFrame = inputNodeFrame @@ -1141,6 +1208,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } self.performAnimateInAsOverlay(from: scheduledAnimateInAsOverlayFromNode, transition: animatedTransition) } + + self.derivedLayoutState = ChatControllerNodeDerivedLayoutState(inputNodeHeight: inputNodeHeightAndOverflow?.0, upperInputPositionBound: inputNodeHeightAndOverflow?.0 != nil ? self.upperInputPositionBound : nil) } private func chatPresentationInterfaceStateRequiresInputFocus(_ state: ChatPresentationInterfaceState) -> Bool { @@ -1156,7 +1225,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - func updateChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, animated: Bool, interactive: Bool) { + func updateChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, transition: ContainedViewLayoutTransition, interactive: Bool) { self.selectedMessages = chatPresentationInterfaceState.interfaceState.selectionState?.selectedIds if let textInputPanelNode = self.textInputPanelNode { @@ -1180,9 +1249,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { - textInputPanelNode.updateInputTextState(chatPresentationInterfaceState.interfaceState.effectiveInputState, keepSendButtonEnabled: keepSendButtonEnabled, extendedSearchLayout: extendedSearchLayout, animated: animated) + textInputPanelNode.updateInputTextState(chatPresentationInterfaceState.interfaceState.effectiveInputState, keepSendButtonEnabled: keepSendButtonEnabled, extendedSearchLayout: extendedSearchLayout, animated: transition.isAnimated) } else { - textInputPanelNode?.updateKeepSendButtonEnabled(keepSendButtonEnabled: keepSendButtonEnabled, extendedSearchLayout: extendedSearchLayout, animated: animated) + textInputPanelNode?.updateKeepSendButtonEnabled(keepSendButtonEnabled: keepSendButtonEnabled, extendedSearchLayout: extendedSearchLayout, animated: transition.isAnimated) } if let peer = chatPresentationInterfaceState.renderedPeer?.peer, let restrictionText = peer.restrictionText { @@ -1201,7 +1270,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.navigateButtons.isHidden = false } - let layoutTransition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate + let layoutTransition: ContainedViewLayoutTransition = transition if updatedInputFocus { if !self.ignoreUpdateHeight { @@ -1294,7 +1363,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { func loadInputPanels(theme: PresentationTheme, strings: PresentationStrings) { if self.inputMediaNode == nil { - let inputNode = ChatMediaInputNode(account: self.account, controllerInteraction: self.controllerInteraction, theme: theme, strings: strings, gifPaneIsActiveUpdated: { [weak self] value in + var peerId: PeerId? + if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation { + peerId = id + } + let inputNode = ChatMediaInputNode(account: self.account, peerId: peerId, controllerInteraction: self.controllerInteraction, theme: theme, strings: strings, gifPaneIsActiveUpdated: { [weak self] value in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId { state in if case let .media(_, expanded) = state.inputMode { @@ -1312,7 +1385,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputNode.interfaceInteraction = interfaceInteraction self.inputMediaNode = inputNode if let (validLayout, _) = self.validLayout { - let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, maximumHeight: validLayout.standardInputHeight, inputPanelHeight: 44.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, inputHeight: validLayout.inputHeight ?? 0.0, maximumHeight: validLayout.standardInputHeight, inputPanelHeight: 44.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } } } @@ -1633,4 +1706,101 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { }) } } + + private func updateLayoutInternal(transition: ContainedViewLayoutTransition) { + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop in + self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop) + }) + } + } + + private func panGestureBegan(location: CGPoint) { + guard let derivedLayoutState = self.derivedLayoutState, let (validLayout, _) = self.validLayout else { + return + } + if self.upperInputPositionBound != nil { + return + } + if let inputHeight = validLayout.inputHeight { + if !inputHeight.isZero { + return + } + } + + let keyboardGestureBeginLocation = location + let accessoryHeight = self.getWindowInputAccessoryHeight() + if let inputHeight = derivedLayoutState.inputNodeHeight, !inputHeight.isZero, keyboardGestureBeginLocation.y < validLayout.size.height - inputHeight - accessoryHeight { + var enableGesture = true + if let view = self.view.hitTest(location, with: nil) { + if doesViewTreeDisableInteractiveTransitionGestureRecognizer(view) { + enableGesture = false + } + } + if enableGesture { + self.keyboardGestureBeginLocation = keyboardGestureBeginLocation + self.keyboardGestureAccessoryHeight = accessoryHeight + } + } + } + + private func panGestureMoved(location: CGPoint) { + if let keyboardGestureBeginLocation = self.keyboardGestureBeginLocation { + let currentLocation = location + let deltaY = keyboardGestureBeginLocation.y - location.y + if deltaY * deltaY >= 3.0 * 3.0 || self.upperInputPositionBound != nil { + self.upperInputPositionBound = currentLocation.y + (self.keyboardGestureAccessoryHeight ?? 0.0) + self.updateLayoutInternal(transition: .immediate) + } + } + } + + private func panGestureEnded(location: CGPoint, velocity: CGPoint?) { + guard let derivedLayoutState = self.derivedLayoutState, let (validLayout, _) = self.validLayout else { + return + } + if self.keyboardGestureBeginLocation == nil { + return + } + + self.keyboardGestureBeginLocation = nil + let currentLocation = location + + let accessoryHeight = (self.keyboardGestureAccessoryHeight ?? 0.0) + + var canDismiss = false + if let upperInputPositionBound = self.upperInputPositionBound, upperInputPositionBound >= validLayout.size.height - accessoryHeight { + canDismiss = true + } else if let velocity = velocity, velocity.y > 100.0 { + canDismiss = true + } + + if canDismiss, let inputHeight = derivedLayoutState.inputNodeHeight, currentLocation.y + (self.keyboardGestureAccessoryHeight ?? 0.0) > validLayout.size.height - inputHeight { + self.upperInputPositionBound = nil + self.requestUpdateInterfaceState(.animated(duration: 0.25, curve: .spring), true, { state in + if case .none = state.inputMode { + return state + } + return state.updatedInputMode { _ in + return .none + } + }) + } else { + self.upperInputPositionBound = nil + self.updateLayoutInternal(transition: .animated(duration: 0.25, curve: .spring)) + } + } + + func cancelInteractiveKeyboardGestures() { + self.panRecognizer?.isEnabled = false + self.panRecognizer?.isEnabled = true + + if self.upperInputPositionBound != nil { + self.updateLayoutInternal(transition: .animated(duration: 0.25, curve: .spring)) + } + + if self.keyboardGestureBeginLocation != nil { + self.keyboardGestureBeginLocation = nil + } + } } diff --git a/TelegramUI/ChatControllerTitlePanelNodeContainer.swift b/TelegramUI/ChatControllerTitlePanelNodeContainer.swift index c588f5a525..862d4ceb09 100644 --- a/TelegramUI/ChatControllerTitlePanelNodeContainer.swift +++ b/TelegramUI/ChatControllerTitlePanelNodeContainer.swift @@ -5,10 +5,12 @@ final class ChatControllerTitlePanelNodeContainer: ASDisplayNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.bounds.contains(point) { var foundHit = false - for subnode in self.subnodes { - if subnode.frame.contains(point) { - foundHit = true - break + if let subnodes = self.subnodes { + for subnode in subnodes { + if subnode.frame.contains(point) { + foundHit = true + break + } } } if !foundHit { diff --git a/TelegramUI/ChatEditInterfaceMessageState.swift b/TelegramUI/ChatEditInterfaceMessageState.swift index ec2ee86c3f..ac6c2cc26d 100644 --- a/TelegramUI/ChatEditInterfaceMessageState.swift +++ b/TelegramUI/ChatEditInterfaceMessageState.swift @@ -2,17 +2,22 @@ import Foundation import Postbox import TelegramCore +enum ChatEditInterfaceMessageStateContent: Equatable { + case plaintext + case media(editable: Bool) +} + final class ChatEditInterfaceMessageState: Equatable { - let hasOriginalMedia: Bool + let content: ChatEditInterfaceMessageStateContent let media: Media? - init(hasOriginalMedia: Bool, media: Media?) { - self.hasOriginalMedia = hasOriginalMedia + init(content: ChatEditInterfaceMessageStateContent, media: Media?) { + self.content = content self.media = media } static func ==(lhs: ChatEditInterfaceMessageState, rhs: ChatEditInterfaceMessageState) -> Bool { - if lhs.hasOriginalMedia != rhs.hasOriginalMedia { + if lhs.content != rhs.content { return false } if let lhsMedia = lhs.media, let rhsMedia = rhs.media { diff --git a/TelegramUI/ChatFeedNavigationInputPanelNode.swift b/TelegramUI/ChatFeedNavigationInputPanelNode.swift index 73e1f77433..4dfc53cf20 100644 --- a/TelegramUI/ChatFeedNavigationInputPanelNode.swift +++ b/TelegramUI/ChatFeedNavigationInputPanelNode.swift @@ -62,11 +62,11 @@ final class ChatFeedNavigationInputPanelNode: ChatInputPanelNode { self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) - return 47.0 + return 45.0 } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return 47.0 + return 45.0 } } diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index 067d09b7eb..265ebd28d0 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -140,14 +140,14 @@ private func maxMessageIndexForEntries(_ entries: [ChatHistoryEntry], indexRange return (nil, overall) } -private func mappedInsertEntries(account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(account: Account, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { case let .MessageEntry(message, presentationData, read, _, selection, isAdmin): let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, isAdmin: isAdmin)) + item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, isAdmin: isAdmin)) case let .list(search, _): item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: message, selection: selection, displayHeader: search) } @@ -156,7 +156,7 @@ private func mappedInsertEntries(account: Account, chatLocation: ChatLocation, c let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, content: .group(messages: messages)) + item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages)) case let .list(search, _): assertionFailure() item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: messages[0].0, selection: .none, displayHeader: search) @@ -185,14 +185,14 @@ private func mappedInsertEntries(account: Account, chatLocation: ChatLocation, c } } -private func mappedUpdateEntries(account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(account: Account, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .MessageEntry(message, presentationData, read, _, selection, isAdmin): let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, isAdmin: isAdmin)) + item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, isAdmin: isAdmin)) case let .list(search, _): item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: message, selection: selection, displayHeader: search) } @@ -201,7 +201,7 @@ private func mappedUpdateEntries(account: Account, chatLocation: ChatLocation, c let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, content: .group(messages: messages)) + item = ChatMessageItem(presentationData: presentationData, account: account, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages)) case let .list(search, _): assertionFailure() item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: messages[0].0, selection: .none, displayHeader: search) @@ -230,8 +230,8 @@ private func mappedUpdateEntries(account: Account, chatLocation: ChatLocation, c } } -private func mappedChatHistoryViewListTransition(account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { - return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, animateIn: transition.animateIn) +private func mappedChatHistoryViewListTransition(account: Account, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { + return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, animateIn: transition.animateIn) } private final class ChatHistoryTransactionOpaqueState { @@ -242,6 +242,35 @@ private final class ChatHistoryTransactionOpaqueState { } } +private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView) -> ChatMessageItemAssociatedData { + var automaticMediaDownloadPeerType: AutomaticMediaDownloadPeerType = .channel + if case let .peer(peerId) = chatLocation { + if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { + var isContact = false + for entry in view.additionalData { + if case let .peerIsContact(_, value) = entry { + isContact = value + break + } + } + automaticMediaDownloadPeerType = isContact ? .contact : .otherPrivate + } else if peerId.namespace == Namespaces.Peer.CloudGroup { + automaticMediaDownloadPeerType = .group + } else if peerId.namespace == Namespaces.Peer.CloudChannel { + for entry in view.additionalData { + if case let .peer(_, value) = entry { + if let channel = value as? TelegramChannel, case .group = channel.info { + automaticMediaDownloadPeerType = .channel + } + break + } + } + } + } + let associatedData = ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType) + return associatedData +} + public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let account: Account private let chatLocation: ChatLocation @@ -362,21 +391,25 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { additionalData.append(.peerNotificationSettings(peerId)) if peerId.namespace == Namespaces.Peer.CloudChannel { additionalData.append(.cacheEntry(cachedChannelAdminIdsEntryId(peerId: peerId))) + additionalData.append(.peer(peerId)) + } + if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { + additionalData.append(.peerIsContact(peerId)) } } additionalData.append(.totalUnreadState) let historyViewUpdate = self.chatHistoryLocation - |> distinctUntilChanged - |> mapToSignal { location in - return chatHistoryViewForLocation(location, account: account, chatLocation: chatLocation, fixedCombinedReadStates: fixedCombinedReadStates.with { $0 }, tagMask: tagMask, additionalData: additionalData) |> beforeNext { viewUpdate in - switch viewUpdate { - case let .HistoryView(view, _, _, _, _): - let _ = fixedCombinedReadStates.swap(view.combinedReadStates) - default: - break - } + |> distinctUntilChanged + |> mapToSignal { location in + return chatHistoryViewForLocation(location, account: account, chatLocation: chatLocation, fixedCombinedReadStates: fixedCombinedReadStates.with { $0 }, tagMask: tagMask, additionalData: additionalData) |> beforeNext { viewUpdate in + switch viewUpdate { + case let .HistoryView(view, _, _, _, _): + let _ = fixedCombinedReadStates.swap(view.combinedReadStates) + default: + break } + } } let previousView = Atomic(value: nil) @@ -462,7 +495,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData) |> map({ mappedChatHistoryViewListTransition(account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view) + + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData) |> map({ mappedChatHistoryViewListTransition(account: account, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -527,9 +562,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex) var readIndexRange = (0, historyView.filteredEntries.count - 1 - visible.firstIndex) - if !visible.firstIndexFullyVisible { + /*if !visible.firstIndexFullyVisible { readIndexRange.1 -= 1 - } + }*/ var messageIdsWithViewCount: [MessageId] = [] var messageIdsWithUnseenPersonalMention: [MessageId] = [] @@ -537,7 +572,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { switch historyView.filteredEntries[i] { case let .MessageEntry(message, _, _, _, _, _): var hasUnconsumedMention = false - var hasUnsonsumedContent = false + var hasUnconsumedContent = false if message.tags.contains(.unseenPersonalMessage) { for attribute in message.attributes { if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.pending { @@ -551,16 +586,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { messageIdsWithViewCount.append(message.id) } } else if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed { - hasUnsonsumedContent = true + hasUnconsumedContent = true } } - if hasUnconsumedMention && !hasUnsonsumedContent { + if hasUnconsumedMention && !hasUnconsumedContent { messageIdsWithUnseenPersonalMention.append(message.id) } case let .MessageGroupEntry(_, messages, _): for (message, _, _, _) in messages { var hasUnconsumedMention = false - var hasUnsonsumedContent = false + var hasUnconsumedContent = false if message.tags.contains(.unseenPersonalMessage) { for attribute in message.attributes { if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.pending { @@ -574,10 +609,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { messageIdsWithViewCount.append(message.id) } } else if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed { - hasUnsonsumedContent = true + hasUnconsumedContent = true } } - if hasUnconsumedMention && !hasUnsonsumedContent { + if hasUnconsumedMention && !hasUnconsumedContent { messageIdsWithUnseenPersonalMention.append(message.id) } } @@ -892,9 +927,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if let visible = visibleRange.visibleRange { var visibleFirstIndex = visible.firstIndex - if !visible.firstIndexFullyVisible { + /*if !visible.firstIndexFullyVisible { visibleFirstIndex += 1 - } + }*/ if visibleFirstIndex <= visible.lastIndex { let (messageIndex, _) = maxMessageIndexForEntries(transition.historyView.filteredEntries, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visibleFirstIndex)) if let messageIndex = messageIndex { @@ -1070,6 +1105,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { func requestMessageUpdate(_ id: MessageId) { if let historyView = self.historyView { + let associatedData = extractAssociatedData(chatLocation: self.chatLocation, view: historyView.originalView) + loop: for i in 0 ..< historyView.filteredEntries.count { switch historyView.filteredEntries[i] { case let .MessageEntry(message, presentationData, read, _, selection, isAdmin): @@ -1078,7 +1115,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let item: ListViewItem switch self.mode { case .bubbles: - item = ChatMessageItem(presentationData: presentationData, account: self.account, chatLocation: self.chatLocation, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, isAdmin: isAdmin)) + item = ChatMessageItem(presentationData: presentationData, account: self.account, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, isAdmin: isAdmin)) case let .list(search, _): item = ListMessageItem(theme: presentationData.theme, strings: presentationData.strings, account: self.account, chatLocation: self.chatLocation, controllerInteraction: self.controllerInteraction, message: message, selection: selection, displayHeader: search) } diff --git a/TelegramUI/ChatHistoryNavigationButtons.swift b/TelegramUI/ChatHistoryNavigationButtons.swift index 20b80a73e8..c5219fc55f 100644 --- a/TelegramUI/ChatHistoryNavigationButtons.swift +++ b/TelegramUI/ChatHistoryNavigationButtons.swift @@ -105,9 +105,11 @@ final class ChatHistoryNavigationButtons: ASDisplayNode { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - for subnode in self.subnodes { - if let result = subnode.hitTest(point.offsetBy(dx: -subnode.frame.minX, dy: -subnode.frame.minY), with: event) { - return result + if let subnodes = self.subnodes { + for subnode in subnodes { + if let result = subnode.hitTest(point.offsetBy(dx: -subnode.frame.minX, dy: -subnode.frame.minY), with: event) { + return result + } } } return nil diff --git a/TelegramUI/ChatInputNode.swift b/TelegramUI/ChatInputNode.swift index 2e42c8d8ae..c3db7a0cf1 100644 --- a/TelegramUI/ChatInputNode.swift +++ b/TelegramUI/ChatInputNode.swift @@ -5,7 +5,7 @@ import AsyncDisplayKit class ChatInputNode: ASDisplayNode { var interfaceInteraction: ChatPanelInterfaceInteraction? - func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { return (0.0, 0.0) } } diff --git a/TelegramUI/ChatInterfaceInputContextPanels.swift b/TelegramUI/ChatInterfaceInputContextPanels.swift index 182d1dfb49..123434da89 100644 --- a/TelegramUI/ChatInterfaceInputContextPanels.swift +++ b/TelegramUI/ChatInterfaceInputContextPanels.swift @@ -1,20 +1,33 @@ import Foundation import TelegramCore -private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult) -> Int { +/* + case stickers([FoundStickerItem]) + case hashtags([String]) + case mentions([Peer]) + case commands([PeerCommand]) + case emojis([(String, String)]) + case contextRequestResult(Peer?, ChatContextResultCollection?) + */ + +private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult) -> (Int, Bool) { switch result { - case .stickers: - return 0 - case .hashtags: - return 1 - case .mentions: - return 2 - case .commands: - return 3 - case .contextRequestResult: - return 4 - case .emojis: - return 5 + case let .stickers(items): + return (0, !items.isEmpty) + case let .hashtags(items): + return (1, !items.isEmpty) + case let .mentions(items): + return (2, !items.isEmpty) + case let .commands(items): + return (3, !items.isEmpty) + case let .contextRequestResult(_, result): + var nonEmpty = false + if let result = result, !result.results.isEmpty { + nonEmpty = true + } + return (4, nonEmpty) + case let .emojis(items): + return (5, !items.isEmpty) } } @@ -23,8 +36,18 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa return nil } - guard let inputQueryResult = chatPresentationInterfaceState.inputQueryResults.values.sorted(by: { - inputQueryResultPriority($0) < inputQueryResultPriority($1) + guard let inputQueryResult = chatPresentationInterfaceState.inputQueryResults.values.sorted(by: { lhs, rhs in + + let (lhsP, lhsHasItems) = inputQueryResultPriority(lhs) + let (rhsP, rhsHasItems) = inputQueryResultPriority(rhs) + if lhsHasItems != rhsHasItems { + if lhsHasItems { + return true + } else { + return false + } + } + return lhsP < rhsP }).first else { return nil } diff --git a/TelegramUI/ChatInterfaceInputNodes.swift b/TelegramUI/ChatInterfaceInputNodes.swift index a9bf4c2826..419025cb45 100644 --- a/TelegramUI/ChatInterfaceInputNodes.swift +++ b/TelegramUI/ChatInterfaceInputNodes.swift @@ -1,6 +1,7 @@ import Foundation import AsyncDisplayKit import TelegramCore +import Postbox func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, inputMediaNode: ChatMediaInputNode?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?) -> ChatInputNode? { if !(inputPanelNode is ChatTextInputPanelNode) { @@ -13,7 +14,11 @@ func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: } else if let inputMediaNode = inputMediaNode { return inputMediaNode } else { - let inputNode = ChatMediaInputNode(account: account, controllerInteraction: controllerInteraction, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, gifPaneIsActiveUpdated: { [weak interfaceInteraction] value in + var peerId: PeerId? + if case let .peer(id) = chatPresentationInterfaceState.chatLocation { + peerId = id + } + let inputNode = ChatMediaInputNode(account: account, peerId: peerId, controllerInteraction: controllerInteraction, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, gifPaneIsActiveUpdated: { [weak interfaceInteraction] value in if let interfaceInteraction = interfaceInteraction { interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId { state in if case let .media(_, expanded) = state.inputMode { diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index a5f44dfb6c..2a28bbe77f 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -431,6 +431,14 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { let mediaRecordingMode: ChatTextInputMediaRecordingButtonMode let silentPosting: Bool + var associatedMessageIds: [MessageId] { + var ids: [MessageId] = [] + if let editMessage = self.editMessage { + ids.append(editMessage.messageId) + } + return ids + } + var chatListEmbeddedState: PeerChatListEmbeddedInterfaceState? { if self.composeInputState.inputText.length != 0 && self.timestamp != 0 { return ChatEmbeddedInterfaceState(timestamp: self.timestamp, text: self.composeInputState.inputText) diff --git a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift index c139ed83b9..e998c8cc67 100644 --- a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift +++ b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift @@ -33,6 +33,17 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS panelNode.interfaceInteraction = interfaceInteraction return panelNode } + } else if let urlPreview = chatPresentationInterfaceState.urlPreview, chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != urlPreview.0 { + if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode { + previewPanelNode.interfaceInteraction = interfaceInteraction + previewPanelNode.replaceWebpage(url: urlPreview.0, webpage: urlPreview.1) + previewPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + return previewPanelNode + } else { + let panelNode = WebpagePreviewAccessoryPanelNode(account: account, url: urlPreview.0, webpage: urlPreview.1, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) + panelNode.interfaceInteraction = interfaceInteraction + return panelNode + } } else if let forwardMessageIds = chatPresentationInterfaceState.interfaceState.forwardMessageIds { if let forwardPanelNode = currentPanel as? ForwardAccessoryPanelNode, forwardPanelNode.messageIds == forwardMessageIds { forwardPanelNode.interfaceInteraction = interfaceInteraction @@ -53,17 +64,6 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS panelNode.interfaceInteraction = interfaceInteraction return panelNode } - } else if let urlPreview = chatPresentationInterfaceState.urlPreview, chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != urlPreview.0 { - if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode { - previewPanelNode.interfaceInteraction = interfaceInteraction - previewPanelNode.replaceWebpage(url: urlPreview.0, webpage: urlPreview.1) - previewPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) - return previewPanelNode - } else { - let panelNode = WebpagePreviewAccessoryPanelNode(account: account, url: urlPreview.0, webpage: urlPreview.1, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) - panelNode.interfaceInteraction = interfaceInteraction - return panelNode - } } else { return nil } diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index 649f943da3..2e58227623 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -64,6 +64,74 @@ enum ChatMessageContextMenuAction { case sheet(ChatMessageContextMenuSheetAction) } +func canEditMessageMedia(message: Message) -> Bool { + if message.id.peerId.namespace == Namespaces.Peer.SecretChat { + return false + } + if message.groupingKey != nil { + return false + } + for attribute in message.attributes { + if attribute is AutoremoveTimeoutMessageAttribute { + return false + } + } + for media in message.media { + if let _ = media as? TelegramMediaImage { + return true + } else if let file = media as? TelegramMediaFile { + for attribute in file.attributes { + switch attribute { + case .Sticker: + return false + case .Animated: + return true + case let .Video(video): + if video.flags.contains(.instantRoundVideo) { + return false + } else { + return true + } + case let .Audio(audio): + if audio.isVoice { + return false + } else { + return true + } + default: + break + } + } + return true + } + } + return false +} + +func updatedChatEditInterfaceMessagetState(state: ChatPresentationInterfaceState, message: Message) -> ChatPresentationInterfaceState { + var updated = state + for media in message.media { + if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + updated = updated.updatedEditingUrlPreview((content.url, webpage)) + } + } + var isPlaintext = true + for media in message.media { + if !(media is TelegramMediaWebpage) { + isPlaintext = false + break + } + } + let content: ChatEditInterfaceMessageStateContent + if isPlaintext { + content = .plaintext + } else { + content = .media(editable: canEditMessageMedia(message: message)) + } + updated = updated.updatedEditMessageState(ChatEditInterfaceMessageState(content: content, media: nil)) + return updated +} + func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, messages: [Message], interfaceInteraction: ChatPanelInterfaceInteraction?, debugStreamSingleVideo: @escaping (MessageId) -> Void) -> Signal<[ChatMessageContextMenuAction], NoError> { guard let interfaceInteraction = interfaceInteraction else { return .single([]) @@ -141,17 +209,11 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if let _ = attribute as? InlineBotMessageAttribute { hasUneditableAttributes = true break - } else if let _ = attribute as? AutoremoveTimeoutMessageAttribute { - hasUneditableAttributes = true - break } } if message.forwardInfo != nil { hasUneditableAttributes = true } - if message.groupingKey != nil { - hasUneditableAttributes = true - } for media in message.media { if let file = media as? TelegramMediaFile { @@ -344,6 +406,7 @@ struct ChatAvailableMessageActionOptions: OptionSet { static let deleteLocally = ChatAvailableMessageActionOptions(rawValue: 1 << 0) static let deleteGlobally = ChatAvailableMessageActionOptions(rawValue: 1 << 1) static let forward = ChatAvailableMessageActionOptions(rawValue: 1 << 2) + static let report = ChatAvailableMessageActionOptions(rawValue: 1 << 3) } struct ChatAvailableMessageActions { @@ -364,6 +427,9 @@ 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) { + optionsMap[id]!.insert(.report) + } if channel.hasAdminRights(.canBanUsers), case .group = channel.info { if message.flags.contains(.Incoming) { if !hadBanPeerId { @@ -390,6 +456,9 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag } else if let group = peer as? TelegramGroup { if message.id.peerId.namespace != Namespaces.Peer.SecretChat { optionsMap[id]!.insert(.forward) + if message.flags.contains(.Incoming) { + optionsMap[id]!.insert(.report) + } } optionsMap[id]!.insert(.deleteLocally) if !message.flags.contains(.Incoming) { diff --git a/TelegramUI/ChatItemGalleryFooterContentNode.swift b/TelegramUI/ChatItemGalleryFooterContentNode.swift index 7a6e56246c..8e6e1a2d3a 100644 --- a/TelegramUI/ChatItemGalleryFooterContentNode.swift +++ b/TelegramUI/ChatItemGalleryFooterContentNode.swift @@ -187,9 +187,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { func setMessage(_ message: Message) { self.currentMessage = message + self.actionButton.isHidden = message.id.peerId.namespace == Namespaces.Peer.SecretChat + let canDelete: Bool if let peer = message.peers[message.id.peerId] { - if let _ = peer as? TelegramUser { + if peer is TelegramUser || peer is TelegramSecretChat { canDelete = true } else if let _ = peer as? TelegramGroup { canDelete = true diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index e828231f1b..53528989e0 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -494,7 +494,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } } - private func activateSearch() { + func activateSearch() { if self.displayNavigationBar { if let scrollToTop = self.scrollToTop { scrollToTop() diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 334c5112e2..b275539743 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -194,9 +194,9 @@ private func revealOptions(strings: PresentationStrings, theme: PresentationThem private func leftRevealOptions(strings: PresentationStrings, theme: PresentationTheme, isUnread: Bool) -> [ItemListRevealOption] { var options: [ItemListRevealOption] = [] if isUnread { - options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: "Read", icon: readIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.ChatList_MarkAsRead, icon: readIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor)) } else { - options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: "Unread", icon: unreadIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.ChatList_MarkAsUnread, icon: unreadIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor)) } return options } diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index f8ac89720e..75ed69d47a 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -14,6 +14,7 @@ public struct ChatListNodePeersFilter: OptionSet { public static let onlyWriteable = ChatListNodePeersFilter(rawValue: 1 << 0) public static let onlyUsers = ChatListNodePeersFilter(rawValue: 1 << 1) + public static let onlyGroups = ChatListNodePeersFilter(rawValue: 1 << 2) } enum ChatListNodeMode { @@ -156,6 +157,17 @@ private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNode enabled = false } } + if filter.contains(.onlyGroups) { + if let peer = peer.peers[peer.peerId] { + if let _ = peer as? TelegramGroup { + } else if let peer = peer as? TelegramChannel, case .group = peer.info { + } else { + enabled = false + } + } else { + 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 if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) @@ -206,6 +218,17 @@ private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNode enabled = false } } + if filter.contains(.onlyGroups) { + if let peer = peer.peers[peer.peerId] { + if let _ = peer as? TelegramGroup { + } else if let peer = peer as? TelegramChannel, case .group = peer.info { + } else { + enabled = false + } + } else { + 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 if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) @@ -374,7 +397,7 @@ final class ChatListNode: ListView { return } - let _ = (togglePeerUnreadMarkInteractively(postbox: account.postbox, peerId: peerId) + let _ = (togglePeerUnreadMarkInteractively(postbox: account.postbox, viewTracker: account.viewTracker, peerId: peerId) |> deliverOnMainQueue).start(completed: { self?.updateState { return $0.withUpdatedPeerIdWithRevealedOptions(nil) diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index 39ec26daa1..db5c0623ef 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -130,6 +130,17 @@ private enum ChatListRecentEntry: Comparable, Identifiable { enabled = false } } + if filter.contains(.onlyGroups) { + if let peer = chatPeer { + if let _ = peer as? TelegramGroup { + } else if let peer = peer as? TelegramChannel, case .group = peer.info { + } else { + enabled = false + } + } else { + enabled = false + } + } 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: { clearRecentlySearchedPeers() @@ -285,6 +296,17 @@ enum ChatListSearchEntry: Comparable, Identifiable { enabled = false } } + if filter.contains(.onlyGroups) { + if let peer = chatPeer { + if let _ = peer as? TelegramGroup { + } else if let peer = peer as? TelegramChannel, case .group = peer.info { + } else { + enabled = false + } + } else { + enabled = false + } + } 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 interaction.peerSelected(peer) @@ -299,6 +321,13 @@ enum ChatListSearchEntry: Comparable, Identifiable { enabled = false } } + if filter.contains(.onlyGroups) { + if let _ = peer.peer as? TelegramGroup { + } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + } else { + enabled = false + } + } var suffixString = "" if let subscribers = peer.subscribers, subscribers != 0 { @@ -384,6 +413,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private var validLayout: ContainerViewLayout? private let recentDisposable = MetaDisposable() + private let updatedRecentPeersDisposable = MetaDisposable() private let searchQuery = Promise() private let searchDisposable = MetaDisposable() @@ -537,10 +567,20 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { }) let previousRecentItems = Atomic<[ChatListRecentEntry]?>(value: nil) - let recentItemsTransition = combineLatest(recentlySearchedPeers(postbox: account.postbox), presentationDataPromise.get(), self.statePromise.get()) - |> mapToSignal { [weak self] peers, presentationData, state -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in + let hasRecentPeers = recentPeers(account: account) + |> map { value -> Bool in + switch value { + case let .peers(peers): + return !peers.isEmpty + case .disabled: + return false + } + } + |> distinctUntilChanged + let recentItemsTransition = combineLatest(hasRecentPeers, recentlySearchedPeers(postbox: account.postbox), presentationDataPromise.get(), self.statePromise.get()) + |> mapToSignal { [weak self] hasRecentPeers, peers, presentationData, state -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in var entries: [ChatListRecentEntry] = [] - if groupId == nil { + if groupId == nil, hasRecentPeers { entries.append(.topPeers([], presentationData.theme, presentationData.strings)) } var peerIds = Set() @@ -579,6 +619,8 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { return .single((transition, previousEntries == nil)) } + self.updatedRecentPeersDisposable.set(managedUpdatedRecentPeers(postbox: account.postbox, network: account.network).start()) + self.recentDisposable.set((recentItemsTransition |> deliverOnMainQueue).start(next: { [weak self] (transition, firstTime) in if let strongSelf = self { strongSelf.enqueueRecentTransition(transition, firstTime: firstTime) @@ -622,6 +664,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } deinit { + self.updatedRecentPeersDisposable.dispose() self.recentDisposable.dispose() self.searchDisposable.dispose() self.presentationDataDisposable?.dispose() diff --git a/TelegramUI/ChatListSearchRecentPeersNode.swift b/TelegramUI/ChatListSearchRecentPeersNode.swift index 6267e993d0..fcf8f8b6f3 100644 --- a/TelegramUI/ChatListSearchRecentPeersNode.swift +++ b/TelegramUI/ChatListSearchRecentPeersNode.swift @@ -23,6 +23,35 @@ private func calculateItemCustomWidth(width: CGFloat) -> CGFloat { return itemWidth + itemSpacing } +private struct ChatListSearchRecentPeersEntry: Comparable, Identifiable { + let index: Int + let peer: Peer + + var stableId: PeerId { + return self.peer.id + } + + static func ==(lhs: ChatListSearchRecentPeersEntry, rhs: ChatListSearchRecentPeersEntry) -> Bool { + if lhs.index != rhs.index { + return false + } + if !lhs.peer.isEqual(rhs.peer) { + return false + } + return true + } + + static func <(lhs: ChatListSearchRecentPeersEntry, rhs: ChatListSearchRecentPeersEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, mode: HorizontalPeerItemMode, peerSelected: @escaping (Peer) -> Void, peerLongTapped: @escaping (Peer) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, itemCustomWidth: CGFloat?) -> ListViewItem { + return HorizontalPeerItem(theme: theme, strings: strings, mode: mode, account: account, peer: self.peer, action: peerSelected, longTapAction: { peer in + peerLongTapped(peer) + }, isPeerSelected: isPeerSelected, customWidth: itemCustomWidth) + } +} + final class ChatListSearchRecentPeersNode: ASDisplayNode { private var theme: PresentationTheme private var strings: PresentationStrings @@ -60,22 +89,46 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { self.addSubnode(self.sectionHeaderNode) self.addSubnode(self.listView) - self.disposable.set((recentPeers(account: account) |> filter { !$0.isEmpty } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peers in + let peersDisposable = DisposableSet() + peersDisposable.add((recentPeers(account: account) + |> filter { value -> Bool in + switch value { + case let .peers(peers): + return !peers.isEmpty + case .disabled: + return true + } + } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peers in if let strongSelf = self { + var entries: [ChatListSearchRecentPeersEntry] = [] + switch peers { + case let .peers(peers): + for peer in peers { + entries.append(ChatListSearchRecentPeersEntry(index: entries.count, peer: peer)) + } + case .disabled: + break + } var items: [ListViewItem] = [] - for peer in peers { - items.append(HorizontalPeerItem(theme: strongSelf.theme, strings: strongSelf.strings, mode: mode, account: account, peer: peer, action: peerSelected, longTapAction: { peer in + for entry in entries { + items.append(entry.item(account: account, theme: strongSelf.theme, strings: strongSelf.strings, mode: mode, peerSelected: peerSelected, peerLongTapped: { peer in peerLongTapped(peer) - }, isPeerSelected: isPeerSelected, customWidth: strongSelf.itemCustomWidth)) + }, isPeerSelected: isPeerSelected, itemCustomWidth: strongSelf.itemCustomWidth)) } strongSelf.items = items strongSelf.listView.transaction(deleteIndices: [], insertIndicesAndItems: (0 ..< items.count).map({ ListViewInsertItem(index: $0, previousIndex: nil, item: items[$0], directionHint: .Down) }), updateIndicesAndItems: [], options: [], updateOpaqueState: nil) } })) + if case .actionSheet = mode { + peersDisposable.add(managedUpdatedRecentPeers(postbox: account.postbox, network: account.network).start()) + } + self.disposable.set(peersDisposable) } deinit { - disposable.dispose() + self.disposable.dispose() } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index bceeb1aec7..899d976c8f 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -5,6 +5,12 @@ import Postbox import TelegramCore import SwiftSignalKit +private struct PeerSpecificPackData { + let peer: Peer + let info: StickerPackCollectionInfo + let items: [ItemCollectionItem] +} + private struct ChatMediaInputPanelTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] @@ -37,7 +43,14 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr var animated = false switch update { case .initial: - break + for i in (0 ..< toEntries.count).reversed() { + switch toEntries[i] { + case .search: + break + case .sticker: + scrollToItem = GridNodeScrollToItem(index: i, position: .top, transition: .animated(duration: 0.0, curve: .easeInOut), directionHint: .down, adjustForSection: true, adjustForTopInset: true) + } + } case .generic: animated = true case .scroll: @@ -62,7 +75,7 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr if !fromEntries.isEmpty && fromEntries[0].index < toEntries[i].index { directionHint = .down } - scrollToItem = GridNodeScrollToItem(index: i, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: directionHint, adjustForSection: true) + scrollToItem = GridNodeScrollToItem(index: i, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: directionHint, adjustForSection: true, adjustForTopInset: true) break } } @@ -74,13 +87,13 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr if !fromEntries.isEmpty && fromEntries[0].index < toEntries[i].index { directionHint = .down } - scrollToItem = GridNodeScrollToItem(index: i, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: directionHint, adjustForSection: true) + scrollToItem = GridNodeScrollToItem(index: i, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: directionHint, adjustForSection: true, adjustForTopInset: true) break } } } if scrollToItem == nil { - scrollToItem = GridNodeScrollToItem(index: 0, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: .up, adjustForSection: true) + scrollToItem = GridNodeScrollToItem(index: 0, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: .up, adjustForSection: true, adjustForTopInset: true) } } } @@ -116,14 +129,36 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, animated: animated) } -private func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, theme: PresentationTheme) -> [ChatMediaInputPanelEntry] { +private func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, theme: PresentationTheme) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] entries.append(.recentGifs(theme)) if let savedStickers = savedStickers, !savedStickers.items.isEmpty { entries.append(.savedStickers(theme)) } + var savedStickerIds = Set() + if let savedStickers = savedStickers, !savedStickers.items.isEmpty { + for i in 0 ..< savedStickers.items.count { + if let item = savedStickers.items[i].contents as? SavedStickerItem { + savedStickerIds.insert(item.file.fileId.id) + } + } + } if let recentStickers = recentStickers, !recentStickers.items.isEmpty { - entries.append(.recentPacks(theme)) + var found = false + for item in recentStickers.items { + if let item = item.contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile, let mediaId = item.media.id { + if !savedStickerIds.contains(mediaId.id) { + found = true + break + } + } + } + if found { + entries.append(.recentPacks(theme)) + } + } + if let peerSpecificPack = peerSpecificPack { + entries.append(.peerSpecific(theme: theme, peer: peerSpecificPack.peer)) } var index = 0 for (_, info, item) in view.collectionInfos { @@ -137,7 +172,7 @@ private func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers return entries } -private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] { +private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] { var entries: [ChatMediaInputGridEntry] = [] if view.lower == nil { @@ -160,7 +195,7 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: savedStickerIds.insert(item.file.fileId.id) let index = ItemCollectionItemIndex(index: Int32(i), id: item.file.fileId.id) let stickerItem = StickerPackItem(index: index, file: item.file, indexKeys: []) - entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -2, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) + entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -3, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) } } } @@ -176,12 +211,24 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: if !savedStickerIds.contains(mediaId.id) { let index = ItemCollectionItemIndex(index: Int32(i), id: mediaId.id) let stickerItem = StickerPackItem(index: index, file: file, indexKeys: []) - entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) + entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -2, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) addedCount += 1 } } } } + + if let peerSpecificPack = peerSpecificPack { + for i in 0 ..< peerSpecificPack.items.count { + let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_GroupStickers, shortName: "", hash: 0, count: 0) + + if let item = peerSpecificPack.items[i] as? StickerPackItem { + let index = ItemCollectionItemIndex(index: Int32(i), id: item.file.fileId.id) + let stickerItem = StickerPackItem(index: index, file: item.file, indexKeys: []) + entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) + } + } + } } for entry in view.entries { @@ -228,16 +275,18 @@ final class ChatMediaInputNodeInteraction { let navigateToCollectionId: (ItemCollectionId) -> Void let openSettings: () -> Void let toggleSearch: (Bool) -> Void + let openPeerSpecificSettings: () -> Void var highlightedStickerItemCollectionId: ItemCollectionId? var highlightedItemCollectionId: ItemCollectionId? var previewedStickerPackItem: StickerPreviewPeekItem? var appearanceTransition: CGFloat = 1.0 - init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool) -> Void) { + init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool) -> Void, openPeerSpecificSettings: @escaping () -> Void) { self.navigateToCollectionId = navigateToCollectionId self.openSettings = openSettings self.toggleSearch = toggleSearch + self.openPeerSpecificSettings = openPeerSpecificSettings } } @@ -292,7 +341,6 @@ final class ChatMediaInputNode: ChatInputNode { private var inputNodeInteraction: ChatMediaInputNodeInteraction! private let collectionListPanel: ASDisplayNode - private var collectionListPanelOffset: CGFloat = 0.0 private let collectionListSeparator: ASDisplayNode private let collectionListContainer: CollectionListContainerNode @@ -312,14 +360,15 @@ final class ChatMediaInputNode: ChatInputNode { private var currentStickerPacksCollectionPosition: StickerPacksCollectionPosition? private var currentView: ItemCollectionsView? - private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState)? + private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState)? private var paneArrangement: ChatMediaInputPaneArrangement + private var initializedArrangement = false private var theme: PresentationTheme private var strings: PresentationStrings private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> - init(account: Account, controllerInteraction: ChatControllerInteraction, theme: PresentationTheme, strings: PresentationStrings, gifPaneIsActiveUpdated: @escaping (Bool) -> Void) { + init(account: Account, peerId: PeerId?, controllerInteraction: ChatControllerInteraction, theme: PresentationTheme, strings: PresentationStrings, gifPaneIsActiveUpdated: @escaping (Bool) -> Void) { self.account = account self.controllerInteraction = controllerInteraction self.theme = theme @@ -376,6 +425,10 @@ final class ChatMediaInputNode: ChatInputNode { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) + } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue { + strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) + strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) + strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) } else { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) for (id, _, _) in currentView.collectionInfos { @@ -410,6 +463,20 @@ final class ChatMediaInputNode: ChatInputNode { } } } + }, openPeerSpecificSettings: { [weak self] in + guard let peerId = peerId, peerId.namespace == Namespaces.Peer.CloudChannel else { + return + } + + let _ = (account.postbox.transaction { transaction -> StickerPackCollectionInfo? in + return (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData)?.stickerPack + } + |> deliverOnMainQueue).start(next: { info in + guard let strongSelf = self else { + return + } + strongSelf.controllerInteraction.presentController(groupStickerPackSetupController(account: account, peerId: peerId, currentPackInfo: info), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }) }) getItemIsPreviewedImpl = { [weak self] item in @@ -475,10 +542,30 @@ final class ChatMediaInputNode: ChatInputNode { let previousEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], [])) let inputNodeInteraction = self.inputNodeInteraction! + let peerSpecificPack: Signal + if let peerId = peerId { + peerSpecificPack = combineLatest(peerSpecificStickerPack(postbox: account.postbox, network: account.network, peerId: peerId), account.postbox.multiplePeersView([peerId])) + |> map { packData, peersView -> PeerSpecificPackData? in + if let peer = peersView.peers[peerId] { + if let (info, items) = packData { + return PeerSpecificPackData(peer: peer, info: info, items: items) + } + } + return nil + } + } else { + peerSpecificPack = .single(nil) + } - let transitions = combineLatest(itemCollectionsView, self.themeAndStringsPromise.get()) - |> map { viewAndUpdate, themeAndStrings -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in - let (view, update) = viewAndUpdate + let previousView = Atomic(value: nil) + let transitions = combineLatest(itemCollectionsView, peerSpecificPack, self.themeAndStringsPromise.get()) + |> map { viewAndUpdate, peerSpecificPack, themeAndStrings -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in + let (view, viewUpdate) = viewAndUpdate + let previous = previousView.swap(view) + var update = viewUpdate + if previous === view { + update = .generic + } let (theme, strings) = themeAndStrings var savedStickers: OrderedItemListView? @@ -490,8 +577,8 @@ final class ChatMediaInputNode: ChatInputNode { savedStickers = orderedView } } - let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, theme: theme) - let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, strings: strings, theme: theme) + let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack, theme: theme) + let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack, strings: strings, theme: theme) let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntries, gridEntries)) return (view, preparedChatMediaInputPanelEntryTransition(account: account, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: account, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction), previousGridEntries.isEmpty) } @@ -500,6 +587,16 @@ final class ChatMediaInputNode: ChatInputNode { if let strongSelf = self { strongSelf.currentView = view strongSelf.enqueuePanelTransition(panelTransition, firstTime: panelFirstTime, thenGridTransition: gridTransition, gridFirstTime: gridFirstTime) + if !strongSelf.initializedArrangement { + strongSelf.initializedArrangement = true + var currentPane = strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex] + if view.entries.isEmpty { + currentPane = .trending + } + if currentPane != strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex] { + strongSelf.setCurrentPane(currentPane, transition: .immediate) + } + } } })) @@ -736,8 +833,9 @@ final class ChatMediaInputNode: ChatInputNode { if let index = self.paneArrangement.panes.index(of: pane), index != self.paneArrangement.currentIndex { let previousGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs self.paneArrangement = self.paneArrangement.withIndexTransition(0.0).withCurrentIndex(index) - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, inputPanelHeight, interfaceState) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + self.updateAppearanceTransition(transition: transition) } let updatedGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs if updatedGifPanelWasActive != previousGifPanelWasActive { @@ -754,8 +852,8 @@ final class ChatMediaInputNode: ChatInputNode { self.setHighlightedItemCollectionId(ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue, id: 0)) } } else { - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, inputPanelHeight, interfaceState) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) } } } @@ -805,6 +903,12 @@ final class ChatMediaInputNode: ChatInputNode { self.listView.ensureItemNodeVisible(itemNode) ensuredNodeVisible = true } + } else if let itemNode = itemNode as? ChatMediaInputPeerSpecificItemNode { + itemNode.updateIsHighlighted() + if itemNode.currentCollectionId == collectionId { + self.listView.ensureItemNodeVisible(itemNode) + ensuredNodeVisible = true + } } } @@ -818,8 +922,45 @@ final class ChatMediaInputNode: ChatInputNode { } } + private func currentCollectionListPanelOffset() -> CGFloat { + let paneOffsets = self.paneArrangement.panes.map { pane -> CGFloat in + switch pane { + case .stickers: + return self.stickerPane.collectionListPanelOffset + case .gifs: + return self.gifPane.collectionListPanelOffset + case .trending: + return self.trendingPane.collectionListPanelOffset + } + } + + let mainOffset = paneOffsets[self.paneArrangement.currentIndex] + if self.paneArrangement.indexTransition.isZero { + return mainOffset + } else { + var sideOffset: CGFloat? + if self.paneArrangement.indexTransition < 0.0 { + if self.paneArrangement.currentIndex != 0 { + sideOffset = paneOffsets[self.paneArrangement.currentIndex - 1] + } + } else { + if self.paneArrangement.currentIndex != paneOffsets.count - 1 { + sideOffset = paneOffsets[self.paneArrangement.currentIndex + 1] + } + } + if let sideOffset = sideOffset { + let interpolator = CGFloat.interpolator() + let value = interpolator(mainOffset, sideOffset, abs(self.paneArrangement.indexTransition)) as! CGFloat + return value + } else { + return mainOffset + } + } + } + private func updateAppearanceTransition(transition: ContainedViewLayoutTransition) { - var value: CGFloat = 1.0 - abs(self.collectionListPanelOffset / 41.0) + + var value: CGFloat = 1.0 - abs(self.currentCollectionListPanelOffset() / 41.0) value = min(1.0, max(0.0, value)) self.inputNodeInteraction.appearanceTransition = max(0.1, value) transition.updateAlpha(node: self.listView, alpha: value) @@ -832,12 +973,14 @@ final class ChatMediaInputNode: ChatInputNode { itemNode.updateAppearanceTransition(transition: transition) } else if let itemNode = itemNode as? ChatMediaInputTrendingItemNode { itemNode.updateAppearanceTransition(transition: transition) + } else if let itemNode = itemNode as? ChatMediaInputPeerSpecificItemNode { + itemNode.updateAppearanceTransition(transition: transition) } } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { - self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, inputPanelHeight, interfaceState) + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { + self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState) if self.theme !== interfaceState.theme || self.strings !== interfaceState.strings { self.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings) @@ -859,12 +1002,14 @@ final class ChatMediaInputNode: ChatInputNode { panelHeight = standardInputHeight } - transition.updateFrame(node: self.collectionListContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: max(0.0, 41.0 + /*self.collectionListPanelOffset + */UIScreenPixel)))) - transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: self.collectionListPanelOffset), size: CGSize(width: width, height: 41.0))) - transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + self.collectionListPanelOffset), size: CGSize(width: width, height: separatorHeight))) + let collectionListPanelOffset = self.currentCollectionListPanelOffset() + + transition.updateFrame(node: self.collectionListContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: max(0.0, 41.0 + UIScreenPixel)))) + transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: CGSize(width: width, height: 41.0))) + transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: CGSize(width: width, height: separatorHeight))) self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width) - transition.updatePosition(node: self.listView, position: CGPoint(x: width / 2.0, y: (41.0 - self.collectionListPanelOffset) / 2.0)) + transition.updatePosition(node: self.listView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0)) var duration: Double = 0.0 var curve: UInt = 0 @@ -1019,7 +1164,7 @@ final class ChatMediaInputNode: ChatInputNode { let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: -inputPanelHeight), size: CGSize(width: width, height: panelHeight + inputPanelHeight)) if let stickerSearchContainerNode = self.stickerSearchContainerNode { transition.updateFrame(node: stickerSearchContainerNode, frame: containerFrame) - stickerSearchContainerNode.updateLayout(size: containerFrame.size, transition: transition) + stickerSearchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: transition) } else { let stickerSearchContainerNode = StickerPaneSearchContainerNode(account: self.account, theme: self.theme, strings: self.strings, controllerInteraction: self.controllerInteraction, inputNodeInteraction: self.inputNodeInteraction, cancel: { [weak self] in self?.stickerSearchContainerNode?.deactivate() @@ -1028,7 +1173,7 @@ final class ChatMediaInputNode: ChatInputNode { self.stickerSearchContainerNode = stickerSearchContainerNode self.addSubnode(stickerSearchContainerNode) stickerSearchContainerNode.frame = containerFrame - stickerSearchContainerNode.updateLayout(size: containerFrame.size, transition: .immediate) + stickerSearchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: .immediate) var placeholderNode: StickerPaneSearchBarPlaceholderNode? self.stickerPane.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? StickerPaneSearchBarPlaceholderNode { @@ -1101,9 +1246,23 @@ final class ChatMediaInputNode: ChatInputNode { @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: - break + if self.animatingGifPaneOut { + self.animatingGifPaneOut = false + self.gifPane.removeFromSupernode() + } + self.gifPane.layer.removeAllAnimations() + self.stickerPane.layer.removeAllAnimations() + if self.animatingStickerPaneOut { + self.animatingStickerPaneOut = false + self.stickerPane.removeFromSupernode() + } + self.trendingPane.layer.removeAllAnimations() + if self.animatingTrendingPaneOut { + self.animatingTrendingPaneOut = false + self.trendingPane.removeFromSupernode() + } case .changed: - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, inputPanelHeight, interfaceState) = self.validLayout { + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState) = self.validLayout { let translationX = -recognizer.translation(in: self.view).x var indexTransition = translationX / width if self.paneArrangement.currentIndex == 0 { @@ -1112,10 +1271,10 @@ final class ChatMediaInputNode: ChatInputNode { indexTransition = min(0.0, indexTransition) } self.paneArrangement = self.paneArrangement.withIndexTransition(indexTransition) - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState) } case .ended: - if let (width, _, _, _, _, _, _, _) = self.validLayout { + if let (width, _, _, _, _, _, _, _, _) = self.validLayout { var updatedIndex = self.paneArrangement.currentIndex if abs(self.paneArrangement.indexTransition * width) > 30.0 { if self.paneArrangement.indexTransition < 0.0 { @@ -1128,9 +1287,9 @@ final class ChatMediaInputNode: ChatInputNode { self.setCurrentPane(self.paneArrangement.panes[updatedIndex], transition: .animated(duration: 0.25, curve: .spring)) } case .cancelled: - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, inputPanelHeight, interfaceState) = self.validLayout { + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState) = self.validLayout { self.paneArrangement = self.paneArrangement.withIndexTransition(0.0) - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) } default: break @@ -1142,39 +1301,44 @@ final class ChatMediaInputNode: ChatInputNode { if let absoluteOffset = state.absoluteOffset, absoluteOffset >= 0.0 { computedAbsoluteOffset = 0.0 } else { - computedAbsoluteOffset = self.collectionListPanelOffset + state.relativeChange + computedAbsoluteOffset = pane.collectionListPanelOffset + state.relativeChange } computedAbsoluteOffset = max(-41.0, min(computedAbsoluteOffset, 0.0)) - self.collectionListPanelOffset = computedAbsoluteOffset + pane.collectionListPanelOffset = computedAbsoluteOffset if transition.isAnimated { - if self.collectionListPanelOffset < -41.0 / 2.0 { - self.collectionListPanelOffset = -41.0 + if pane.collectionListPanelOffset < -41.0 / 2.0 { + pane.collectionListPanelOffset = -41.0 } else { - self.collectionListPanelOffset = 0.0 + pane.collectionListPanelOffset = 0.0 } } + let collectionListPanelOffset = self.currentCollectionListPanelOffset() + self.updateAppearanceTransition(transition: transition) - transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: self.collectionListPanelOffset), size: self.collectionListPanel.bounds.size)) - transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + self.collectionListPanelOffset), size: self.collectionListSeparator.bounds.size)) - transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - self.collectionListPanelOffset) / 2.0)) + transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: self.collectionListPanel.bounds.size)) + transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: self.collectionListSeparator.bounds.size)) + transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0)) } private func fixPaneScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState) { if let absoluteOffset = state.absoluteOffset, absoluteOffset >= 0.0 { - self.collectionListPanelOffset = 0.0 + pane.collectionListPanelOffset = 0.0 } else { - if self.collectionListPanelOffset < -41.0 / 2.0 { - self.collectionListPanelOffset = -41.0 + if pane.collectionListPanelOffset < -41.0 / 2.0 { + pane.collectionListPanelOffset = -41.0 } else { - self.collectionListPanelOffset = 0.0 + pane.collectionListPanelOffset = 0.0 } } + + let collectionListPanelOffset = self.currentCollectionListPanelOffset() + let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .spring) self.updateAppearanceTransition(transition: transition) - transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: self.collectionListPanelOffset), size: self.collectionListPanel.bounds.size)) - transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + self.collectionListPanelOffset), size: self.collectionListSeparator.bounds.size)) - transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - self.collectionListPanelOffset) / 2.0)) + transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: self.collectionListPanel.bounds.size)) + transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: self.collectionListSeparator.bounds.size)) + transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0)) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { diff --git a/TelegramUI/ChatMediaInputPane.swift b/TelegramUI/ChatMediaInputPane.swift index 6d806a4224..c7b2f993a1 100644 --- a/TelegramUI/ChatMediaInputPane.swift +++ b/TelegramUI/ChatMediaInputPane.swift @@ -8,6 +8,8 @@ struct ChatMediaInputPaneScrollState { } class ChatMediaInputPane: ASDisplayNode { + var collectionListPanelOffset: CGFloat = 0.0 + func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { } } diff --git a/TelegramUI/ChatMediaInputPanelEntries.swift b/TelegramUI/ChatMediaInputPanelEntries.swift index d9a3a60082..4b8c6f5cdf 100644 --- a/TelegramUI/ChatMediaInputPanelEntries.swift +++ b/TelegramUI/ChatMediaInputPanelEntries.swift @@ -4,11 +4,12 @@ import SwiftSignalKit import Display enum ChatMediaInputPanelAuxiliaryNamespace: Int32 { - case recentGifs = 3 case savedStickers = 2 + case recentGifs = 3 case recentStickers = 4 - case trending = 5 - case settings = 6 + case peerSpecific = 5 + case trending = 6 + case settings = 7 } enum ChatMediaInputPanelEntryStableId: Hashable { @@ -16,6 +17,7 @@ enum ChatMediaInputPanelEntryStableId: Hashable { case savedStickers case recentPacks case stickerPack(Int64) + case peerSpecific case trending case settings @@ -45,6 +47,12 @@ enum ChatMediaInputPanelEntryStableId: Hashable { } else { return false } + case .peerSpecific: + if case .peerSpecific = rhs { + return true + } else { + return false + } case .trending: if case .trending = rhs { return true @@ -69,9 +77,11 @@ enum ChatMediaInputPanelEntryStableId: Hashable { case .recentPacks: return 2 case .trending: - return 2 + return 3 case .settings: - return 2 + return 4 + case .peerSpecific: + return 5 case let .stickerPack(id): return id.hashValue } @@ -84,6 +94,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { case recentPacks(PresentationTheme) case trending(Bool, PresentationTheme) case settings(PresentationTheme) + case peerSpecific(theme: PresentationTheme, peer: Peer) case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?, theme: PresentationTheme) var stableId: ChatMediaInputPanelEntryStableId { @@ -98,6 +109,8 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { return .trending case .settings: return .settings + case .peerSpecific: + return .peerSpecific case let .stickerPack(_, info, _, _): return .stickerPack(info.id.id) } @@ -135,6 +148,12 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { } else { return false } + case let .peerSpecific(lhsTheme, lhsPeer): + if case let .peerSpecific(rhsTheme, rhsPeer) = rhs, lhsTheme === rhsTheme, lhsPeer.isEqual(rhsPeer) { + return true + } else { + return false + } case let .stickerPack(index, info, topItem, lhsTheme): if case let .stickerPack(rhsIndex, rhsInfo, rhsTopItem, rhsTheme) = rhs, index == rhsIndex, info == rhsInfo, topItem == rhsTopItem, lhsTheme === rhsTheme { return true @@ -171,9 +190,18 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { default: return true } + case .peerSpecific: + switch rhs { + case .recentGifs, .savedStickers, recentPacks, .peerSpecific: + return false + case let .trending(elevated, _) where elevated: + return false + default: + return true + } case let .stickerPack(lhsIndex, lhsInfo, _, _): switch rhs { - case .recentGifs, .savedStickers, .recentPacks: + case .recentGifs, .savedStickers, .recentPacks, .peerSpecific: return false case let .trending(elevated, _): if elevated { @@ -236,6 +264,11 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { return ChatMediaInputSettingsItem(inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { inputNodeInteraction.openSettings() }) + case let .peerSpecific(theme, peer): + let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, id: 0) + return ChatMediaInputPeerSpecificItem(account: account, inputNodeInteraction: inputNodeInteraction, collectionId: collectionId, peer: peer, theme: theme, selected: { + inputNodeInteraction.navigateToCollectionId(collectionId) + }) case let .stickerPack(index, info, topItem, theme): return ChatMediaInputStickerPackItem(account: account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, stickerPackItem: topItem, index: index, theme: theme, selected: { inputNodeInteraction.navigateToCollectionId(info.id) diff --git a/TelegramUI/ChatMediaInputPeerSpecificItem.swift b/TelegramUI/ChatMediaInputPeerSpecificItem.swift new file mode 100644 index 0000000000..02f288bc82 --- /dev/null +++ b/TelegramUI/ChatMediaInputPeerSpecificItem.swift @@ -0,0 +1,128 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import Postbox + +final class ChatMediaInputPeerSpecificItem: ListViewItem { + let account: Account + let inputNodeInteraction: ChatMediaInputNodeInteraction + let collectionId: ItemCollectionId + let peer: Peer + let selectedItem: () -> Void + let theme: PresentationTheme + + var selectable: Bool { + return true + } + + init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, peer: Peer, theme: PresentationTheme, selected: @escaping () -> Void) { + self.account = account + self.inputNodeInteraction = inputNodeInteraction + self.collectionId = collectionId + self.peer = peer + self.selectedItem = selected + self.theme = theme + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = ChatMediaInputPeerSpecificItemNode() + node.contentSize = boundingSize + node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem) + node.inputNodeInteraction = self.inputNodeInteraction + completion(node, { + return (nil, { + node.updateItem(account: self.account, peer: self.peer, collectionId: self.collectionId, theme: self.theme) + node.updateAppearanceTransition(transition: .immediate) + }) + }) + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { + (node as? ChatMediaInputPeerSpecificItemNode)?.updateItem(account: self.account, peer: self.peer, collectionId: self.collectionId, theme: self.theme) + }) + } + + func selected(listView: ListView) { + self.selectedItem() + } +} + +private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 10.0)! +private let boundingSize = CGSize(width: 41.0, height: 41.0) +private let boundingImageSize = CGSize(width: 28.0, height: 28.0) +private let highlightSize = CGSize(width: 35.0, height: 35.0) +private let verticalOffset: CGFloat = 3.0 + +final class ChatMediaInputPeerSpecificItemNode: ListViewItemNode { + private let avatarNode: AvatarNode + private let highlightNode: ASImageNode + + var inputNodeInteraction: ChatMediaInputNodeInteraction? + var currentCollectionId: ItemCollectionId? + private var theme: PresentationTheme? + + private let stickerFetchedDisposable = MetaDisposable() + + init() { + self.highlightNode = ASImageNode() + self.highlightNode.isLayerBacked = true + self.highlightNode.isHidden = true + + self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.isLayerBacked = true + self.avatarNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + let imageSize = CGSize(width: 32.0, height: 32.0) + self.avatarNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize) + + self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.highlightNode) + self.addSubnode(self.avatarNode) + } + + deinit { + self.stickerFetchedDisposable.dispose() + } + + func updateItem(account: Account, peer: Peer, collectionId: ItemCollectionId, theme: PresentationTheme) { + self.currentCollectionId = collectionId + + if self.theme !== theme { + self.theme = theme + + self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme) + } + + self.avatarNode.setPeer(account: account, peer: peer) + } + + func updateIsHighlighted() { + assert(Queue.mainQueue().isCurrent()) + if let currentCollectionId = self.currentCollectionId, let inputNodeInteraction = self.inputNodeInteraction { + self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId + } + } + + func updateAppearanceTransition(transition: ContainedViewLayoutTransition) { + assert(Queue.mainQueue().isCurrent()) + if let inputNodeInteraction = self.inputNodeInteraction { + transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition) + } + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/TelegramUI/ChatMediaInputStickerGridItem.swift b/TelegramUI/ChatMediaInputStickerGridItem.swift index 5447c0464a..60d96fbe34 100644 --- a/TelegramUI/ChatMediaInputStickerGridItem.swift +++ b/TelegramUI/ChatMediaInputStickerGridItem.swift @@ -8,6 +8,7 @@ import Postbox final class ChatMediaInputStickerGridSection: GridSection { let collectionId: ItemCollectionId let collectionInfo: StickerPackCollectionInfo? + let interaction: ChatMediaInputNodeInteraction let theme: PresentationTheme let height: CGFloat = 26.0 @@ -15,10 +16,11 @@ final class ChatMediaInputStickerGridSection: GridSection { return self.collectionId.hashValue } - init(collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo?, theme: PresentationTheme) { + init(collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo?, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) { self.collectionId = collectionId self.collectionInfo = collectionInfo self.theme = theme + self.interaction = interaction } func isEqual(to: GridSection) -> Bool { @@ -30,7 +32,7 @@ final class ChatMediaInputStickerGridSection: GridSection { } func node() -> ASDisplayNode { - return ChatMediaInputStickerGridSectionNode(collectionInfo: self.collectionInfo, theme: self.theme) + return ChatMediaInputStickerGridSectionNode(collectionInfo: self.collectionInfo, theme: self.theme, interaction: self.interaction) } } @@ -38,17 +40,31 @@ private let sectionTitleFont = Font.medium(12.0) final class ChatMediaInputStickerGridSectionNode: ASDisplayNode { let titleNode: ASTextNode + let setupNode: HighlightableButtonNode? + let interaction: ChatMediaInputNodeInteraction - init(collectionInfo: StickerPackCollectionInfo?, theme: PresentationTheme) { + init(collectionInfo: StickerPackCollectionInfo?, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) { + self.interaction = interaction self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true + if collectionInfo?.id.namespace == ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue { + let setupNode = HighlightableButtonNode() + setupNode.setImage(PresentationResourcesChat.chatInputMediaPanelGridSetupImage(theme), for: []) + self.setupNode = setupNode + } else { + self.setupNode = nil + } + super.init() self.addSubnode(self.titleNode) self.titleNode.attributedText = NSAttributedString(string: collectionInfo?.title.uppercased() ?? "", font: sectionTitleFont, textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) self.titleNode.maximumNumberOfLines = 1 self.titleNode.truncationMode = .byTruncatingTail + + self.setupNode.flatMap(self.addSubnode) + self.setupNode?.addTarget(self, action: #selector(self.setupPressed), forControlEvents: .touchUpInside) } override func layout() { @@ -58,6 +74,14 @@ final class ChatMediaInputStickerGridSectionNode: ASDisplayNode { let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 9.0), size: titleSize) + + if let setupNode = self.setupNode { + setupNode.frame = CGRect(origin: CGPoint(x: bounds.width - 12.0 - 16.0, y: 0.0), size: CGSize(width: 16.0, height: 26.0)) + } + } + + @objc private func setupPressed() { + self.interaction.openPeerSpecificSettings() } } @@ -81,7 +105,7 @@ final class ChatMediaInputStickerGridItem: GridItem { if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue { self.section = nil } else { - self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo, theme: theme) + self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo, theme: theme, interaction: inputNodeInteraction) } } @@ -131,7 +155,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { } deinit { - stickerFetchedDisposable.dispose() + self.stickerFetchedDisposable.dispose() } override func didLoad() { diff --git a/TelegramUI/ChatMediaInputStickerPackItem.swift b/TelegramUI/ChatMediaInputStickerPackItem.swift index e3f503c403..2091cd200c 100644 --- a/TelegramUI/ChatMediaInputStickerPackItem.swift +++ b/TelegramUI/ChatMediaInputStickerPackItem.swift @@ -131,4 +131,12 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition) } } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } } diff --git a/TelegramUI/ChatMediaInputStickerPane.swift b/TelegramUI/ChatMediaInputStickerPane.swift index 81d8d63e63..ec86aaab2a 100644 --- a/TelegramUI/ChatMediaInputStickerPane.swift +++ b/TelegramUI/ChatMediaInputStickerPane.swift @@ -14,7 +14,7 @@ final class ChatMediaInputStickerPane: ChatMediaInputPane { init(theme: PresentationTheme, strings: PresentationStrings, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void) { self.gridNode = GridNode() - self.gridNode.initialOffset = 54.0 + //self.gridNode.initialOffset = 54.0 self.paneDidScroll = paneDidScroll self.fixPaneScroll = fixPaneScroll @@ -22,7 +22,7 @@ final class ChatMediaInputStickerPane: ChatMediaInputPane { self.addSubnode(self.gridNode) self.gridNode.presentationLayoutUpdated = { [weak self] layout, transition in - if let strongSelf = self, !transition.isAnimated { + if let strongSelf = self { let offset = -(layout.contentOffset.y + 41.0) var relativeChange: CGFloat = 0.0 if let didScrollPreviousOffset = strongSelf.didScrollPreviousOffset { @@ -31,7 +31,9 @@ final class ChatMediaInputStickerPane: ChatMediaInputPane { strongSelf.didScrollPreviousOffset = offset let state = ChatMediaInputPaneScrollState(absoluteOffset: offset, relativeChange: relativeChange) strongSelf.didScrollPreviousState = state - strongSelf.paneDidScroll(strongSelf, state, transition) + if !transition.isAnimated { + strongSelf.paneDidScroll(strongSelf, state, transition) + } } } self.gridNode.scrollingCompleted = { [weak self] in @@ -39,6 +41,7 @@ final class ChatMediaInputStickerPane: ChatMediaInputPane { strongSelf.fixPaneScroll(strongSelf, didScrollPreviousState) } } + self.gridNode.scrollView.alwaysBounceVertical = true } override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { diff --git a/TelegramUI/ChatMediaInputTrendingPane.swift b/TelegramUI/ChatMediaInputTrendingPane.swift index fd56d63b1c..c51e3f033a 100644 --- a/TelegramUI/ChatMediaInputTrendingPane.swift +++ b/TelegramUI/ChatMediaInputTrendingPane.swift @@ -103,6 +103,8 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { private var disposable: Disposable? private var isActivated = false + var scrollingInitiated: (() -> Void)? + init(account: Account, controllerInteraction: ChatControllerInteraction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { self.account = account self.controllerInteraction = controllerInteraction @@ -113,6 +115,10 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { super.init() self.addSubnode(self.listNode) + + self.listNode.beganInteractiveDragging = { [weak self] in + self?.scrollingInitiated?() + } } deinit { @@ -231,13 +237,12 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { if let transition = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) - let options = ListViewDeleteAndInsertOptions() + var options = ListViewDeleteAndInsertOptions() if transition.initial { //options.insert(.Synchronous) //options.insert(.LowLatency) } else { - //options.insert(.AnimateTopItemPosition) - //options.insert(.AnimateCrossfade) + options.insert(.AnimateInsertion) } self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index 9464e25066..71f3bf7d24 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -57,7 +57,11 @@ private func universalServiceMessageString(theme: PresentationTheme?, strings: P } case let .addedMembers(peerIds): if let peerId = peerIds.first, peerId == message.author?.id { - attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedChat(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, peerId)])) + if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedChannel(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, peerId)])) + } else { + attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedChat(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, peerId)])) + } } else { var attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)] if peerIds.count == 1 { @@ -67,7 +71,11 @@ private func universalServiceMessageString(theme: PresentationTheme?, strings: P } case let .removedMembers(peerIds): if peerIds.first == message.author?.id { - attributedString = addAttributesToStringWithRanges(strings.Notification_LeftChat(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + attributedString = addAttributesToStringWithRanges(strings.Notification_LeftChannel(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + } else { + attributedString = addAttributesToStringWithRanges(strings.Notification_LeftChat(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)])) + } } else { var attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)] if peerIds.count == 1 { diff --git a/TelegramUI/ChatMessageBubbleContentNode.swift b/TelegramUI/ChatMessageBubbleContentNode.swift index bb848fee47..88da21dca9 100644 --- a/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageBubbleContentNode.swift @@ -146,4 +146,8 @@ class ChatMessageBubbleContentNode: ASDisplayNode { func updateTouchesAtPoint(_ point: CGPoint?) { } + + func updateHighlightedState(animated: Bool) -> Bool { + return false + } } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 04b93d4288..dd06b2550f 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -166,9 +166,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) - for node in self.subnodes { - if node !== self.accessoryItemNode { - node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if let subnodes = self.subnodes { + for node in subnodes { + if node !== self.accessoryItemNode { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } } } @@ -1447,7 +1449,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { if let item = self.item, let author = item.content.firstMessage.author { - item.controllerInteraction.openPeer(item.effectiveAuthorId ?? author.id, .info, item.message) + let navigate: ChatControllerInteractionNavigateToPeer + if item.content.firstMessage.id.peerId == item.account.peerId { + navigate = .chat(textInputState: nil, messageId: nil) + } else { + navigate = .info + } + item.controllerInteraction.openPeer(item.effectiveAuthorId ?? author.id, navigate, item.message) } return } @@ -1591,10 +1599,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let parent = parent as? GridMessageSelectionNode, parent.bounds.contains(point) { return parent } else { - for subnode in parent.subnodes { - let subnodeFrame = subnode.frame - if let result = traceSelectionNodes(parent: subnode, point: point.offsetBy(dx: -subnodeFrame.minX, dy: -subnodeFrame.minY)) { - return result + if let parentSubnodes = parent.subnodes { + for subnode in parentSubnodes { + let subnodeFrame = subnode.frame + if let result = traceSelectionNodes(parent: subnode, point: point.offsetBy(dx: -subnodeFrame.minX, dy: -subnodeFrame.minY)) { + return result + } } } return nil @@ -1774,34 +1784,41 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { override func updateHighlightedState(animated: Bool) { super.updateHighlightedState(animated: animated) - if let item = self.item { - var highlighted = false - if let highlightedState = item.controllerInteraction.highlightedState { - for message in item.content { - if highlightedState.messageStableId == message.stableId { - highlighted = true - break - } + guard let item = self.item, let _ = self.backgroundType else { + return + } + + var highlighted = false + + for contentNode in self.contentNodes { + let _ = contentNode.updateHighlightedState(animated: animated) + } + + if let highlightedState = item.controllerInteraction.highlightedState { + for message in item.content { + if highlightedState.messageStableId == message.stableId { + highlighted = true + break } } - - if self.highlightedState != highlighted { - self.highlightedState = highlighted - if let backgroundType = self.backgroundType { - let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme) - - if highlighted { - self.backgroundNode.setType(type: backgroundType, highlighted: true, graphics: graphics, transition: .immediate) - } else { - if let previousContents = self.backgroundNode.layer.contents, animated { - self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, transition: .immediate) - - if let updatedContents = self.backgroundNode.layer.contents { - self.backgroundNode.layer.animate(from: previousContents as AnyObject, to: updatedContents as AnyObject, keyPath: "contents", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: 0.42) - } - } else { - self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, transition: .immediate) + } + + if self.highlightedState != highlighted { + self.highlightedState = highlighted + if let backgroundType = self.backgroundType { + let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme) + + if highlighted { + self.backgroundNode.setType(type: backgroundType, highlighted: true, graphics: graphics, transition: .immediate) + } else { + if let previousContents = self.backgroundNode.layer.contents, animated { + self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, transition: .immediate) + + if let updatedContents = self.backgroundNode.layer.contents { + self.backgroundNode.layer.animate(from: previousContents as AnyObject, to: updatedContents as AnyObject, keyPath: "contents", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: 0.42) } + } else { + self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, transition: .immediate) } } } @@ -1844,7 +1861,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { peerId = item.message.id.peerId } if let botPeer = botPeer, let addressName = botPeer.addressName { - item.controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: "@\(addressName) \(query)")), messageId: nil), nil) + item.controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)") } case .payment: item.controllerInteraction.openCheckoutOrReceipt(item.message.id) @@ -1875,7 +1892,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { self.swipeToReplyFeedback = HapticFeedback() self.swipeToReplyFeedback?.prepareImpact() } - (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() + self.item?.controllerInteraction.cancelInteractiveKeyboardGestures() case .changed: let translation = recognizer.translation(in: self.view) var animateReplyNodeIn = false diff --git a/TelegramUI/ChatMessageDateAndStatusNode.swift b/TelegramUI/ChatMessageDateAndStatusNode.swift index 0349ae8c1a..91ce59909e 100644 --- a/TelegramUI/ChatMessageDateAndStatusNode.swift +++ b/TelegramUI/ChatMessageDateAndStatusNode.swift @@ -226,13 +226,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } var updatedDateText = dateText - if let impressionCount = impressionCount { - updatedDateText = compactNumericCountString(impressionCount) + " " + updatedDateText - } - if edited { updatedDateText = "\(strings.Conversation_MessageEditedLabel) \(updatedDateText)" } + if let impressionCount = impressionCount { + updatedDateText = compactNumericCountString(impressionCount) + " " + updatedDateText + } let (date, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: updatedDateText, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -243,6 +242,22 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var clockPosition = CGPoint() + var impressionSize = CGSize() + var impressionWidth: CGFloat = 0.0 + if let impressionImage = impressionImage { + if currentImpressionIcon == nil { + let iconNode = ASImageNode() + iconNode.isLayerBacked = true + iconNode.displayWithoutProcessing = true + iconNode.displaysAsynchronously = false + currentImpressionIcon = iconNode + } + impressionSize = impressionImage.size + impressionWidth = impressionSize.width + 3.0 + } else { + currentImpressionIcon = nil + } + if let outgoingStatus = outgoingStatus { switch outgoingStatus { case .Sending: @@ -279,7 +294,15 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } clockPosition = CGPoint(x: leftInset + date.size.width + 8.5, y: 7.5) case let .Sent(read): - if impressionCount != nil { + let hideStatus: Bool + switch type { + case .BubbleOutgoing, .FreeOutgoing, .ImageOutgoing: + hideStatus = false + default: + hideStatus = impressionCount != nil + } + + if hideStatus { statusWidth = 0.0 checkReadNode = nil @@ -309,9 +332,9 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let checkSize = loadedCheckFullImage!.size if read { - checkReadFrame = CGRect(origin: CGPoint(x: leftInset + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0), size: checkSize) + checkReadFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0), size: checkSize) } - checkSentFrame = CGRect(origin: CGPoint(x: leftInset + date.size.width + 5.0 + statusWidth - checkSize.width - 6.0, y: 3.0), size: checkSize) + checkSentFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width - 6.0, y: 3.0), size: checkSize) } case .Failed: statusWidth = 0.0 @@ -332,7 +355,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var backgroundInsets = UIEdgeInsets() - if let backgroundImage = backgroundImage { + if let _ = backgroundImage { if currentBackgroundNode == nil { let backgroundNode = ASImageNode() backgroundNode.isLayerBacked = true @@ -343,22 +366,6 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { backgroundInsets = UIEdgeInsets(top: 2.0, left: 7.0, bottom: 2.0, right: 7.0) } - var impressionSize = CGSize() - var impressionWidth: CGFloat = 0.0 - if let impressionImage = impressionImage { - if currentImpressionIcon == nil { - let iconNode = ASImageNode() - iconNode.isLayerBacked = true - iconNode.displayWithoutProcessing = true - iconNode.displaysAsynchronously = false - currentImpressionIcon = iconNode - } - impressionSize = impressionImage.size - impressionWidth = impressionSize.width + 3.0 - } else { - currentImpressionIcon = nil - } - let layoutSize = CGSize(width: leftInset + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom) return (layoutSize, { [weak self] animated in diff --git a/TelegramUI/ChatMessageDateHeader.swift b/TelegramUI/ChatMessageDateHeader.swift index e770f39912..9c07d8e3d4 100644 --- a/TelegramUI/ChatMessageDateHeader.swift +++ b/TelegramUI/ChatMessageDateHeader.swift @@ -231,6 +231,9 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } if self.labelNode.alpha.isZero { return nil } @@ -240,6 +243,10 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { return nil } + override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) + } + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index f3a7f81a92..372625fd01 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -247,6 +247,9 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if mediaUpdated { if let image = media as? TelegramMediaImage { + if hasCurrentVideoNode { + replaceVideoNode = true + } if isSecretMedia { updateImageSignal = chatSecretPhoto(account: account, photo: image) } else { @@ -261,6 +264,9 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { chatMessagePhotoCancelInteractiveFetch(account: account, photo: image) }) } else if let image = media as? TelegramMediaWebFile { + if hasCurrentVideoNode { + replaceVideoNode = true + } updateImageSignal = chatWebFileImage(account: account, file: image) updatedFetchControls = FetchControls(fetch: { @@ -579,5 +585,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { }) } } + + func setOverlayColor(_ color: UIColor?, animated: Bool) { + self.imageNode.setOverlayColor(color, animated: animated) + } } - diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index 3455c9f7e7..ea4e471f88 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -187,10 +187,19 @@ enum ChatMessageMerge: Int32 { } } +public final class ChatMessageItemAssociatedData { + let automaticDownloadPeerType: AutomaticMediaDownloadPeerType + + init(automaticDownloadPeerType: AutomaticMediaDownloadPeerType) { + self.automaticDownloadPeerType = automaticDownloadPeerType + } +} + public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let presentationData: ChatPresentationData let account: Account let chatLocation: ChatLocation + let associatedData: ChatMessageItemAssociatedData let controllerInteraction: ChatControllerInteraction let content: ChatMessageItemContent let disableDate: Bool @@ -218,10 +227,11 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } } - public init(presentationData: ChatPresentationData, account: Account, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, content: ChatMessageItemContent, disableDate: Bool = false, additionalContent: ChatMessageItemAdditionalContent? = nil) { + public init(presentationData: ChatPresentationData, account: Account, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, content: ChatMessageItemContent, disableDate: Bool = false, additionalContent: ChatMessageItemAdditionalContent? = nil) { self.presentationData = presentationData self.account = account self.chatLocation = chatLocation + self.associatedData = associatedData self.controllerInteraction = controllerInteraction self.content = content self.disableDate = disableDate diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index 5cd9788293..a74865f610 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -13,6 +13,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { private let interactiveImageNode: ChatMessageInteractiveMediaNode private let dateAndStatusNode: ChatMessageDateAndStatusNode private var selectionNode: GridMessageSelectionNode? + private var highlightedState: Bool = false private var media: Media? @@ -277,4 +278,23 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { override func animateInsertionIntoBubble(_ duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } + + override func updateHighlightedState(animated: Bool) -> Bool { + guard let item = self.item else { + return false + } + let highlighted = item.controllerInteraction.highlightedState?.messageStableId == item.message.stableId + + if self.highlightedState != highlighted { + self.highlightedState = highlighted + + if highlighted { + self.interactiveImageNode.setOverlayColor(item.presentationData.theme.chat.bubble.mediaHighlightOverlayColor, animated: false) + } else { + self.interactiveImageNode.setOverlayColor(nil, animated: animated) + } + } + + return false + } } diff --git a/TelegramUI/ChatMessageSelectionInputPanelNode.swift b/TelegramUI/ChatMessageSelectionInputPanelNode.swift index 01fef3ac50..71b5443217 100644 --- a/TelegramUI/ChatMessageSelectionInputPanelNode.swift +++ b/TelegramUI/ChatMessageSelectionInputPanelNode.swift @@ -7,10 +7,13 @@ import SwiftSignalKit final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { private let deleteButton: UIButton + private let reportButton: UIButton private let forwardButton: UIButton private let shareButton: UIButton + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics)? private var presentationInterfaceState: ChatPresentationInterfaceState? + private var actions: ChatAvailableMessageActions? private var theme: PresentationTheme @@ -22,16 +25,19 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.forwardButton.isEnabled = self.selectedMessages.count != 0 if self.selectedMessages.isEmpty { + self.actions = nil + if let (width, leftInset, rightInset, maxHeight, metrics) = self.validLayout, let interfaceState = self.presentationInterfaceState { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, transition: .immediate, interfaceState: interfaceState, metrics: metrics) + } self.canDeleteMessagesDisposable.set(nil) - self.deleteButton.isEnabled = false - self.shareButton.isEnabled = false } else if let account = self.account { - let isEmpty = self.selectedMessages.isEmpty self.canDeleteMessagesDisposable.set((chatAvailableMessageActions(postbox: account.postbox, accountPeerId: account.peerId, messageIds: self.selectedMessages) |> deliverOnMainQueue).start(next: { [weak self] actions in if let strongSelf = self { - strongSelf.deleteButton.isEnabled = !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty - strongSelf.shareButton.isEnabled = !actions.options.intersection([.forward]).isEmpty + strongSelf.actions = actions + if let (width, leftInset, rightInset, maxHeight, metrics) = strongSelf.validLayout, let interfaceState = strongSelf.presentationInterfaceState { + let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, transition: .immediate, interfaceState: interfaceState, metrics: metrics) + } } })) } @@ -44,12 +50,16 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.deleteButton = UIButton() self.deleteButton.isEnabled = false + self.reportButton = UIButton() + self.reportButton.isEnabled = false self.forwardButton = UIButton() self.shareButton = UIButton() self.shareButton.isEnabled = false self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + self.reportButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionReport"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) + self.reportButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionReport"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat List/NavigationShare"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) @@ -58,12 +68,14 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { super.init() self.view.addSubview(self.deleteButton) + self.view.addSubview(self.reportButton) self.view.addSubview(self.forwardButton) self.view.addSubview(self.shareButton) self.forwardButton.isEnabled = false self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside]) + self.reportButton.addTarget(self, action: #selector(self.reportButtonPressed), for: [.touchUpInside]) self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), for: [.touchUpInside]) self.shareButton.addTarget(self, action: #selector(self.shareButtonPressed), for: [.touchUpInside]) } @@ -78,6 +90,8 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + self.reportButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionReport"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) + self.reportButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionReport"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) } @@ -87,6 +101,10 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.deleteSelectedMessages() } + @objc func reportButtonPressed() { + self.interfaceInteraction?.reportSelectedMessages() + } + @objc func forwardButtonPressed() { self.interfaceInteraction?.forwardSelectedMessages() } @@ -96,18 +114,75 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { } override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { + self.validLayout = (width, leftInset, rightInset, maxHeight, metrics) if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState } + if let actions = self.actions { + self.deleteButton.isEnabled = false + self.reportButton.isEnabled = false + self.forwardButton.isEnabled = true + self.shareButton.isEnabled = false + + self.deleteButton.isEnabled = !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty + self.shareButton.isEnabled = !actions.options.intersection([.forward]).isEmpty + self.reportButton.isEnabled = !actions.options.intersection([.report]).isEmpty + + self.deleteButton.isHidden = !self.deleteButton.isEnabled + self.reportButton.isHidden = !self.reportButton.isEnabled + } else { + self.deleteButton.isEnabled = false + self.deleteButton.isHidden = true + self.reportButton.isEnabled = false + self.reportButton.isHidden = true + self.forwardButton.isEnabled = false + self.shareButton.isEnabled = false + } - self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 53.0, height: 47.0)) - self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: 47.0)) - self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: 47.0)) + if self.deleteButton.isHidden && self.reportButton.isHidden { + if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = peer.info { + self.reportButton.isHidden = false + } else { + self.deleteButton.isHidden = false + } + } - return 47.0 + if self.reportButton.isHidden { + self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 57.0, height: 47.0)) + self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: 47.0)) + self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: 47.0)) + } else if !self.deleteButton.isHidden { + let buttons: [UIButton] = [ + self.deleteButton, + self.reportButton, + self.shareButton, + self.forwardButton + ] + let buttonSize = CGSize(width: 57.0, height: 47.0) + + let availableWidth = width - leftInset - rightInset + let spacing: CGFloat = floor((availableWidth - buttonSize.width * CGFloat(buttons.count)) / CGFloat(buttons.count - 1)) + var offset: CGFloat = leftInset + for i in 0 ..< buttons.count { + let button = buttons[i] + if i == buttons.count - 1 { + button.frame = CGRect(origin: CGPoint(x: width - rightInset - buttonSize.width, y: 0.0), size: buttonSize) + } else { + button.frame = CGRect(origin: CGPoint(x: offset, y: 0.0), size: buttonSize) + } + offset += buttonSize.width + spacing + } + } else { + self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 53.0, height: 47.0)) + self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: 47.0)) + self.reportButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 53.0, height: 47.0)) + self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: 47.0)) + } + + return 45.0 } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return 47.0 + return 45.0 } } diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index 392a0372f0..a5ae9e9ab4 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -444,7 +444,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { self.highlightedState = highlighted if highlighted { - self.imageNode.setOverlayColor(UIColor(white: 0.0, alpha: 0.2), animated: false) + self.imageNode.setOverlayColor(item.presentationData.theme.chat.bubble.mediaHighlightOverlayColor, animated: false) } else { self.imageNode.setOverlayColor(nil, animated: animated) } diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index 4e8c9eaa08..40fe73b8c2 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -36,16 +36,12 @@ enum ChatPanelRestrictionInfoSubject { case stickers } -struct SetupEditMessageMediaResult { - let f: (Media) -> Void -} - final class ChatPanelInterfaceInteraction { let setupReplyMessage: (MessageId) -> Void let setupEditMessage: (MessageId?) -> Void - let setupEditMessageMedia: () -> Void let beginMessageSelection: ([MessageId]) -> Void let deleteSelectedMessages: () -> Void + let reportSelectedMessages: () -> Void let deleteMessages: ([Message]) -> Void let forwardSelectedMessages: () -> Void let forwardMessages: ([Message]) -> Void @@ -91,12 +87,12 @@ final class ChatPanelInterfaceInteraction { let toggleSilentPost: () -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId?) -> Void, setupEditMessageMedia: @escaping () -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, deleteMessages: @escaping ([Message]) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId?) -> Void, beginMessageSelection: @escaping ([MessageId]) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, deleteMessages: @escaping ([Message]) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage - self.setupEditMessageMedia = setupEditMessageMedia self.beginMessageSelection = beginMessageSelection self.deleteSelectedMessages = deleteSelectedMessages + self.reportSelectedMessages = reportSelectedMessages self.deleteMessages = deleteMessages self.forwardSelectedMessages = forwardSelectedMessages self.forwardMessages = forwardMessages diff --git a/TelegramUI/ChatRecentActionsController.swift b/TelegramUI/ChatRecentActionsController.swift index 3ac1e0109d..6ec2228ae2 100644 --- a/TelegramUI/ChatRecentActionsController.swift +++ b/TelegramUI/ChatRecentActionsController.swift @@ -38,9 +38,9 @@ final class ChatRecentActionsController: ViewController { self.panelInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _ in }, setupEditMessage: { _ in - }, setupEditMessageMedia: { }, beginMessageSelection: { _ in }, deleteSelectedMessages: { + }, reportSelectedMessages: { }, deleteMessages: { _ in }, forwardSelectedMessages: { }, forwardMessages: { _ in diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index 5546545e4c..cb9e1a7b93 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -68,6 +68,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private var historyDisposable: Disposable? private let resolvePeerByNameDisposable = MetaDisposable() + private var adminsDisposable: Disposable? + private var adminsState: ChannelMemberListState? + private let banDisposables = DisposableDict() init(account: Account, peer: Peer, presentationData: PresentationData, interaction: ChatRecentActionsInteraction, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?) { self.account = account @@ -115,6 +118,11 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self.panelButtonNode.addTarget(self, action: #selector(self.infoButtonPressed), forControlEvents: .touchUpInside) + let (adminsDisposable, _) = self.account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: self.account.postbox, network: self.account.network, peerId: self.peer.id, updated: { [weak self] state in + self?.adminsState = state + }) + self.adminsDisposable = adminsDisposable + let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { return openChatMessage(account: account, message: message, standalone: true, reverseMessageGalleryOrder: false, navigationController: navigationController, dismissInput: { @@ -175,7 +183,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self?.openPeerMention(name) }, openMessageContextMenu: { [weak self] message, node, frame in self?.openMessageContextMenu(message: message, node: node, frame: frame) - }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { [weak self] url in + }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url in self?.openUrl(url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { @@ -341,6 +349,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in + }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings) self.controllerInteraction = controllerInteraction @@ -418,6 +427,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self.navigationActionDisposable.dispose() self.galleryHiddenMesageAndMediaDisposable.dispose() self.resolvePeerByNameDisposable.dispose() + self.adminsDisposable?.dispose() + self.banDisposables.dispose() } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -592,6 +603,42 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { UIPasteboard.general.string = message.text })) + if let author = message.author, let adminsState = self.adminsState { + var canBan = author.id != self.account.peerId + if let channel = self.peer as? TelegramChannel { + if !channel.hasAdminRights(.canBanUsers) { + canBan = false + } + } + for member in adminsState.list { + if member.peer.id == author.id { + switch member.participant { + case .creator: + canBan = false + case let .member(_, _, adminInfo, _): + if let adminInfo = adminInfo { + if adminInfo.promotedBy != self.account.peerId { + canBan = false + } + } + } + } + } + + if canBan { + actions.append(ContextMenuAction(content: .text(self.presentationData.strings.Conversation_ContextMenuBan), action: { [weak self] in + if let strongSelf = self { + strongSelf.banDisposables.set((fetchChannelParticipant(account: strongSelf.account, peerId: strongSelf.peer.id, participantId: author.id) + |> deliverOnMainQueue).start(next: { participant in + if let strongSelf = self { + strongSelf.presentController(channelBannedMemberController(account: strongSelf.account, peerId: strongSelf.peer.id, memberId: author.id, initialParticipant: participant, updated: { _ in }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + }), forKey: author.id) + } + })) + } + } + if !actions.isEmpty { let contextMenuController = ContextMenuController(actions: actions) diff --git a/TelegramUI/ChatRecentActionsFilterController.swift b/TelegramUI/ChatRecentActionsFilterController.swift index 32c235d758..a2017aefb6 100644 --- a/TelegramUI/ChatRecentActionsFilterController.swift +++ b/TelegramUI/ChatRecentActionsFilterController.swift @@ -363,6 +363,8 @@ public func channelRecentActionsFilterController(account: Account, peer: Peer, e let presentationDataSignal = (account.applicationContext as! TelegramApplicationContext).presentationData + let actionsDisposable = DisposableSet() + let arguments = ChatRecentActionsFilterControllerArguments(account: account, toggleAllActions: { updateState { current in if current.events.isEmpty { @@ -423,41 +425,58 @@ public func channelRecentActionsFilterController(account: Account, peer: Peer, e }) }) - let adminsSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelAdmins(account: account, peerId: peer.id) |> map { Optional($0) }) + adminsPromise.set(.single(nil)) - adminsPromise.set(adminsSignal) + let (membersDisposable, _) = account.telegramApplicationContext.peerChannelMemberCategoriesContextsManager.admins(postbox: account.postbox, network: account.network, peerId: peer.id) { membersState in + if case .loading = membersState.loadingState, membersState.list.isEmpty { + adminsPromise.set(.single(nil)) + } else { + adminsPromise.set(.single(membersState.list)) + } + } + actionsDisposable.add(membersDisposable) var previousPeers: [RenderedChannelParticipant]? let signal = combineLatest(presentationDataSignal, statePromise.get(), adminsPromise.get() |> deliverOnMainQueue) - |> deliverOnMainQueue - |> map { presentationData, state, admins -> (ItemListControllerState, (ItemListNodeState, ChatRecentActionsFilterEntry.ItemGenerationArguments)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { - dismissImpl?() - }) - - let doneEnabled = !state.events.isEmpty - - let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: { - var resultState: ChatRecentActionsFilterControllerState? - updateState { current in - resultState = current - return current - } - if let resultState = resultState { - apply(resultState.events, resultState.adminPeerIds) - } - dismissImpl?() - }) - - let previous = previousPeers - previousPeers = admins - - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChatAdmins_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: channelRecentActionsFilterControllerEntries(presentationData: presentationData, accountPeerId: account.peerId, peer: peer, state: state, participants: admins), style: .blocks, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) - - return (controllerState, (listState, arguments)) + |> deliverOnMainQueue + |> map { presentationData, state, admins -> (ItemListControllerState, (ItemListNodeState, ChatRecentActionsFilterEntry.ItemGenerationArguments)) in + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }) + + let doneEnabled = !state.events.isEmpty + + let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: { + var resultState: ChatRecentActionsFilterControllerState? + updateState { current in + resultState = current + return current + } + if let resultState = resultState { + apply(resultState.events, resultState.adminPeerIds) + } + dismissImpl?() + }) + + var sortedAdmins: [RenderedChannelParticipant]? + if let admins = admins { + sortedAdmins = admins.filter { $0.peer.id == account.peerId } + admins.filter({ $0.peer.id != account.peerId }) + } else { + sortedAdmins = nil } + + let previous = previousPeers + previousPeers = sortedAdmins + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChatAdmins_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(entries: channelRecentActionsFilterControllerEntries(presentationData: presentationData, accountPeerId: account.peerId, peer: peer, state: state, participants: sortedAdmins), style: .blocks, animateChanges: previous != nil && admins != nil && previous!.count >= sortedAdmins!.count) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } let controller = ItemListController(account: account, state: signal) dismissImpl = { [weak controller] in diff --git a/TelegramUI/ChatRecentActionsHistoryTransition.swift b/TelegramUI/ChatRecentActionsHistoryTransition.swift index 97903921b4..fdac6afc6a 100644 --- a/TelegramUI/ChatRecentActionsHistoryTransition.swift +++ b/TelegramUI/ChatRecentActionsHistoryTransition.swift @@ -53,6 +53,13 @@ private func appendAttributedText(text: (String, [(Int, NSRange)]), generateEnti string.append(text.0) } +private func appendAttributedText(text: String, withEntities: [MessageTextEntityType], to string: inout String, entities: inout [MessageTextEntity]) { + for type in withEntities { + entities.append(MessageTextEntity(range: string.count ..< (string.count + text.count), type: type)) + } + string.append(text) +} + private func filterOriginalMessageFlags(_ message: Message) -> Message { return message.withUpdatedFlags([.Incoming]) } @@ -104,7 +111,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.titleUpdated(title: new) 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: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case let .changeAbout(prev, new): var peers = SimpleDictionary() var author: Peer? @@ -135,14 +142,14 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case .content: let peers = SimpleDictionary() let attributes: [MessageAttribute] = [] let prevMessage = 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: author, text: prev, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) 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: author, text: new, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) + 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), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) } case let .changeUsername(prev, new): var peers = SimpleDictionary() @@ -173,7 +180,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case .content: var previousAttributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = [] @@ -192,7 +199,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let prevMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: prevText, attributes: previousAttributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) 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: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) + 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), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) } case let .changePhoto(_, new): var peers = SimpleDictionary() @@ -210,7 +217,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.photoUpdated(image: photo) 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: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case let .toggleInvites(value): var peers = SimpleDictionary() var author: Peer? @@ -237,7 +244,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) 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: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case let .toggleSignatures(value): var peers = SimpleDictionary() var author: Peer? @@ -264,7 +271,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) 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: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case let .updatePinned(message): switch self.id.contentIndex { case .header: @@ -286,7 +293,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case .content: if let message = message { var peers = SimpleDictionary() @@ -304,7 +311,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } 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), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) } else { var peers = SimpleDictionary() var author: Peer? @@ -326,7 +333,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) } } case let .editMessage(prev, message): @@ -342,7 +349,26 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] - appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageEdited(author?.displayTitle ?? ""), generateEntities: { index in + var mediaUpdated = false + if prev.media.count == message.media.count { + for i in 0 ..< prev.media.count { + if !prev.media[i].isEqual(message.media[i]) { + mediaUpdated = true + break + } + } + } else { + mediaUpdated = true + } + + let titleText: (String, [(Int, NSRange)]) + if mediaUpdated || message.media.isEmpty { + titleText = self.presentationData.strings.Channel_AdminLog_MessageEdited(author?.displayTitle ?? "") + } else { + titleText = self.presentationData.strings.Channel_AdminLog_CaptionEdited(author?.displayTitle ?? "") + } + + appendAttributedText(text: titleText, generateEntities: { index in if index == 0, let author = author { return [.TextMention(peerId: author.id)] } @@ -352,7 +378,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -369,7 +395,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } 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), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, isAdmin: false), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) + return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, isAdmin: false), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) } case let .deleteMessage(message): switch self.id.contentIndex { @@ -395,7 +421,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -412,7 +438,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } 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), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) } case .participantJoin, .participantLeave: var peers = SimpleDictionary() @@ -430,7 +456,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { action = TelegramMediaActionType.removedMembers(peerIds: [self.entry.event.peerId]) } 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: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case let .participantInvite(participant): var peers = SimpleDictionary() var author: Peer? @@ -446,7 +472,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action: TelegramMediaActionType action = TelegramMediaActionType.addedMembers(peerIds: [participant.peer.id]) 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: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case let .participantToggleBan(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -528,7 +554,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } else { text += "-" } - text += string + appendAttributedText(text: string, withEntities: [.Italic], to: &text, entities: &entities) } } } @@ -547,7 +573,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } 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: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case let .participantToggleAdmin(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -598,7 +624,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } else { text += "-" } - text += string + appendAttributedText(text: string, withEntities: [.Italic], to: &text, entities: &entities) } } } @@ -616,7 +642,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } 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: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case let .changeStickerPack(_, new): var peers = SimpleDictionary() var author: Peer? @@ -645,7 +671,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) 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: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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)) case let .togglePreHistoryHidden(value): var peers = SimpleDictionary() var author: Peer? @@ -675,7 +701,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) 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: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, account: account, chatLocation: .peer(peer.id), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, isAdmin: false)) + 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/ChatRecordingPreviewInputPanelNode.swift b/TelegramUI/ChatRecordingPreviewInputPanelNode.swift index 42bda39ca1..ab96274223 100644 --- a/TelegramUI/ChatRecordingPreviewInputPanelNode.swift +++ b/TelegramUI/ChatRecordingPreviewInputPanelNode.swift @@ -158,7 +158,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return 47.0 + return 45.0 } } diff --git a/TelegramUI/ChatRestrictedInputPanelNode.swift b/TelegramUI/ChatRestrictedInputPanelNode.swift index eea908800b..5f13b650ed 100644 --- a/TelegramUI/ChatRestrictedInputPanelNode.swift +++ b/TelegramUI/ChatRestrictedInputPanelNode.swift @@ -42,6 +42,6 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return 47.0 + return 45.0 } } diff --git a/TelegramUI/ChatSearchInputPanelNode.swift b/TelegramUI/ChatSearchInputPanelNode.swift index d5b07efded..509c095430 100644 --- a/TelegramUI/ChatSearchInputPanelNode.swift +++ b/TelegramUI/ChatSearchInputPanelNode.swift @@ -154,6 +154,6 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return 47.0 + return 45.0 } } diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index fe65d7e8ee..c423d1de2a 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -827,6 +827,18 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + if let presentationInterfaceState = self.presentationInterfaceState { + var isMediaEnabled = true + if let editMessageState = presentationInterfaceState.editMessageState { + if case .media(true) = editMessageState.content { + isMediaEnabled = true + } else { + isMediaEnabled = false + } + } + transition.updateAlpha(layer: self.attachmentButton.layer, alpha: isMediaEnabled ? 1.0 : 0.5) + self.attachmentButton.isEnabled = isMediaEnabled + } transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: panelHeight - minimalHeight + audioRecordingItemsVerticalOffset), size: CGSize(width: 40.0, height: minimalHeight))) var composeButtonsOffset: CGFloat = 0.0 diff --git a/TelegramUI/ChatUnblockInputPanelNode.swift b/TelegramUI/ChatUnblockInputPanelNode.swift index 8ebfbbdbbe..9e97404365 100644 --- a/TelegramUI/ChatUnblockInputPanelNode.swift +++ b/TelegramUI/ChatUnblockInputPanelNode.swift @@ -94,10 +94,10 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { let indicatorSize = self.activityIndicator.bounds.size self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) - return 47.0 + return 45.0 } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return 47.0 + return 45.0 } } diff --git a/TelegramUI/ComposeControllerNode.swift b/TelegramUI/ComposeControllerNode.swift index 4fd9adc6c7..18b1c390af 100644 --- a/TelegramUI/ComposeControllerNode.swift +++ b/TelegramUI/ComposeControllerNode.swift @@ -95,8 +95,6 @@ final class ComposeControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) - if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) if !searchDisplayController.isDeactivating { @@ -104,6 +102,8 @@ final class ComposeControllerNode: ASDisplayNode { } } + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging), transition: transition) + self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) } diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index bf8364e81b..3089636458 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -398,8 +398,19 @@ struct ContactListNodeGroupSelectionState: Equatable { } } +struct ContactListFilter: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let excludeSelf = ContactListFilter(rawValue: 1 << 1) +} + final class ContactListNode: ASDisplayNode { private let account: Account + private let filter: ContactListFilter let listNode: ListView @@ -449,8 +460,9 @@ final class ContactListNode: ASDisplayNode { private var presentationDataDisposable: Disposable? private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> - init(account: Account, presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState? = nil) { + init(account: Account, presentation: ContactListPresentation, filter: ContactListFilter = [.excludeSelf], selectionState: ContactListNodeGroupSelectionState? = nil) { self.account = account + self.filter = filter self.listNode = ListView() @@ -483,50 +495,58 @@ final class ContactListNode: ASDisplayNode { let themeAndStringsPromise = self.themeAndStringsPromise if case let .search(query) = presentation { transition = query - |> mapToSignal { query in - let foundLocalContacts = account.postbox.searchContacts(query: query.lowercased()) - let foundRemoteContacts: Signal<([FoundPeer], [FoundPeer]), NoError> = - .single(([], [])) - |> then( - searchPeers(account: account, query: query) - |> map { ($0.0, $0.1) } - |> delay(0.2, queue: Queue.concurrentDefaultQueue()) - ) - - return combineLatest(foundLocalContacts, foundRemoteContacts, selectionStateSignal, themeAndStringsPromise.get()) - |> mapToQueue { localPeers, remotePeers, selectionState, themeAndStrings -> Signal in - let signal = deferred { () -> Signal in - var peers: [ContactListPeer] = localPeers.map({ ContactListPeer(peer: $0, isGlobal: false) }) - var existingPeerIds = Set(peers.map { $0.peer.id }) - 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)) - } - } - } - for peer in remotePeers.1 { - if peer.peer is TelegramUser { - if !existingPeerIds.contains(peer.peer.id) { - existingPeerIds.insert(peer.peer.id) - peers.append(ContactListPeer(peer: peer.peer, isGlobal: true)) - } - } - } - - 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)) - } - - if OSAtomicCompareAndSwap32(1, 0, &firstTime) { - return signal |> runOn(Queue.mainQueue()) - } else { - return signal |> runOn(processingQueue) + |> mapToSignal { query in + let foundLocalContacts = account.postbox.searchContacts(query: query.lowercased()) + let foundRemoteContacts: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], [])) + |> then( + searchPeers(account: account, query: query) + |> map { ($0.0, $0.1) } + |> delay(0.2, queue: Queue.concurrentDefaultQueue()) + ) + + return combineLatest(foundLocalContacts, foundRemoteContacts, selectionStateSignal, themeAndStringsPromise.get()) + |> mapToQueue { localPeers, remotePeers, selectionState, themeAndStrings -> Signal in + let signal = deferred { () -> Signal in + var existingPeerIds = Set() + if filter.contains(.excludeSelf) { + existingPeerIds.insert(account.peerId) + } + var peers: [ContactListPeer] = [] + for peer in localPeers { + if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) + peers.append(ContactListPeer(peer: peer, isGlobal: false)) } } + 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)) + } + } + } + for peer in remotePeers.1 { + if peer.peer is TelegramUser { + if !existingPeerIds.contains(peer.peer.id) { + existingPeerIds.insert(peer.peer.id) + peers.append(ContactListPeer(peer: peer.peer, isGlobal: true)) + } + } + } + + 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)) + } + + if OSAtomicCompareAndSwap32(1, 0, &firstTime) { + return signal |> runOn(Queue.mainQueue()) + } else { + return signal |> runOn(processingQueue) + } } + } } else { transition = (combineLatest(self.contactPeersViewPromise.get(), selectionStateSignal, themeAndStringsPromise.get()) |> mapToQueue { view, selectionState, themeAndStrings -> Signal in diff --git a/TelegramUI/ContactsSearchContainerNode.swift b/TelegramUI/ContactsSearchContainerNode.swift index cdd69aa362..34e0355e07 100644 --- a/TelegramUI/ContactsSearchContainerNode.swift +++ b/TelegramUI/ContactsSearchContainerNode.swift @@ -71,7 +71,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { private var containerViewLayout: (ContainerViewLayout, CGFloat)? private var enqueuedTransitions: [ContactListSearchContainerTransition] = [] - init(account: Account, onlyWriteable: Bool, openPeer: @escaping (PeerId) -> Void) { + init(account: Account, onlyWriteable: Bool, filter: ContactListFilter = [.excludeSelf], openPeer: @escaping (PeerId) -> Void) { self.account = account self.openPeer = openPeer @@ -114,8 +114,14 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { |> map { localPeers, remotePeers, themeAndStrings -> [ContactListSearchEntry] in var entries: [ContactListSearchEntry] = [] var existingPeerIds = Set() + if filter.contains(.excludeSelf) { + existingPeerIds.insert(account.peerId) + } var index = 0 for peer in localPeers { + if existingPeerIds.contains(peer.id) { + continue + } existingPeerIds.insert(peer.id) var enabled = true if onlyWriteable { diff --git a/TelegramUI/ConvertToSupergroupController.swift b/TelegramUI/ConvertToSupergroupController.swift index eb94fc9473..63cb758982 100644 --- a/TelegramUI/ConvertToSupergroupController.swift +++ b/TelegramUI/ConvertToSupergroupController.swift @@ -114,6 +114,7 @@ private func convertToSupergroupEntries(presentationData: PresentationData) -> [ public func convertToSupergroupController(account: Account, peerId: PeerId) -> ViewController { var replaceControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? let statePromise = ValuePromise(ConvertToSupergroupState(), ignoreRepeated: true) let stateValue = Atomic(value: ConvertToSupergroupState()) @@ -127,19 +128,24 @@ public func convertToSupergroupController(account: Account, peerId: PeerId) -> V actionsDisposable.add(convertDisposable) let arguments = ConvertToSupergroupArguments(convert: { - var alreadyConverting = false - updateState { state in - if state.isConverting { - alreadyConverting = true - } - return ConvertToSupergroupState(isConverting: true) - } + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - if !alreadyConverting { - convertDisposable.set((convertGroupToSupergroup(account: account, peerId: peerId) |> deliverOnMainQueue).start(next: { createdPeerId in - replaceControllerImpl?(ChatController(account: account, chatLocation: .peer(createdPeerId))) - })) - } + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Group_UpgradeConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + var alreadyConverting = false + updateState { state in + if state.isConverting { + alreadyConverting = true + } + return ConvertToSupergroupState(isConverting: true) + } + + if !alreadyConverting { + convertDisposable.set((convertGroupToSupergroup(account: account, peerId: peerId) + |> deliverOnMainQueue).start(next: { createdPeerId in + replaceControllerImpl?(ChatController(account: account, chatLocation: .peer(createdPeerId))) + })) + } + })]), nil) }) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) @@ -166,5 +172,8 @@ public func convertToSupergroupController(account: Account, peerId: PeerId) -> V (controller.navigationController as? NavigationController)?.replaceAllButRootController(c, animated: true) } } + presentControllerImpl = { [weak controller] value, presentationArguments in + controller?.present(value, in: .window(.root), with: presentationArguments) + } return controller } diff --git a/TelegramUI/CreateChannelController.swift b/TelegramUI/CreateChannelController.swift index 0688f8e577..3ef37bef77 100644 --- a/TelegramUI/CreateChannelController.swift +++ b/TelegramUI/CreateChannelController.swift @@ -4,12 +4,15 @@ import SwiftSignalKit import Postbox import TelegramCore +import LegacyComponents + private struct CreateChannelArguments { let account: Account let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void let updateEditingDescriptionText: (String) -> Void let done: () -> Void + let changeProfilePhoto: () -> Void } private enum CreateChannelSection: Int32 { @@ -37,7 +40,7 @@ private enum CreateChannelEntryTag: ItemListItemTag { } private enum CreateChannelEntry: ItemListNodeEntry { - case channelInfo(PresentationTheme, PresentationStrings, Peer?, ItemListAvatarAndNameInfoItemState) + case channelInfo(PresentationTheme, PresentationStrings, Peer?, ItemListAvatarAndNameInfoItemState, ItemListAvatarAndNameInfoItemUpdatingAvatar?) case setProfilePhoto(PresentationTheme, String) case descriptionSetup(PresentationTheme, String, String) @@ -67,8 +70,8 @@ private enum CreateChannelEntry: ItemListNodeEntry { static func ==(lhs: CreateChannelEntry, rhs: CreateChannelEntry) -> Bool { switch lhs { - case let .channelInfo(lhsTheme, lhsStrings, lhsPeer, lhsEditingState): - if case let .channelInfo(rhsTheme, rhsStrings, rhsPeer, rhsEditingState) = rhs { + case let .channelInfo(lhsTheme, lhsStrings, lhsPeer, lhsEditingState, lhsAvatar): + if case let .channelInfo(rhsTheme, rhsStrings, rhsPeer, rhsEditingState, rhsAvatar) = rhs { if lhsTheme !== rhsTheme { return false } @@ -85,6 +88,9 @@ private enum CreateChannelEntry: ItemListNodeEntry { if lhsEditingState != rhsEditingState { return false } + if lhsAvatar != rhsAvatar { + return false + } return true } else { return false @@ -116,14 +122,14 @@ private enum CreateChannelEntry: ItemListNodeEntry { func item(_ arguments: CreateChannelArguments) -> ListViewItem { switch self { - case let .channelInfo(theme, strings, peer, state): + case let .channelInfo(theme, strings, peer, state, avatar): return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { - }, tag: CreateChannelEntryTag.info) + }, updatingImage: avatar, tag: CreateChannelEntryTag.info) case let .setProfilePhoto(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - + arguments.changeProfilePhoto() }) case let .descriptionSetup(theme, text, value): return ItemListMultilineInputItem(theme: theme, text: value, placeholder: text, maxLength: 1000, sectionId: self.section, style: .blocks, textUpdated: { updatedText in @@ -138,21 +144,10 @@ private enum CreateChannelEntry: ItemListNodeEntry { } private struct CreateChannelState: Equatable { - let creating: Bool - let editingName: ItemListAvatarAndNameInfoItemName - let editingDescriptionText: String - - init(creating: Bool, editingName: ItemListAvatarAndNameInfoItemName, editingDescriptionText: String) { - self.creating = creating - self.editingName = editingName - self.editingDescriptionText = editingDescriptionText - } - - init() { - self.creating = false - self.editingName = .title(title: "", type: .channel) - self.editingDescriptionText = "" - } + var creating: Bool + var editingName: ItemListAvatarAndNameInfoItemName + var editingDescriptionText: String + var avatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? static func ==(lhs: CreateChannelState, rhs: CreateChannelState) -> Bool { if lhs.creating != rhs.creating { @@ -164,6 +159,9 @@ private struct CreateChannelState: Equatable { if lhs.editingDescriptionText != rhs.editingDescriptionText { return false } + if lhs.avatar != rhs.avatar { + return false + } return true } } @@ -173,10 +171,10 @@ private func CreateChannelEntries(presentationData: PresentationData, state: Cre let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil) - let peer = TelegramGroup(id: PeerId(namespace: 100, id: 0), title: state.editingName.composedTitle, photo: [], participantCount: 0, role: .creator, membership: .Member, flags: [], migrationReference: nil, creationDate: 0, version: 0) + let peer = TelegramGroup(id: PeerId(namespace: -1, id: 0), title: state.editingName.composedTitle, photo: [], participantCount: 0, role: .creator, membership: .Member, flags: [], migrationReference: nil, creationDate: 0, version: 0) - entries.append(.channelInfo(presentationData.theme, presentationData.strings, peer, groupInfoState)) - entries.append(.setProfilePhoto(presentationData.theme, presentationData.strings.Settings_SetProfilePhoto)) + entries.append(.channelInfo(presentationData.theme, presentationData.strings, peer, groupInfoState, state.avatar)) + entries.append(.setProfilePhoto(presentationData.theme, presentationData.strings.Channel_UpdatePhotoItem)) entries.append(.descriptionSetup(presentationData.theme, presentationData.strings.Channel_Edit_AboutItem, state.editingDescriptionText)) entries.append(.descriptionInfo(presentationData.theme, presentationData.strings.Channel_About_Help)) @@ -185,7 +183,7 @@ private func CreateChannelEntries(presentationData: PresentationData, state: Cre } public func createChannelController(account: Account) -> ViewController { - let initialState = CreateChannelState() + let initialState = CreateChannelState(creating: false, editingName: ItemListAvatarAndNameInfoItemName.title(title: "", type: .channel), editingDescriptionText: "", avatar: nil) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((CreateChannelState) -> CreateChannelState) -> Void = { f in @@ -193,16 +191,26 @@ public func createChannelController(account: Account) -> ViewController { } var replaceControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? + var endEditingImpl: (() -> Void)? let actionsDisposable = DisposableSet() + let currentAvatarMixin = Atomic(value: nil) + + let uploadedAvatar = Promise() + let arguments = CreateChannelArguments(account: account, updateEditingName: { editingName in updateState { current in - return CreateChannelState(creating: current.creating, editingName: editingName, editingDescriptionText: current.editingDescriptionText) + var current = current + current.editingName = editingName + return current } }, updateEditingDescriptionText: { text in updateState { current in - return CreateChannelState(creating: current.creating, editingName: current.editingName, editingDescriptionText: text) + var current = current + current.editingDescriptionText = text + return current } }, done: { let (creating, title, description) = stateValue.with { state -> (Bool, String, String) in @@ -211,17 +219,29 @@ public func createChannelController(account: Account) -> ViewController { if !creating && !title.isEmpty { updateState { current in - return CreateChannelState(creating: true, editingName: current.editingName, editingDescriptionText: current.editingDescriptionText) + var current = current + current.creating = true + return current } + endEditingImpl?() actionsDisposable.add((createChannel(account: account, title: title, description: description.isEmpty ? nil : description) |> deliverOnMainQueue |> afterDisposed { Queue.mainQueue().async { updateState { current in - return CreateChannelState(creating: false, editingName: current.editingName, editingDescriptionText: current.editingDescriptionText) + var current = current + current.creating = false + return current } } }).start(next: { peerId in if let peerId = peerId { + let updatingAvatar = stateValue.with { + return $0.avatar + } + if let _ = updatingAvatar { + let _ = updatePeerPhoto(account: account, peerId: peerId, photo: uploadedAvatar.get()).start() + } + let controller = channelVisibilityController(account: account, peerId: peerId, mode: .initialSetup) replaceControllerImpl?(controller) } @@ -229,6 +249,57 @@ public func createChannelController(account: Account) -> ViewController { })) } + }, changeProfilePhoto: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) + + legacyController.bind(controller: navigationController) + + endEditingImpl?() + presentControllerImpl?(legacyController, nil) + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false)! + let _ = currentAvatarMixin.swap(mixin) + mixin.didFinishWithImage = { image in + if let image = image, let data = UIImageJPEGRepresentation(image, 0.6) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) + uploadedAvatar.set(uploadedPeerPhoto(account: account, resource: resource)) + updateState { current in + var current = current + current.avatar = .image(representation) + return current + } + } + } + if stateValue.with({ $0.avatar }) != nil { + mixin.didFinishWithDelete = { + updateState { current in + var current = current + current.avatar = nil + return current + } + uploadedAvatar.set(.never()) + } + } + mixin.didDismiss = { [weak legacyController] in + let _ = currentAvatarMixin.swap(nil) + legacyController?.dismiss() + } + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } }) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) @@ -255,5 +326,15 @@ public func createChannelController(account: Account) -> ViewController { replaceControllerImpl = { [weak controller] value in (controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true) } + presentControllerImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + controller.willDisappear = { _ in + endEditingImpl?() + } + endEditingImpl = { + [weak controller] in + controller?.view.endEditing(true) + } return controller } diff --git a/TelegramUI/CreateGroupController.swift b/TelegramUI/CreateGroupController.swift index 58945151d9..ff585546eb 100644 --- a/TelegramUI/CreateGroupController.swift +++ b/TelegramUI/CreateGroupController.swift @@ -4,11 +4,14 @@ import SwiftSignalKit import Postbox import TelegramCore +import LegacyComponents + private struct CreateGroupArguments { let account: Account let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void let done: () -> Void + let changeProfilePhoto: () -> Void } private enum CreateGroupSection: Int32 { @@ -36,7 +39,7 @@ private enum CreateGroupEntryTag: ItemListItemTag { } private enum CreateGroupEntry: ItemListNodeEntry { - case groupInfo(PresentationTheme, PresentationStrings, Peer?, ItemListAvatarAndNameInfoItemState) + case groupInfo(PresentationTheme, PresentationStrings, Peer?, ItemListAvatarAndNameInfoItemState, ItemListAvatarAndNameInfoItemUpdatingAvatar?) case setProfilePhoto(PresentationTheme, String) case member(Int32, PresentationTheme, PresentationStrings, Peer, PeerPresence?) @@ -63,8 +66,8 @@ private enum CreateGroupEntry: ItemListNodeEntry { static func ==(lhs: CreateGroupEntry, rhs: CreateGroupEntry) -> Bool { switch lhs { - case let .groupInfo(lhsTheme, lhsStrings, lhsPeer, lhsEditingState): - if case let .groupInfo(rhsTheme, rhsStrings, rhsPeer, rhsEditingState) = rhs { + case let .groupInfo(lhsTheme, lhsStrings, lhsPeer, lhsEditingState, lhsAvatar): + if case let .groupInfo(rhsTheme, rhsStrings, rhsPeer, rhsEditingState, rhsAvatar) = rhs { if lhsTheme !== rhsTheme { return false } @@ -81,6 +84,9 @@ private enum CreateGroupEntry: ItemListNodeEntry { if lhsEditingState != rhsEditingState { return false } + if lhsAvatar != rhsAvatar { + return false + } return true } else { return false @@ -125,14 +131,14 @@ private enum CreateGroupEntry: ItemListNodeEntry { func item(_ arguments: CreateGroupArguments) -> ListViewItem { switch self { - case let .groupInfo(theme, strings, peer, state): + case let .groupInfo(theme, strings, peer, state, avatar): return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, mode: .generic, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { - }, tag: CreateGroupEntryTag.info) + }, updatingImage: avatar, tag: CreateGroupEntryTag.info) case let .setProfilePhoto(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { - + arguments.changeProfilePhoto() }) case let .member(_, theme, strings, peer, presence): return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: presence, text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) @@ -141,8 +147,9 @@ private enum CreateGroupEntry: ItemListNodeEntry { } private struct CreateGroupState: Equatable { - let creating: Bool - let editingName: ItemListAvatarAndNameInfoItemName + var creating: Bool + var editingName: ItemListAvatarAndNameInfoItemName + var avatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? static func ==(lhs: CreateGroupState, rhs: CreateGroupState) -> Bool { if lhs.creating != rhs.creating { @@ -151,6 +158,9 @@ private struct CreateGroupState: Equatable { if lhs.editingName != rhs.editingName { return false } + if lhs.avatar != rhs.avatar { + return false + } return true } @@ -161,10 +171,10 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil) - let peer = TelegramGroup(id: PeerId(namespace: 100, id: 0), title: state.editingName.composedTitle, photo: [], participantCount: 0, role: .creator, membership: .Member, flags: [], migrationReference: nil, creationDate: 0, version: 0) + let peer = TelegramGroup(id: PeerId(namespace: -1, id: 0), title: state.editingName.composedTitle, photo: [], participantCount: 0, role: .creator, membership: .Member, flags: [], migrationReference: nil, creationDate: 0, version: 0) - entries.append(.groupInfo(presentationData.theme, presentationData.strings, peer, groupInfoState)) - entries.append(.setProfilePhoto(presentationData.theme, presentationData.strings.Settings_SetProfilePhoto)) + entries.append(.groupInfo(presentationData.theme, presentationData.strings, peer, groupInfoState, state.avatar)) + entries.append(.setProfilePhoto(presentationData.theme, presentationData.strings.GroupInfo_SetGroupPhoto)) var peers: [Peer] = [] for peerId in peerIds { @@ -201,7 +211,7 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat } public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewController { - let initialState = CreateGroupState(creating: false, editingName: .title(title: "", type: .group)) + let initialState = CreateGroupState(creating: false, editingName: .title(title: "", type: .group), avatar: nil) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((CreateGroupState) -> CreateGroupState) -> Void = { f in @@ -209,12 +219,20 @@ public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewCo } var replaceControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? + var endEditingImpl: (() -> Void)? let actionsDisposable = DisposableSet() + let currentAvatarMixin = Atomic(value: nil) + + let uploadedAvatar = Promise() + let arguments = CreateGroupArguments(account: account, updateEditingName: { editingName in updateState { current in - return CreateGroupState(creating: current.creating, editingName: editingName) + var current = current + current.editingName = editingName + return current } }, done: { let (creating, title) = stateValue.with { state -> (Bool, String) in @@ -223,21 +241,83 @@ public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewCo if !creating && !title.isEmpty { updateState { current in - return CreateGroupState(creating: true, editingName: current.editingName) + var current = current + current.creating = true + return current } + endEditingImpl?() actionsDisposable.add((createGroup(account: account, title: title, peerIds: peerIds) |> deliverOnMainQueue |> afterDisposed { Queue.mainQueue().async { updateState { current in - return CreateGroupState(creating: false, editingName: current.editingName) + var current = current + current.creating = false + return current } } }).start(next: { peerId in if let peerId = peerId { + let updatingAvatar = stateValue.with { + return $0.avatar + } + if let _ = updatingAvatar { + let _ = updatePeerPhoto(account: account, peerId: peerId, photo: uploadedAvatar.get()).start() + } let controller = ChatController(account: account, chatLocation: .peer(peerId)) replaceControllerImpl?(controller) } })) } + }, changeProfilePhoto: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) + + legacyController.bind(controller: navigationController) + + endEditingImpl?() + presentControllerImpl?(legacyController, nil) + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false)! + let _ = currentAvatarMixin.swap(mixin) + mixin.didFinishWithImage = { image in + if let image = image, let data = UIImageJPEGRepresentation(image, 0.6) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) + uploadedAvatar.set(uploadedPeerPhoto(account: account, resource: resource)) + updateState { current in + var current = current + current.avatar = .image(representation) + return current + } + } + } + if stateValue.with({ $0.avatar }) != nil { + mixin.didFinishWithDelete = { + updateState { current in + var current = current + current.avatar = nil + return current + } + uploadedAvatar.set(.never()) + } + } + mixin.didDismiss = { [weak legacyController] in + let _ = currentAvatarMixin.swap(nil) + legacyController?.dismiss() + } + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } }) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.postbox.multiplePeersView(peerIds)) @@ -264,5 +344,15 @@ public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewCo replaceControllerImpl = { [weak controller] value in (controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true) } + presentControllerImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + controller.willDisappear = { _ in + endEditingImpl?() + } + endEditingImpl = { + [weak controller] in + controller?.view.endEditing(true) + } return controller } diff --git a/TelegramUI/DataAndStorageSettingsController.swift b/TelegramUI/DataAndStorageSettingsController.swift index 94c26112da..761db78a78 100644 --- a/TelegramUI/DataAndStorageSettingsController.swift +++ b/TelegramUI/DataAndStorageSettingsController.swift @@ -358,11 +358,16 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other)) //entries.append(.saveIncomingPhotos(presentationData.theme, presentationData.strings.Settings_SaveIncomingPhotos, data.automaticMediaDownloadSettings.saveIncomingPhotos)) entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos)) - /*entries.append(.autoplayGifs(presentationData.theme, presentationData.strings.ChatSettings_AutoPlayAnimations, data.automaticMediaDownloadSettings.categories.gif.privateChats))*/ + entries.append(.autoplayGifs(presentationData.theme, presentationData.strings.ChatSettings_AutoPlayAnimations, data.automaticMediaDownloadSettings.autoplayGifs)) let proxyValue: String - if let proxySettings = data.proxySettings, let _ = proxySettings.activeServer, proxySettings.enabled { - proxyValue = presentationData.strings.ChatSettings_ConnectionType_UseSocks5 + if let proxySettings = data.proxySettings, let activeServer = proxySettings.activeServer, proxySettings.enabled { + switch activeServer.connection { + case .socks5: + proxyValue = presentationData.strings.ChatSettings_ConnectionType_UseSocks5 + case .mtp: + proxyValue = presentationData.strings.SocksProxySetup_ProxyTelegram + } } else { proxyValue = presentationData.strings.GroupInfo_SharedMediaNone } diff --git a/TelegramUI/DebugController.swift b/TelegramUI/DebugController.swift index dd441bca2e..e9ce3d41c8 100644 --- a/TelegramUI/DebugController.swift +++ b/TelegramUI/DebugController.swift @@ -31,6 +31,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case clearPaymentData(PresentationTheme) case logToFile(PresentationTheme, Bool) case logToConsole(PresentationTheme, Bool) + case redactSensitiveData(PresentationTheme, Bool) case enableRaiseToSpeak(PresentationTheme, Bool) case keepChatNavigationStack(PresentationTheme, Bool) @@ -42,7 +43,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logs.rawValue case .clearPaymentData: return DebugControllerSection.payments.rawValue - case .logToFile, .logToConsole: + case .logToFile, .logToConsole, .redactSensitiveData: return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack: return DebugControllerSection.experiments.rawValue @@ -61,10 +62,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 3 case .logToConsole: return 4 - case .enableRaiseToSpeak: + case .redactSensitiveData: return 5 - case .keepChatNavigationStack: + case .enableRaiseToSpeak: return 6 + case .keepChatNavigationStack: + return 7 } } @@ -100,6 +103,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { } else { return false } + case let .redactSensitiveData(lhsTheme, lhsValue): + if case let .redactSensitiveData(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { + return true + } else { + return false + } case let .enableRaiseToSpeak(lhsTheme, lhsValue): if case let .enableRaiseToSpeak(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { return true @@ -161,6 +170,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { $0.withUpdatedLogToConsole(value) }).start() }) + case let .redactSensitiveData(theme, value): + return ItemListSwitchItem(theme: theme, title: "Redact Sensitive Data", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = updateLoggingSettings(postbox: arguments.account.postbox, { + $0.withUpdatedRedactSensitiveData(value) + }).start() + }) case let .enableRaiseToSpeak(theme, value): return ItemListSwitchItem(theme: theme, title: "Enable Raise to Speak", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateMediaInputSettingsInteractively(postbox: arguments.account.postbox, { @@ -188,6 +203,7 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS entries.append(.logToFile(presentationData.theme, loggingSettings.logToFile)) entries.append(.logToConsole(presentationData.theme, loggingSettings.logToConsole)) + entries.append(.redactSensitiveData(presentationData.theme, loggingSettings.redactSensitiveData)) entries.append(.enableRaiseToSpeak(presentationData.theme, mediaInputSettings.enableRaiseToSpeak)) entries.append(.keepChatNavigationStack(presentationData.theme, experimentalSettings.keepChatNavigationStack)) diff --git a/TelegramUI/DefaultDarkAccentPresentationTheme.swift b/TelegramUI/DefaultDarkAccentPresentationTheme.swift index cb9adfb6ee..4439d57ff6 100644 --- a/TelegramUI/DefaultDarkAccentPresentationTheme.swift +++ b/TelegramUI/DefaultDarkAccentPresentationTheme.swift @@ -184,15 +184,16 @@ private let bubble = PresentationThemeChatBubble( actionButtonsOutgoingTextColor: UIColor(rgb: 0xffffff), selectionControlBorderColor: .white, selectionControlFillColor: accentColor, - selectionControlForegroundColor: .white + selectionControlForegroundColor: .white, + mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.4) ) private let serviceMessage = PresentationThemeServiceMessage( serviceMessageFillColor: UIColor(rgb: 0x18222D, alpha: 1.0), serviceMessagePrimaryTextColor: UIColor(rgb: 0xffffff), serviceMessageLinkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.12), - unreadBarFillColor: UIColor(rgb: 0x18222D), - unreadBarStrokeColor: UIColor(rgb: 0x18222D), + unreadBarFillColor: UIColor(rgb: 0x213040), + unreadBarStrokeColor: UIColor(rgb: 0x213040), unreadBarTextColor: UIColor(rgb: 0xffffff), dateFillStaticColor: UIColor(rgb: 0x18222D, alpha: 1.0), dateFillFloatingColor: UIColor(rgb: 0x18222D, alpha: 0.2), diff --git a/TelegramUI/DefaultDarkPresentationTheme.swift b/TelegramUI/DefaultDarkPresentationTheme.swift index effd21b525..d8bba6f88f 100644 --- a/TelegramUI/DefaultDarkPresentationTheme.swift +++ b/TelegramUI/DefaultDarkPresentationTheme.swift @@ -184,7 +184,8 @@ private let bubble = PresentationThemeChatBubble( actionButtonsOutgoingTextColor: UIColor(rgb: 0xffffff), selectionControlBorderColor: .white, selectionControlFillColor: accentColor, - selectionControlForegroundColor: .black + selectionControlForegroundColor: .black, + mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.4) ) private let serviceMessage = PresentationThemeServiceMessage( diff --git a/TelegramUI/DefaultPresentationTheme.swift b/TelegramUI/DefaultPresentationTheme.swift index 43589721db..32994e1254 100644 --- a/TelegramUI/DefaultPresentationTheme.swift +++ b/TelegramUI/DefaultPresentationTheme.swift @@ -214,7 +214,8 @@ private let bubble = PresentationThemeChatBubble( actionButtonsOutgoingTextColor: .white, selectionControlBorderColor: UIColor(rgb: 0xC7C7CC), selectionControlFillColor: accentColor, - selectionControlForegroundColor: .white + selectionControlForegroundColor: .white, + mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.2) ) private let bubbleDay = PresentationThemeChatBubble( @@ -271,7 +272,8 @@ private let bubbleDay = PresentationThemeChatBubble( actionButtonsOutgoingTextColor: UIColor(rgb: 0x3996ee), selectionControlBorderColor: UIColor(rgb: 0xC7C7CC), selectionControlFillColor: accentColor, - selectionControlForegroundColor: .white + selectionControlForegroundColor: .white, + mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.2) ) private let serviceMessage = PresentationThemeServiceMessage( diff --git a/TelegramUI/DeleteChatInputPanelNode.swift b/TelegramUI/DeleteChatInputPanelNode.swift index 71f9e99b09..4ea7a452eb 100644 --- a/TelegramUI/DeleteChatInputPanelNode.swift +++ b/TelegramUI/DeleteChatInputPanelNode.swift @@ -50,6 +50,6 @@ final class DeleteChatInputPanelNode: ChatInputPanelNode { } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return 47.0 + return 45.0 } } diff --git a/TelegramUI/EditAccessoryPanelNode.swift b/TelegramUI/EditAccessoryPanelNode.swift index 2e6291ff44..5288e8eb2a 100644 --- a/TelegramUI/EditAccessoryPanelNode.swift +++ b/TelegramUI/EditAccessoryPanelNode.swift @@ -16,6 +16,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { private let activityIndicator: ActivityIndicator private let statusNode: RadialStatusNode + private let tapNode: ASDisplayNode private let messageDisposable = MetaDisposable() private let editingMessageDisposable = MetaDisposable() @@ -88,6 +89,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) + self.tapNode = ASDisplayNode() + super.init() self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) @@ -99,11 +102,11 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.addSubnode(self.imageNode) self.addSubnode(self.activityIndicator) self.addSubnode(self.statusNode) - + self.addSubnode(self.tapNode) self.messageDisposable.set((account.postbox.messageAtId(messageId) - |> deliverOnMainQueue).start(next: { [weak self] message in - self?.updateMessage(message) - })) + |> deliverOnMainQueue).start(next: { [weak self] message in + self?.updateMessage(message) + })) } deinit { @@ -114,12 +117,9 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { override func didLoad() { super.didLoad() - self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:)))) - self.textNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:)))) + self.tapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.contentTap(_:)))) } - - private func updateMessage(_ message: Message?) { self.currentMessage = message @@ -209,7 +209,9 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { isMedia = false } - self.titleNode.attributedText = NSAttributedString(string: self.strings.Conversation_EditingMessagePanelTitle, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) + let canEditMedia = message.flatMap(canEditMessageMedia) ?? false + + self.titleNode.attributedText = NSAttributedString(string: canEditMedia ? self.strings.Conversation_EditingCaptionPanelTitle : self.strings.Conversation_EditingMessagePanelTitle, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: isMedia ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor) if let applyImage = applyImage { @@ -303,6 +305,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { let textSize = self.textNode.measure(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset, y: 25.0), size: textSize) + + self.tapNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: bounds.width - leftInset - rightInset - closeButtonSize.width - 4.0, height: bounds.height)) } @objc func closePressed() { @@ -311,9 +315,9 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { } } - @objc func imageTap(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.interfaceInteraction?.setupEditMessageMedia() + @objc func contentTap(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state, let message = self.currentMessage { + self.interfaceInteraction?.navigateToMessage(message.id) } } } diff --git a/TelegramUI/EditableTokenListNode.swift b/TelegramUI/EditableTokenListNode.swift index 621b4afc7e..006f07f02a 100644 --- a/TelegramUI/EditableTokenListNode.swift +++ b/TelegramUI/EditableTokenListNode.swift @@ -207,7 +207,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { } let tokenSize = tokenNode.measure(CGSize(width: max(1.0, width - sideInset - sideInset), height: CGFloat.greatestFiniteMagnitude)) - if tokenSize.width + currentOffset.x >= width - sideInset { + if tokenSize.width + currentOffset.x >= width - sideInset && !currentOffset.x.isEqual(to: sideInset) { currentOffset.x = sideInset currentOffset.y += tokenSize.height } diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 2e70738a8a..1f51cceaaf 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -54,14 +54,14 @@ private func mediaForMessage(message: Message) -> Media? { case let .Loaded(content): if let embedUrl = content.embedUrl, !embedUrl.isEmpty { return webpage - } else if let image = content.image { - if let result = galleryMediaForMedia(media: image) { - return result - } } else if let file = content.file { if let result = galleryMediaForMedia(media: file) { return result } + } else if let image = content.image { + if let result = galleryMediaForMedia(media: image) { + return result + } } case .Pending: break diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 7f711085ea..a1d084de09 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -33,8 +33,9 @@ private final class GroupInfoArguments { let displayUsernameContextMenu: (String) -> Void let displayAboutContextMenu: (String) -> Void 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) { + 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) { self.account = account self.peerId = peerId self.avatarAndNameInfoContext = avatarAndNameInfoContext @@ -59,6 +60,7 @@ private final class GroupInfoArguments { self.displayUsernameContextMenu = displayUsernameContextMenu self.displayAboutContextMenu = displayAboutContextMenu self.aboutLinkAction = aboutLinkAction + self.openStickerPackSetup = openStickerPackSetup } } @@ -67,6 +69,7 @@ private enum GroupInfoSection: ItemListSectionId { case about case infoManagement case sharedMediaAndNotifications + case stickerPack case memberManagement case members case leave @@ -132,6 +135,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { case sharedMedia(PresentationTheme, String) case notifications(PresentationTheme, String, String) case notificationSound(PresentationTheme, String, String) + case stickerPack(PresentationTheme, String, String) case adminManagement(PresentationTheme, String) case groupTypeSetup(PresentationTheme, String, String) case preHistory(PresentationTheme, String, Bool) @@ -154,6 +158,8 @@ private enum GroupInfoEntry: ItemListNodeEntry { return GroupInfoSection.infoManagement.rawValue case .sharedMedia, .notifications, .notificationSound, .adminManagement: return GroupInfoSection.sharedMediaAndNotifications.rawValue + case .stickerPack: + return GroupInfoSection.stickerPack.rawValue case .membersAdmins, .membersBlacklist: return GroupInfoSection.memberManagement.rawValue case .addMember, .member: @@ -260,6 +266,21 @@ private enum GroupInfoEntry: ItemListNodeEntry { } else { return false } + case let .stickerPack(lhsTheme, lhsTitle, lhsValue): + if case let .stickerPack(rhsTheme, rhsTitle, rhsValue) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsTitle != rhsTitle { + return false + } + if lhsValue != rhsValue { + return false + } + return true + } else { + return false + } case let .preHistory(lhsTheme, lhsTitle, lhsValue): if case let .preHistory(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { return true @@ -379,16 +400,18 @@ private enum GroupInfoEntry: ItemListNodeEntry { return 8 case .notificationSound: return 9 - case .sharedMedia: + case .stickerPack: return 10 - case .groupManagementInfoLabel: + case .sharedMedia: return 11 - case .membersAdmins: + case .groupManagementInfoLabel: return 12 - case .membersBlacklist: + case .membersAdmins: return 13 - case .addMember: + case .membersBlacklist: return 14 + case .addMember: + return 15 case let .member(_, _, index, _, _, _, _, _, _, _, _): return 20 + index case .convertToSupergroup: @@ -432,6 +455,10 @@ private enum GroupInfoEntry: ItemListNodeEntry { return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.changeNotificationSoundSettings() }) + case let .stickerPack(theme, title, value): + return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { + 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) @@ -732,6 +759,10 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa entries.append(GroupInfoEntry.notifications(presentationData.theme, presentationData.strings.GroupInfo_Notifications, notificationsText)) entries.append(GroupInfoEntry.notificationSound(presentationData.theme, presentationData.strings.GroupInfo_Sound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: peerNotificationSettings.messageSound, default: globalNotificationSettings.effective.groupChats.sound))) + if cachedChannelData.flags.contains(.canSetStickerSet) && canEditGroupInfo { + entries.append(GroupInfoEntry.stickerPack(presentationData.theme, presentationData.strings.Stickers_GroupStickers, cachedChannelData.stickerPack?.title ?? presentationData.strings.GroupInfo_SharedMediaNone)) + } + var canViewAdminsAndBanned = false if let channel = view.peers[view.peerId] as? TelegramChannel { if let adminRights = channel.adminRights, !adminRights.isEmpty { @@ -979,7 +1010,7 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa peerActions.append(ParticipantRevealAction(type: .destructive, title: presentationData.strings.Common_Delete, action: .remove)) } - entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, index: i, peerId: participant.peer.id, peer: participant.peer, participant: participant, presence: participant.presences[participant.peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: participant.peer.id, invitedBy: nil), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == participant.peer.id), revealActions: peerActions, enabled: !disabledPeerIds.contains(participant.peer.id))) + entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, index: i, peerId: participant.peer.id, peer: participant.peer, participant: participant, presence: participant.presences[participant.peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: !peerActions.isEmpty, editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == participant.peer.id), revealActions: peerActions, enabled: !disabledPeerIds.contains(participant.peer.id))) } } @@ -1123,7 +1154,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl hasPhotos = true } - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: hasPhotos, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: hasPhotos, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false)! let _ = currentAvatarMixin.swap(mixin) mixin.didFinishWithImage = { image in if let image = image { @@ -1134,7 +1165,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl updateState { $0.withUpdatedUpdatingAvatar(.image(representation)) } - updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, resource: resource) |> deliverOnMainQueue).start(next: { result in + updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, photo: uploadedPeerPhoto(account: account, resource: resource)) |> deliverOnMainQueue).start(next: { result in switch result { case .complete: updateState { @@ -1156,7 +1187,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl return $0.withUpdatedUpdatingAvatar(.none) } } - updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, resource: nil) |> deliverOnMainQueue).start(next: { result in + updateAvatarDisposable.set((updatePeerPhoto(account: account, peerId: peerId, photo: nil) |> deliverOnMainQueue).start(next: { result in switch result { case .complete: updateState { @@ -1477,6 +1508,13 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl displayAboutContextMenuImpl?(text) }, aboutLinkAction: { action, itemLink in aboutLinkActionImpl?(action, itemLink) + }, openStickerPackSetup: { + let _ = (account.postbox.transaction { transaction -> StickerPackCollectionInfo? in + return (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData)?.stickerPack + } + |> deliverOnMainQueue).start(next: { stickerPack in + presentControllerImpl?(groupStickerPackSetupController(account: account, peerId: peerId, currentPackInfo: stickerPack), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }) }) var loadMoreControl: PeerChannelMemberCategoryControl? diff --git a/TelegramUI/GroupStickerPackCurrentItem.swift b/TelegramUI/GroupStickerPackCurrentItem.swift new file mode 100644 index 0000000000..d2f149d4f7 --- /dev/null +++ b/TelegramUI/GroupStickerPackCurrentItem.swift @@ -0,0 +1,377 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +enum GroupStickerPackCurrentItemContent: Equatable { + case notFound + case searching + case found(packInfo: StickerPackCollectionInfo, topItem: StickerPackItem?, subtitle: String) +} + +final class GroupStickerPackCurrentItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let strings: PresentationStrings + let account: Account + let content: GroupStickerPackCurrentItemContent + let sectionId: ItemListSectionId + let action: (() -> Void)? + + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, content: GroupStickerPackCurrentItemContent, sectionId: ItemListSectionId, action: (() -> Void)?) { + self.theme = theme + self.strings = strings + self.account = account + self.content = content + self.sectionId = sectionId + self.action = action + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = GroupStickerPackCurrentItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply(false) }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? GroupStickerPackCurrentItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + var animated = true + if case .None = animation { + animated = false + } + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply(animated) + }) + } + } + } + } + } + + var selectable: Bool = true + + func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action?() + } +} + +private let titleFont = Font.bold(15.0) +private let statusFont = Font.regular(14.0) + +class GroupStickerPackCurrentItemNode: ItemListRevealOptionsItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + fileprivate let imageNode: TransformImageNode + private let notFoundNode: ASImageNode + private let titleNode: TextNode + private let statusNode: TextNode + private let activityIndicator: ActivityIndicator + + private var item: GroupStickerPackCurrentItem? + + private var editableControlNode: ItemListEditableControlNode? + private var reorderControlNode: ItemListEditableReorderControlNode? + + private let fetchDisposable = MetaDisposable() + + override var canBeSelected: Bool { + if let item = self.item { + if case .found = item.content { + return true + } + } + return false + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.imageNode = TransformImageNode() + self.imageNode.isLayerBacked = true + + self.notFoundNode = ASImageNode() + self.notFoundNode.isLayerBacked = true + self.notFoundNode.displayWithoutProcessing = true + self.notFoundNode.displaysAsynchronously = false + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.statusNode = TextNode() + self.statusNode.isLayerBacked = true + self.statusNode.contentMode = .left + self.statusNode.contentsScale = UIScreen.main.scale + + self.activityIndicator = ActivityIndicator(type: .custom(.blue, 22.0, 1.0)) + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.imageNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.statusNode) + self.addSubnode(self.notFoundNode) + self.addSubnode(self.activityIndicator) + } + + deinit { + self.fetchDisposable.dispose() + } + + func asyncLayout() -> (_ item: GroupStickerPackCurrentItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeImageLayout = self.imageNode.asyncLayout() + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeStatusLayout = TextNode.asyncLayout(self.statusNode) + + let currentItem = self.item + + return { item, params, neighbors in + var titleAttributedString: NSAttributedString? + var statusAttributedString: NSAttributedString? + + var updatedTheme: PresentationTheme? + + var updatedNotFoundImage: UIImage? + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + updatedNotFoundImage = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/GroupStickerPackNotFound"), color: item.theme.list.freeMonoIcon) + } + + let rightInset: CGFloat = params.rightInset + + var file: TelegramMediaFile? + var previousFile: TelegramMediaFile? + if let currentItem = currentItem, case let .found(found) = currentItem.content { + previousFile = found.topItem?.file + } + + switch item.content { + case .notFound: + titleAttributedString = NSAttributedString(string: item.strings.Channel_Stickers_NotFound, font: titleFont, textColor: item.theme.list.itemDestructiveColor) + statusAttributedString = NSAttributedString(string: item.strings.Channel_Stickers_NotFoundHelp, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + case .searching: + titleAttributedString = NSAttributedString(string: item.strings.Channel_Stickers_Searching, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) + statusAttributedString = NSAttributedString(string: "", font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + case let .found(packInfo, topItem, subtitle): + file = topItem?.file + titleAttributedString = NSAttributedString(string: packInfo.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) + statusAttributedString = NSAttributedString(string: subtitle, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + } + + var fileUpdated = false + if let file = file, let previousFile = previousFile { + fileUpdated = !file.isEqual(previousFile) + } else if (file != nil) != (previousFile != nil) { + fileUpdated = true + } + + let leftInset: CGFloat = 65.0 + params.leftInset + + let insets = itemListNeighborsGroupedInsets(neighbors) + let contentSize = CGSize(width: params.width, height: 59.0) + let separatorHeight = UIScreenPixel + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + let editingOffset: CGFloat = 0.0 + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset - 10.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + var imageApply: (() -> Void)? + var imageSize: CGSize = CGSize(width: 34.0, height: 34.0) + if let file = file, let dimensions = file.dimensions { + let imageBoundingSize = CGSize(width: 34.0, height: 34.0) + imageSize = dimensions.aspectFitted(imageBoundingSize) + imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) + } + + var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var updatedFetchSignal: Signal? + if fileUpdated { + if let file = file { + updatedImageSignal = chatMessageSticker(account: item.account, file: file, small: false) + updatedFetchSignal = item.account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)) + } else { + updatedImageSignal = .single({ _ in return nil }) + updatedFetchSignal = .complete() + } + } + + return (layout, { [weak self] animated in + if let strongSelf = self { + strongSelf.item = item + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + strongSelf.activityIndicator.type = .custom(item.theme.list.itemAccentColor, 22.0, 1.0) + } + + if case .notFound = item.content { + strongSelf.notFoundNode.isHidden = false + } else { + strongSelf.notFoundNode.isHidden = true + } + + if case .searching = item.content { + strongSelf.activityIndicator.isHidden = false + } else { + strongSelf.activityIndicator.isHidden = true + } + + let revealOffset = strongSelf.revealOffset + + let transition: ContainedViewLayoutTransition = .immediate + + imageApply?() + + let _ = titleApply() + let _ = statusApply() + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + editingOffset + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) + transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) + + let titleVerticalOffset: CGFloat + if statusLayout.size.width.isZero { + titleVerticalOffset = 19.0 + } else { + titleVerticalOffset = 11.0 + } + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: titleVerticalOffset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 32.0), size: statusLayout.size)) + + let boundingSize = CGSize(width: 34.0, height: 34.0) + transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0 + floor((boundingSize.width - imageSize.width) / 2.0), y: 11.0 + floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize)) + let indicatorSize = CGSize(width: 22.0, height: 22.0) + transition.updateFrame(node: strongSelf.activityIndicator, frame: CGRect(origin: CGPoint(x: params.leftInset + 15.0 + floor((boundingSize.width - indicatorSize.width) / 2.0), y: 11.0 + floor((boundingSize.height - indicatorSize.height) / 2.0)), size: indicatorSize)) + + if let image = updatedNotFoundImage { + strongSelf.notFoundNode.image = image + } + if let image = strongSelf.notFoundNode.image { + transition.updateFrame(node: strongSelf.notFoundNode, frame: CGRect(origin: CGPoint(x: params.leftInset + 15.0 + floor((boundingSize.width - image.size.width) / 2.0), y: 13.0 + floor((boundingSize.height - image.size.height) / 2.0)), size: image.size)) + } + + if let updatedImageSignal = updatedImageSignal { + strongSelf.imageNode.setSignal(updatedImageSignal) + } + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 59.0 + UIScreenPixel + UIScreenPixel)) + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + if let updatedFetchSignal = updatedFetchSignal { + strongSelf.fetchDisposable.set(updatedFetchSignal.start()) + } + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/TelegramUI/GroupStickerPackSetupController.swift b/TelegramUI/GroupStickerPackSetupController.swift new file mode 100644 index 0000000000..478ac2f029 --- /dev/null +++ b/TelegramUI/GroupStickerPackSetupController.swift @@ -0,0 +1,475 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class GroupStickerPackSetupControllerArguments { + let account: Account + + let selectStickerPack: (StickerPackCollectionInfo) -> Void + let openStickerPack: (StickerPackCollectionInfo) -> Void + let updateSearchText: (String) -> Void + let openStickersBot: () -> Void + + init(account: Account, selectStickerPack: @escaping (StickerPackCollectionInfo) -> Void, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, updateSearchText: @escaping (String) -> Void, openStickersBot: @escaping () -> Void) { + self.account = account + self.selectStickerPack = selectStickerPack + self.openStickerPack = openStickerPack + self.updateSearchText = updateSearchText + self.openStickersBot = openStickersBot + } +} + +private enum GroupStickerPackSection: Int32 { + case search + case stickers +} + +private enum GroupStickerPackEntryId: Hashable { + case index(Int32) + case pack(ItemCollectionId) + + var hashValue: Int { + switch self { + case let .index(index): + return index.hashValue + case let .pack(id): + return id.hashValue + } + } + + static func ==(lhs: GroupStickerPackEntryId, rhs: GroupStickerPackEntryId) -> Bool { + switch lhs { + case let .index(index): + if case .index(index) = rhs { + return true + } else { + return false + } + case let .pack(id): + if case .pack(id) = rhs { + return true + } else { + return false + } + } + } +} + +private enum GroupStickerPackEntry: ItemListNodeEntry { + case search(PresentationTheme, String, String, String) + case currentPack(Int32, PresentationTheme, PresentationStrings, GroupStickerPackCurrentItemContent) + case searchInfo(PresentationTheme, String) + case packsTitle(PresentationTheme, String) + case pack(Int32, PresentationTheme, PresentationStrings, StickerPackCollectionInfo, StickerPackItem?, String, Bool) + + var section: ItemListSectionId { + switch self { + case .search, .currentPack, .searchInfo: + return GroupStickerPackSection.search.rawValue + case .packsTitle, .pack: + return GroupStickerPackSection.stickers.rawValue + } + } + + var stableId: GroupStickerPackEntryId { + switch self { + case .search: + return .index(0) + case .currentPack: + return .index(1) + case .searchInfo: + return .index(2) + case .packsTitle: + return .index(3) + case let .pack(_, _, _, info, _, _, _): + return .pack(info.id) + } + } + + static func ==(lhs: GroupStickerPackEntry, rhs: GroupStickerPackEntry) -> Bool { + switch lhs { + case let .search(lhsTheme, lhsPrefix, lhsPlaceholder, lhsValue): + if case let .search(rhsTheme, rhsPrefix, rhsPlaceholder, rhsValue) = rhs, lhsTheme === rhsTheme, lhsPrefix == rhsPrefix, lhsPlaceholder == rhsPlaceholder, lhsValue == rhsValue { + return true + } else { + return false + } + case let .searchInfo(lhsTheme, lhsText): + if case let .searchInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .packsTitle(lhsTheme, lhsText): + if case let .packsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .currentPack(lhsIndex, lhsTheme, lhsStrings, lhsContent): + if case let .currentPack(rhsIndex, rhsTheme, rhsStrings, rhsContent) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if lhsContent != rhsContent { + return false + } + return true + } else { + return false + } + case let .pack(lhsIndex, lhsTheme, lhsStrings, lhsInfo, lhsTopItem, lhsCount, lhsSelected): + if case let .pack(rhsIndex, rhsTheme, rhsStrings, rhsInfo, rhsTopItem, rhsCount, rhsSelected) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if lhsInfo != rhsInfo { + return false + } + if lhsTopItem != rhsTopItem { + return false + } + if lhsCount != rhsCount { + return false + } + if lhsSelected != rhsSelected { + return false + } + return true + } else { + return false + } + } + } + + static func <(lhs: GroupStickerPackEntry, rhs: GroupStickerPackEntry) -> Bool { + switch lhs { + case .search: + switch rhs { + case .search: + return false + default: + return true + } + case .currentPack: + switch rhs { + case .search, .currentPack: + return false + default: + return true + } + case .searchInfo: + switch rhs { + case .search, .currentPack, .searchInfo: + return false + default: + return true + } + case .packsTitle: + switch rhs { + case .search, .currentPack, .searchInfo, .packsTitle: + return false + default: + return true + } + case let .pack(lhsIndex, _, _, _, _, _, _): + switch rhs { + case let .pack(rhsIndex, _, _, _, _, _, _): + return lhsIndex < rhsIndex + default: + return false + } + } + } + + func item(_ arguments: GroupStickerPackSetupControllerArguments) -> ListViewItem { + switch self { + case let .search(theme, prefix, placeholder, value): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: prefix), text: value, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), spacing: 0.0, clearButton: true, tag: nil, sectionId: self.section, textUpdated: { value in + arguments.updateSearchText(value) + }, action: {}) + case let .searchInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section, linkAction: nil) + case let .packsTitle(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .pack(_, theme, strings, info, topItem, count, selected): + return ItemListStickerPackItem(theme: theme, strings: strings, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: selected ? .selection : .none, editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false), enabled: true, sectionId: self.section, action: { + if selected { + arguments.openStickerPack(info) + } else { + arguments.selectStickerPack(info) + } + }, setPackIdWithRevealedOptions: { _, _ in + }, addPack: { + }, removePack: { + }) + case let .currentPack(_, theme, strings, content): + return GroupStickerPackCurrentItem(theme: theme, strings: strings, account: arguments.account, content: content, sectionId: self.section, action: { + if case let .found(found) = content { + arguments.openStickerPack(found.packInfo) + } + }) + } + } +} + +private struct StickerPackData: Equatable { + let info: StickerPackCollectionInfo + let item: StickerPackItem? +} + +private enum InitialStickerPackData { + case noData + case data(StickerPackData) +} + +private enum GroupStickerPackSearchState: Equatable { + case none + case found(StickerPackData) + case notFound + case searching +} + +private struct GroupStickerPackSetupControllerState: Equatable { + var isSaving: Bool +} + +private func groupStickerPackSetupControllerEntries(presentationData: PresentationData, searchText: String, view: CombinedView, initialData: InitialStickerPackData?, searchState: GroupStickerPackSearchState) -> [GroupStickerPackEntry] { + if initialData == nil { + return [] + } + var entries: [GroupStickerPackEntry] = [] + + entries.append(.search(presentationData.theme, "t.me/addstickers/", presentationData.strings.Channel_Stickers_Placeholder, searchText)) + switch searchState { + case .none: + break + case .notFound: + entries.append(.currentPack(0, presentationData.theme, presentationData.strings, .notFound)) + case .searching: + entries.append(.currentPack(0, presentationData.theme, presentationData.strings, .searching)) + case let .found(data): + entries.append(.currentPack(0, presentationData.theme, presentationData.strings, .found(packInfo: data.info, topItem: data.item, subtitle: presentationData.strings.StickerPack_StickerCount(data.info.count)))) + } + entries.append(.searchInfo(presentationData.theme, presentationData.strings.Channel_Stickers_CreateYourOwn)) + entries.append(.packsTitle(presentationData.theme, presentationData.strings.Channel_Stickers_YourStickers)) + + let namespace = Namespaces.ItemCollection.CloudStickerPacks + if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [namespace])] as? ItemCollectionInfosView { + if let packsEntries = stickerPacksView.entriesByNamespace[namespace] { + var index: Int32 = 0 + for entry in packsEntries { + if let info = entry.info as? StickerPackCollectionInfo { + var selected = false + if case let .found(found) = searchState { + selected = found.info.id == info.id + } + entries.append(.pack(index, presentationData.theme, presentationData.strings, info, entry.firstItem as? StickerPackItem, presentationData.strings.StickerPack_StickerCount(info.count == 0 ? entry.count : info.count), selected)) + index += 1 + } + } + } + } + + return entries +} + +public func groupStickerPackSetupController(account: Account, peerId: PeerId, currentPackInfo: StickerPackCollectionInfo?) -> ViewController { + let initialState = GroupStickerPackSetupControllerState(isSaving: false) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((GroupStickerPackSetupControllerState) -> GroupStickerPackSetupControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let searchText = ValuePromise(currentPackInfo?.shortName ?? "", ignoreRepeated: true) + + let initialData = Promise() + if let currentPackInfo = currentPackInfo { + initialData.set(cachedStickerPack(postbox: account.postbox, network: account.network, reference: .id(id: currentPackInfo.id.id, accessHash: currentPackInfo.accessHash)) + |> map { result -> InitialStickerPackData? in + switch result { + case .none: + return .noData + case .fetching: + return nil + case let .result(info, items, _): + return InitialStickerPackData.data(StickerPackData(info: info, item: items.first as? StickerPackItem)) + } + }) + } else { + initialData.set(.single(.noData)) + } + + let stickerPacks = Promise() + stickerPacks.set(account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) + + let searchState = Promise<(String, GroupStickerPackSearchState)>() + searchState.set(combineLatest(searchText.get(), initialData.get(), stickerPacks.get()) + |> mapToSignal { searchText, initialData, view -> Signal<(String, GroupStickerPackSearchState), NoError> in + if let initialData = initialData { + if searchText.isEmpty { + return .single((searchText, .none)) + } else if case let .data(data) = initialData, searchText.lowercased() == data.info.shortName { + return .single((searchText, .found(StickerPackData(info: data.info, item: data.item)))) + } else { + let namespace = Namespaces.ItemCollection.CloudStickerPacks + if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [namespace])] as? ItemCollectionInfosView { + if let packsEntries = stickerPacksView.entriesByNamespace[namespace] { + for entry in packsEntries { + if let info = entry.info as? StickerPackCollectionInfo { + if info.shortName.lowercased() == searchText.lowercased() { + return .single((searchText, .found(StickerPackData(info: info, item: entry.firstItem as? StickerPackItem)))) + } + } + } + } + } + return .single((searchText, .searching)) + |> then((loadedStickerPack(postbox: account.postbox, network: account.network, reference: .name(searchText.lowercased())) |> delay(0.1, queue: Queue.concurrentDefaultQueue())) + |> mapToSignal { value -> Signal<(String, GroupStickerPackSearchState), NoError> in + switch value { + case .fetching: + return .complete() + case .none: + return .single((searchText, .notFound)) + case let .result(info, items, _): + return .single((searchText, .found(StickerPackData(info: info, item: items.first as? StickerPackItem)))) + } + }) + } + } else { + return .single((searchText, .none)) + } + }) + + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var navigateToChatControllerImpl: ((PeerId) -> Void)? + var dismissImpl: (() -> Void)? + + let actionsDisposable = DisposableSet() + + let resolveDisposable = MetaDisposable() + actionsDisposable.add(resolveDisposable) + + let saveDisposable = MetaDisposable() + actionsDisposable.add(saveDisposable) + + var presentStickerPackController: ((StickerPackCollectionInfo) -> Void)? + + let arguments = GroupStickerPackSetupControllerArguments(account: account, selectStickerPack: { info in + searchText.set(info.shortName) + }, openStickerPack: { info in + presentStickerPackController?(info) + }, updateSearchText: { text in + searchText.set(text) + }, openStickersBot: { + resolveDisposable.set((resolvePeerByName(account: account, name: "stickers") |> deliverOnMainQueue).start(next: { peerId in + if let peerId = peerId { + dismissImpl?() + navigateToChatControllerImpl?(peerId) + } + })) + }) + + let previousHadData = Atomic(value: false) + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, initialData.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, searchState.get() |> deliverOnMainQueue) + |> map { presentationData, state, initialData, view, searchState -> (ItemListControllerState, (ItemListNodeState, GroupStickerPackEntry.ItemGenerationArguments)) in + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }) + + var rightNavigationButton: ItemListNavigationButton? + if initialData != nil { + if state.isSaving { + rightNavigationButton = ItemListNavigationButton(content: .text(""), style: .activity, enabled: true, action: {}) + } else { + let enabled: Bool + var info: StickerPackCollectionInfo? + switch searchState.1 { + case .searching, .notFound: + enabled = false + case .none: + enabled = true + case let .found(data): + enabled = true + info = data.info + } + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: enabled, action: { + if info?.id == currentPackInfo?.id { + dismissImpl?() + } else { + updateState { state in + var state = state + state.isSaving = true + return state + } + saveDisposable.set((updateGroupSpecificStickerset(postbox: account.postbox, network: account.network, peerId: peerId, info: info) + |> deliverOnMainQueue).start(error: { + updateState { state in + var state = state + state.isSaving = false + return state + } + }, completed: { + dismissImpl?() + })) + } + }) + } + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Channel_Info_Stickers), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + + let hasData = initialData != nil + let hadData = previousHadData.swap(hasData) + + var emptyStateItem: ItemListLoadingIndicatorEmptyStateItem? + if !hasData { + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) + } + + let listState = ItemListNodeState(entries: groupStickerPackSetupControllerEntries(presentationData: presentationData, searchText: searchState.0, view: view, initialData: initialData, searchState: searchState.1), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: hasData && hadData) + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(account: account, state: signal) + + presentControllerImpl = { [weak controller] c, p in + if let controller = controller { + controller.present(c, in: .window(.root), with: p) + } + } + presentStickerPackController = { [weak controller] info in + presentControllerImpl?(StickerPackPreviewController(account: account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: controller?.navigationController as? NavigationController), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + navigateToChatControllerImpl = { [weak controller] peerId in + if let controller = controller, let navigationController = controller.navigationController as? NavigationController { + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) + } + } + dismissImpl = { [weak controller] in + controller?.dismiss() + } + + return controller +} + diff --git a/TelegramUI/HorizontalPeerItem.swift b/TelegramUI/HorizontalPeerItem.swift index 26b4994336..acad4e7220 100644 --- a/TelegramUI/HorizontalPeerItem.swift +++ b/TelegramUI/HorizontalPeerItem.swift @@ -84,11 +84,11 @@ final class HorizontalPeerItemNode: ListViewItemNode { item.action(item.peer) } } - /*self.peerNode.longTapAction = { [weak self] in + self.peerNode.longTapAction = { [weak self] in if let item = self?.item { item.longTapAction(item.peer) } - }*/ + } } override func didLoad() { diff --git a/TelegramUI/InstalledStickerPacksController.swift b/TelegramUI/InstalledStickerPacksController.swift index 23cd7f4f10..6d060d4d8f 100644 --- a/TelegramUI/InstalledStickerPacksController.swift +++ b/TelegramUI/InstalledStickerPacksController.swift @@ -287,14 +287,6 @@ private func namespaceForMode(_ mode: InstalledStickerPacksControllerMode) -> It } } -private func stringForStickerCount(_ count: Int32) -> String { - if count == 1 { - return "1 sticker" - } else { - return "\(count) stickers" - } -} - private func installedStickerPacksControllerEntries(presentationData: PresentationData, state: InstalledStickerPacksControllerState, mode: InstalledStickerPacksControllerMode, view: CombinedView, featured: [FeaturedStickerPackItem]) -> [InstalledStickerPacksEntry] { var entries: [InstalledStickerPacksEntry] = [] @@ -321,7 +313,7 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati var index: Int32 = 0 for entry in packsEntries { if let info = entry.info as? StickerPackCollectionInfo { - entries.append(.pack(index, presentationData.theme, presentationData.strings, info, entry.firstItem as? StickerPackItem, stringForStickerCount(info.count == 0 ? entry.count : info.count), true, ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == entry.id, reorderable: true))) + entries.append(.pack(index, presentationData.theme, presentationData.strings, info, entry.firstItem as? StickerPackItem, presentationData.strings.StickerPack_StickerCount(info.count == 0 ? entry.count : info.count), true, ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == entry.id, reorderable: true))) index += 1 } } @@ -384,7 +376,11 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { dismissAction() - let _ = removeStickerPackInteractively(postbox: account.postbox, id: id).start() + let _ = removeStickerPackInteractively(postbox: account.postbox, id: id, option: .delete).start() + }), + ActionSheetButtonItem(title: presentationData.strings.StickerSettings_ContextHide, color: .destructive, action: { + dismissAction() + let _ = removeStickerPackInteractively(postbox: account.postbox, id: id, option: .archive).start() }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) @@ -531,7 +527,7 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti } else if afterAll { infos.append((fromPackInfo.id, reorderInfo)) } - addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespaceForMode(mode)) + addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespaceForMode(mode), content: .sync) transaction.replaceItemCollectionInfos(namespace: namespaceForMode(mode), itemCollectionInfos: infos) } }).start() diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index ea12dd5366..902bbbab41 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -324,10 +324,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { let visibleBounds = self.scrollNode.view.bounds var topNode: ASDisplayNode? - for node in self.scrollNode.subnodes.reversed() { - if let node = node as? InstantPageTileNode { - topNode = node - break + if let scrollSubnodes = self.scrollNode.subnodes { + for node in scrollSubnodes.reversed() { + if let node = node as? InstantPageTileNode { + topNode = node + break + } } } diff --git a/TelegramUI/InstantPageSettingsBacklightItemNode.swift b/TelegramUI/InstantPageSettingsBacklightItemNode.swift index 0b5955701a..e1a340dd74 100644 --- a/TelegramUI/InstantPageSettingsBacklightItemNode.swift +++ b/TelegramUI/InstantPageSettingsBacklightItemNode.swift @@ -20,6 +20,7 @@ final class InstantPageSettingsBacklightItemNode: InstantPageSettingsItemNode { init(theme: InstantPageSettingsItemTheme) { self.sliderView = TGPhotoEditorSliderView() + self.sliderView.enablePanHandling = true self.sliderView.trackCornerRadius = 1.0 self.sliderView.lineSize = 2.0 self.sliderView.minimumValue = 0.0 diff --git a/TelegramUI/InstantPageSettingsFontSizeItemNode.swift b/TelegramUI/InstantPageSettingsFontSizeItemNode.swift index 86307d881f..ccbf20a66c 100644 --- a/TelegramUI/InstantPageSettingsFontSizeItemNode.swift +++ b/TelegramUI/InstantPageSettingsFontSizeItemNode.swift @@ -24,6 +24,7 @@ final class InstantPageSettingsFontSizeItemNode: InstantPageSettingsItemNode { self.updated = updated self.sliderView = TGPhotoEditorSliderView() + self.sliderView.enablePanHandling = true self.sliderView.trackCornerRadius = 1.0 self.sliderView.lineSize = 2.0 self.sliderView.dotSize = 5.0 diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index 5257b976b3..f5c7f01f6a 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -462,7 +462,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite } var updateAvatarOverlayImage: UIImage? - if item.updatingImage != nil && currentOverlayImage == nil { + if item.updatingImage != nil && item.peer?.id.namespace != -1 && currentOverlayImage == nil { updateAvatarOverlayImage = updatingAvatarOverlayImage } diff --git a/TelegramUI/ItemListController.swift b/TelegramUI/ItemListController.swift index 8d6cc14b23..3d15bcb64a 100644 --- a/TelegramUI/ItemListController.swift +++ b/TelegramUI/ItemListController.swift @@ -182,6 +182,8 @@ final class ItemListController: ViewController { } } + var willDisappear: ((Bool) -> Void)? + init(account: Account, state: Signal<(ItemListControllerState, (ItemListNodeState, Entry.ItemGenerationArguments)), NoError>, tabBarItem: Signal? = nil) { self.state = state @@ -412,6 +414,12 @@ final class ItemListController: ViewController { } } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + self.willDisappear?(animated) + } + override func dismiss(completion: (() -> Void)? = nil) { (self.displayNode as! ItemListControllerNode).animateOut(completion: completion) } diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index e833376b1a..40850ae809 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -111,6 +111,8 @@ class ItemListControllerNode: ViewControllerTracingNod private var theme: PresentationTheme? private var listStyle: ItemListStyle? + private var appliedFocusItemTag: ItemListItemTag? + let updateNavigationOffset: (CGFloat) -> Void var dismiss: (() -> Void)? @@ -313,11 +315,20 @@ class ItemListControllerNode: ViewControllerTracingNod strongSelf._ready.set(true) } - 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() + var updatedFocusItemTag = false + if let appliedFocusItemTag = strongSelf.appliedFocusItemTag, let focusItemTag = focusItemTag { + updatedFocusItemTag = !appliedFocusItemTag.isEqual(to: focusItemTag) + } else if (strongSelf.appliedFocusItemTag != nil) != (focusItemTag != nil) { + updatedFocusItemTag = true + } + if updatedFocusItemTag { + 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() + } } } } diff --git a/TelegramUI/ItemListMultilineInputItem.swift b/TelegramUI/ItemListMultilineInputItem.swift index 72f871f758..18370e3b66 100644 --- a/TelegramUI/ItemListMultilineInputItem.swift +++ b/TelegramUI/ItemListMultilineInputItem.swift @@ -11,9 +11,10 @@ class ItemListMultilineInputItem: ListViewItem, ItemListItem { let style: ItemListStyle let action: () -> Void let textUpdated: (String) -> Void + let tag: ItemListItemTag? let maxLength: Int? - init(theme: PresentationTheme, text: String, placeholder: String, maxLength: Int?, sectionId: ItemListSectionId, style: ItemListStyle, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { + init(theme: PresentationTheme, text: String, placeholder: String, maxLength: Int?, sectionId: ItemListSectionId, style: ItemListStyle, textUpdated: @escaping (String) -> Void, tag: ItemListItemTag? = nil, action: @escaping () -> Void) { self.theme = theme self.text = text self.placeholder = placeholder @@ -21,6 +22,7 @@ class ItemListMultilineInputItem: ListViewItem, ItemListItem { self.sectionId = sectionId self.style = style self.textUpdated = textUpdated + self.tag = tag self.action = action } @@ -58,7 +60,7 @@ class ItemListMultilineInputItem: ListViewItem, ItemListItem { private let titleFont = Font.regular(17.0) -class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelegate { +class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelegate, ItemListItemNode, ItemListItemFocusableNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -72,6 +74,10 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega private var item: ItemListMultilineInputItem? private var layoutParams: ListViewItemLayoutParams? + var tag: ItemListItemTag? { + return self.item?.tag + } + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -294,4 +300,10 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega } } } + + func focus() { + if !self.textNode.textView.isFirstResponder { + self.textNode.textView.becomeFirstResponder() + } + } } diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index 5632099715..27cf9ff87e 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -296,7 +296,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { if currentCheckNode == nil { currentCheckNode = ASImageNode() } - rightInset += 10.0 + rightInset += 24.0 currentSwitchNode = nil } } else { diff --git a/TelegramUI/ItemListSingleLineInputItem.swift b/TelegramUI/ItemListSingleLineInputItem.swift index b811ca9668..4c80d4802d 100644 --- a/TelegramUI/ItemListSingleLineInputItem.swift +++ b/TelegramUI/ItemListSingleLineInputItem.swift @@ -3,8 +3,8 @@ import Display import AsyncDisplayKit import SwiftSignalKit -enum ItemListSingleLineInputItemType { - case regular +enum ItemListSingleLineInputItemType: Equatable { + case regular(capitalization: Bool, autocorrection: Bool) case password case email case number @@ -17,18 +17,20 @@ class ItemListSingleLineInputItem: ListViewItem, ItemListItem { let placeholder: String let type: ItemListSingleLineInputItemType let spacing: CGFloat + let clearButton: Bool let sectionId: ItemListSectionId let action: () -> Void let textUpdated: (String) -> Void let tag: ItemListItemTag? - init(theme: PresentationTheme, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular, spacing: CGFloat = 0.0, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { + init(theme: PresentationTheme, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), spacing: CGFloat = 0.0, clearButton: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { self.theme = theme self.title = title self.text = text self.placeholder = placeholder self.type = type self.spacing = spacing + self.clearButton = clearButton self.tag = tag self.sectionId = sectionId self.textUpdated = textUpdated @@ -76,6 +78,8 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It private let titleNode: TextNode private let textNode: TextFieldNode + private let clearIconNode: ASImageNode + private let clearButtonNode: HighlightableButtonNode private var item: ItemListSingleLineInputItem? @@ -96,10 +100,32 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It self.titleNode = TextNode() self.textNode = TextFieldNode() + self.clearIconNode = ASImageNode() + self.clearIconNode.isLayerBacked = true + self.clearIconNode.displayWithoutProcessing = true + self.clearIconNode.displaysAsynchronously = false + + self.clearButtonNode = HighlightableButtonNode() + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) + self.addSubnode(self.clearIconNode) + self.addSubnode(self.clearButtonNode) + + self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) + self.clearButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.clearIconNode.alpha = 0.4 + } else { + strongSelf.clearIconNode.alpha = 1.0 + strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } } override func didLoad() { @@ -125,17 +151,24 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It return { item, params, neighbors in var updatedTheme: PresentationTheme? + var updatedClearIcon: UIImage? if currentItem?.theme !== item.theme { updatedTheme = item.theme + updatedClearIcon = PresentationResourcesItemList.itemListClearInputIcon(item.theme) } let leftInset: CGFloat = 16.0 + params.leftInset + var rightInset: CGFloat = params.rightInset + + if item.clearButton { + rightInset += 32.0 + } let titleString = NSMutableAttributedString(attributedString: item.title) titleString.removeAttribute(NSAttributedStringKey.font, range: NSMakeRange(0, titleString.length)) titleString.addAttributes([NSAttributedStringKey.font: Font.regular(17.0)], range: NSMakeRange(0, titleString.length)) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let separatorHeight = UIScreenPixel @@ -165,24 +198,29 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It let secureEntry: Bool let capitalizationType: UITextAutocapitalizationType + let autocorrectionType: UITextAutocorrectionType let keyboardType: UIKeyboardType switch item.type { - case .regular: + case let .regular(capitalization, autocorrection): secureEntry = false - capitalizationType = .sentences + capitalizationType = capitalization ? .sentences : .none + autocorrectionType = autocorrection ? .default : .no keyboardType = UIKeyboardType.default case .email: secureEntry = false capitalizationType = .none + autocorrectionType = .no keyboardType = UIKeyboardType.emailAddress case .password: secureEntry = true capitalizationType = .none + autocorrectionType = .no keyboardType = UIKeyboardType.default case .number: secureEntry = false capitalizationType = .none + autocorrectionType = .no keyboardType = UIKeyboardType.numberPad } @@ -195,6 +233,9 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It if strongSelf.textNode.textField.autocapitalizationType != capitalizationType { strongSelf.textNode.textField.autocapitalizationType = capitalizationType } + if strongSelf.textNode.textField.autocorrectionType != autocorrectionType { + strongSelf.textNode.textField.autocorrectionType = autocorrectionType + } if let currentText = strongSelf.textNode.textField.text { if currentText != item.text { @@ -204,7 +245,20 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It strongSelf.textNode.textField.text = item.text } - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + item.spacing, y: floor((layout.contentSize.height - 40.0) / 2.0)), size: CGSize(width: max(1.0, params.width - (leftInset + titleLayout.size.width + item.spacing)), height: 40.0)) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + item.spacing, y: floor((layout.contentSize.height - 40.0) / 2.0)), size: CGSize(width: max(1.0, params.width - (leftInset + rightInset + titleLayout.size.width + item.spacing)), height: 40.0)) + + if let image = updatedClearIcon { + strongSelf.clearIconNode.image = image + } + + let buttonSize = CGSize(width: 38.0, height: layout.contentSize.height) + strongSelf.clearButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - buttonSize.width, y: 0.0), size: buttonSize) + if let image = strongSelf.clearIconNode.image { + strongSelf.clearIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((layout.contentSize.height - image.size.height) / 2.0)), size: image.size) + } + + strongSelf.clearIconNode.isHidden = !item.clearButton || item.text.isEmpty + strongSelf.clearButtonNode.isHidden = !item.clearButton || item.text.isEmpty if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -248,7 +302,7 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } - @objc func textFieldTextChanged(_ textField: UITextField) { + @objc private func textFieldTextChanged(_ textField: UITextField) { if let item = self.item { if let text = self.textNode.textField.text { item.textUpdated(text) @@ -258,6 +312,13 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It } } + @objc private func clearButtonPressed() { + if let item = self.item { + self.textNode.textField.text = "" + item.textUpdated("") + } + } + func focus() { if !self.textNode.textField.isFirstResponder { self.textNode.textField.becomeFirstResponder() diff --git a/TelegramUI/ItemListStickerPackItem.swift b/TelegramUI/ItemListStickerPackItem.swift index 0375a70c3f..0ad226897a 100644 --- a/TelegramUI/ItemListStickerPackItem.swift +++ b/TelegramUI/ItemListStickerPackItem.swift @@ -31,23 +31,7 @@ struct ItemListStickerPackItemEditing: Equatable { enum ItemListStickerPackItemControl: Equatable { case none case installation(installed: Bool) - - static func ==(lhs: ItemListStickerPackItemControl, rhs: ItemListStickerPackItemControl) -> Bool { - switch lhs { - case .none: - if case .none = rhs { - return true - } else { - return false - } - case let .installation(installed): - if case .installation(installed) = rhs { - return true - } else { - return false - } - } - } + case selection } final class ItemListStickerPackItem: ListViewItem, ItemListItem { @@ -145,6 +129,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { private let statusNode: TextNode private let installationActionImageNode: ASImageNode private let installationActionNode: HighlightableButtonNode + private let selectionIconNode: ASImageNode private var layoutParams: (ItemListStickerPackItem, ListViewItemLayoutParams, ItemListNeighbors)? @@ -198,6 +183,11 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { self.installationActionImageNode.isLayerBacked = true self.installationActionNode = HighlightableButtonNode() + self.selectionIconNode = ASImageNode() + self.selectionIconNode.displaysAsynchronously = false + self.selectionIconNode.displayWithoutProcessing = true + self.selectionIconNode.isLayerBacked = true + self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true @@ -209,6 +199,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { self.addSubnode(self.unreadNode) self.addSubnode(self.installationActionImageNode) self.addSubnode(self.installationActionNode) + self.addSubnode(self.selectionIconNode) self.installationActionNode.addTarget(self, action: #selector(self.installationActionPressed), forControlEvents: .touchUpInside) } @@ -249,6 +240,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { var rightInset: CGFloat = params.rightInset var installationActionImage: UIImage? + var checkImage: UIImage? switch item.control { case .none: break @@ -259,6 +251,9 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } else { installationActionImage = PresentationResourcesItemList.plusIconImage(item.theme) } + case .selection: + rightInset += 16.0 + checkImage = PresentationResourcesItemList.checkIconImage(item.theme) } var unreadImage: UIImage? @@ -430,15 +425,25 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { case .none: strongSelf.installationActionNode.isHidden = true strongSelf.installationActionImageNode.isHidden = true + strongSelf.selectionIconNode.isHidden = true case let .installation(installed): strongSelf.installationActionImageNode.isHidden = false strongSelf.installationActionNode.isHidden = false + strongSelf.selectionIconNode.isHidden = true strongSelf.installationActionNode.isUserInteractionEnabled = !installed if let image = installationActionImage { let imageSize = image.size strongSelf.installationActionImageNode.image = image strongSelf.installationActionImageNode.frame = CGRect(origin: CGPoint(x: installationActionFrame.minX + floor((installationActionFrame.size.width - imageSize.width) / 2.0), y: installationActionFrame.minY + floor((installationActionFrame.size.height - imageSize.height) / 2.0)), size: imageSize) } + case .selection: + strongSelf.installationActionNode.isHidden = true + strongSelf.installationActionImageNode.isHidden = true + strongSelf.selectionIconNode.isHidden = false + if let image = checkImage { + strongSelf.selectionIconNode.image = image + strongSelf.selectionIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - image.size.width - floor((44.0 - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + } } if strongSelf.backgroundNode.supernode == nil { diff --git a/TelegramUI/LegacyAttachmentMenu.swift b/TelegramUI/LegacyAttachmentMenu.swift index 1b2ba09b64..8fdd5421e4 100644 --- a/TelegramUI/LegacyAttachmentMenu.swift +++ b/TelegramUI/LegacyAttachmentMenu.swift @@ -6,15 +6,15 @@ import SwiftSignalKit import Postbox import TelegramCore -func legacyAttachmentMenu(account: Account, peer: Peer, saveEditedPhotos: Bool, allowGrouping: Bool, theme: PresentationTheme, strings: PresentationStrings, parentController: LegacyController, recentlyUsedInlineBots: [Peer], openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void) -> TGMenuSheetController { +func legacyAttachmentMenu(account: Account, peer: Peer, editingMessage: Bool, saveEditedPhotos: Bool, allowGrouping: Bool, theme: PresentationTheme, strings: PresentationStrings, parentController: LegacyController, recentlyUsedInlineBots: [Peer], openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void) -> TGMenuSheetController { let controller = TGMenuSheetController(context: parentController.context, dark: false)! controller.dismissesByOutsideTap = true controller.hasSwipeGesture = true - controller.maxHeight = 445.0// - TGMenuSheetButtonItemViewHeight + controller.maxHeight = 445.0 controller.forceFullScreen = true var itemViews: [Any] = [] - let carouselItem = TGAttachmentCarouselItemView(context: parentController.context, camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType, saveEditedPhotos: saveEditedPhotos, allowGrouping: allowGrouping)! + let carouselItem = TGAttachmentCarouselItemView(context: parentController.context, camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType, saveEditedPhotos: saveEditedPhotos, allowGrouping: !editingMessage && allowGrouping, allowSelection: !editingMessage, allowEditing: true, document: false)! carouselItem.suggestionContext = legacySuggestionContext(account: account, peerId: peer.id) carouselItem.recipientName = peer.displayTitle carouselItem.cameraPressed = { [weak controller] cameraView in @@ -48,31 +48,35 @@ func legacyAttachmentMenu(account: Account, peer: Peer, saveEditedPhotos: Bool, })! itemViews.append(fileItem) - let locationItem = TGMenuSheetButtonItemView(title: strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in - controller?.dismiss(animated: true) - openMap() - })! - itemViews.append(locationItem) + if !editingMessage { + let locationItem = TGMenuSheetButtonItemView(title: strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + controller?.dismiss(animated: true) + openMap() + })! + itemViews.append(locationItem) - let contactItem = TGMenuSheetButtonItemView(title: strings.Conversation_Contact, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in - controller?.dismiss(animated: true) - openContacts() - })! - itemViews.append(contactItem) + let contactItem = TGMenuSheetButtonItemView(title: strings.Conversation_Contact, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + controller?.dismiss(animated: true) + openContacts() + })! + itemViews.append(contactItem) + } carouselItem.underlyingViews = [galleryItem, fileItem] - for i in 0 ..< min(20, recentlyUsedInlineBots.count) { - let peer = recentlyUsedInlineBots[i] - let addressName = peer.addressName - if let addressName = addressName { - let botItem = TGMenuSheetButtonItemView(title: "@" + addressName, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in - controller?.dismiss(animated: true) - - selectRecentlyUsedInlineBot(peer) - })! - botItem.overflow = true - itemViews.append(botItem) + if !editingMessage { + for i in 0 ..< min(20, recentlyUsedInlineBots.count) { + let peer = recentlyUsedInlineBots[i] + let addressName = peer.addressName + if let addressName = addressName { + let botItem = TGMenuSheetButtonItemView(title: "@" + addressName, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + controller?.dismiss(animated: true) + + selectRecentlyUsedInlineBot(peer) + })! + botItem.overflow = true + itemViews.append(botItem) + } } } diff --git a/TelegramUI/LegacyCamera.swift b/TelegramUI/LegacyCamera.swift index 27b2394f3f..b545326621 100644 --- a/TelegramUI/LegacyCamera.swift +++ b/TelegramUI/LegacyCamera.swift @@ -4,6 +4,7 @@ import Display import UIKit import TelegramCore import Postbox +import SwiftSignalKit func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, saveCapturedPhotos: Bool, sendMessagesWithSignals: @escaping ([Any]?) -> Void) { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } @@ -12,16 +13,18 @@ func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmen legacyController.statusBar.statusBarStyle = .Hide legacyController.deferScreenEdgeGestures = [.top] + + let isSecretChat = peer.id.namespace == Namespaces.Peer.SecretChat let controller: TGCameraController if let cameraView = cameraView, let previewView = cameraView.previewView() { - controller = TGCameraController(context: legacyController.context, saveEditedPhotos: true, saveCapturedMedia: true, camera: previewView.camera, previewView: previewView, intent: TGCameraControllerGenericIntent) + controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat, camera: previewView.camera, previewView: previewView, intent: TGCameraControllerGenericIntent) } else { controller = TGCameraController() } controller.isImportant = true - controller.shouldStoreCapturedAssets = saveCapturedPhotos + controller.shouldStoreCapturedAssets = saveCapturedPhotos && !isSecretChat controller.allowCaptions = true controller.inhibitDocumentCaptions = false controller.suggestionContext = legacySuggestionContext(account: account, peerId: peer.id) @@ -113,3 +116,132 @@ func presentedLegacyCamera(account: Account, peer: Peer, cameraView: TGAttachmen parentController.present(legacyController, in: .window(.root)) } + +func presentedLegacyShortcutCamera(account: Account, saveCapturedMedia: Bool, saveEditedPhotos: Bool, parentController: ViewController) { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) + legacyController.supportedOrientations = .portrait + legacyController.statusBar.statusBarStyle = .Hide + + legacyController.deferScreenEdgeGestures = [.top] + + let controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveEditedPhotos, saveCapturedMedia: saveCapturedMedia)! + controller.shortcut = false + controller.isImportant = true + controller.shouldStoreCapturedAssets = saveCapturedMedia + controller.allowCaptions = true + + let screenSize = parentController.view.bounds.size + let startFrame = CGRect(x: 0, y: screenSize.height, width: screenSize.width, height: screenSize.height) + + legacyController.bind(controller: controller) + legacyController.controllerLoaded = { [weak controller] in + if let controller = controller { + controller.beginTransitionIn(from: startFrame) + } + } + + controller.finishedTransitionOut = { [weak legacyController] in + legacyController?.dismiss() + } + + controller.customDismissBlock = { [weak legacyController] in + legacyController?.dismiss() + } + + controller.finishedWithResults = { [weak controller, weak parentController, weak legacyController] overlayController, selectionContext, editingContext, currentItem in + if let selectionContext = selectionContext, let editingContext = editingContext { + let signals = TGCameraController.resultSignals(for: selectionContext, editingContext: editingContext, currentItem: currentItem, storeAssets: saveCapturedMedia, saveEditedPhotos: saveEditedPhotos, descriptionGenerator: legacyAssetPickerItemGenerator()) + if let parentController = parentController { + parentController.present(ShareController(account: account, subject: .fromExternal({ peerIds, text in + return legacyAssetPickerEnqueueMessages(account: account, signals: signals!) + |> mapToSignal { messages -> Signal in + let resultSignals = peerIds.map({ peerId in + return enqueueMessages(account: account, peerId: peerId, messages: messages) + |> mapToSignal { _ -> Signal in + return .complete() + } + }) + return combineLatest(resultSignals) + |> mapToSignal { _ -> Signal in + return .complete() + } + |> then(.single(ShareControllerExternalStatus.done)) + } + }), saveToCameraRoll: false, showInChat: nil, externalShare: false), in: .window(.root)) + } + } + + //legacyController?.dismissWithAnimation() + } + + parentController.present(legacyController, in: .window(.root)) + + + /*TGCameraControllerWindow *controllerWindow = [[TGCameraControllerWindow alloc] initWithManager:[[TGLegacyComponentsContext shared] makeOverlayWindowManager] parentController:TGAppDelegateInstance.rootController contentController:controller]; + controllerWindow.hidden = false; + + CGSize screenSize = TGScreenSize(); + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) + controllerWindow.frame = CGRectMake(0, 0, screenSize.width, screenSize.height); + + CGRect startFrame = CGRectMake(0, screenSize.height, screenSize.width, screenSize.height); + [controller beginTransitionInFromRect:startFrame]; + + __weak TGCameraController *weakCameraController = controller; + controller.finishedWithResults = ^(TGOverlayController *controller, TGMediaSelectionContext *selectionContext, TGMediaEditingContext *editingContext, id currentItem) + { + __autoreleasing NSString *disabledMessage = nil; + if (![TGApplicationFeatures isPhotoUploadEnabledForPeerType:TGApplicationFeaturePeerPrivate disabledMessage:&disabledMessage]) + { + [TGCustomAlertView presentAlertWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil]; + return; + } + + __strong TGCameraController *strongCameraController = weakCameraController; + if (strongCameraController == nil) + return; + + [TGCameraController showTargetController:[TGCameraController resultSignalsForSelectionContext:selectionContext editingContext:editingContext currentItem:currentItem storeAssets:false saveEditedPhotos:false descriptionGenerator:^id(id item, NSString *caption, NSArray *entities, __unused NSString *stickers) + { + if ([item isKindOfClass:[NSDictionary class]]) + { + NSDictionary *dict = (NSDictionary *)item; + NSString *type = dict[@"type"]; + + if ([type isEqualToString:@"editedPhoto"]) + { + NSMutableDictionary *result = [[NSMutableDictionary alloc] init]; + result[@"type"] = @"image"; + result[@"image"] = dict[@"image"]; + if (caption.length > 0) + result[@"caption"] = caption; + if (entities.count > 0) + result[@"entities"] = entities; + if (dict[@"stickers"] != nil) + result[@"stickers"] = dict[@"stickers"]; + + return result; + } + else if ([type isEqualToString:@"cameraVideo"]) + { + NSMutableDictionary *result = [[NSMutableDictionary alloc] init]; + result[@"type"] = @"cameraVideo"; + result[@"url"] = dict[@"url"]; + if (dict[@"adjustments"] != nil) + result[@"adjustments"] = dict[@"adjustments"]; + if (entities.count > 0) + result[@"entities"] = entities; + if (dict[@"stickers"] != nil) + result[@"stickers"] = dict[@"stickers"]; + if (dict[@"previewImage"] != nil) + result[@"previewImage"] = dict[@"previewImage"]; + + return result; + } + } + + return nil; + }] cameraController:strongCameraController resultController:controller navigationController:(TGNavigationController *)controller.navigationController]; + };*/ +} diff --git a/TelegramUI/LegacyInstantVideoController.swift b/TelegramUI/LegacyInstantVideoController.swift index b4b77d9f13..c911c79689 100644 --- a/TelegramUI/LegacyInstantVideoController.swift +++ b/TelegramUI/LegacyInstantVideoController.swift @@ -91,13 +91,16 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, legacyController.bind(controller: baseController) legacyController.presentationCompleted = { [weak legacyController, weak baseController] in if let legacyController = legacyController, let baseController = baseController { - //let controllerTheme = TGVideoMessageCaptureControllerTheme(darkBackground: theme.rootController.statusBar.style.style == .White, panelSeparatorColor: theme.chat.inputPanel.panelStrokeColor, panelBackgroundColor: theme.chat.inputPanel.panelBackgroundColor, panelTime: theme.chat.inputPanel.primaryTextColor, panelDotColor: theme.chat.inputPanel.mediaRecordingDotColor, panelAccentColor: theme.chat.inputPanel.panelControlAccentColor) let inputPanelTheme = theme.chat.inputPanel - let controller = TGVideoMessageCaptureController(context: legacyController.context, assets: TGVideoMessageCaptureControllerAssets(send: PresentationResourcesChat.chatInputPanelSendButtonImage(theme)!, slideToCancel:PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)!, actionDelete: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor))!, transitionInView: { + var uploadInterface: LegacyLiveUploadInterface? + if peerId.namespace != Namespaces.Peer.SecretChat { + uploadInterface = LegacyLiveUploadInterface(account: account) + } + let controller = TGVideoMessageCaptureController(context: legacyController.context, assets: TGVideoMessageCaptureControllerAssets(send: PresentationResourcesChat.chatInputPanelSendButtonImage(theme)!, slideToCancel: PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme)!, actionDelete: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor))!, transitionInView: { return nil }, parentController: baseController, controlsFrame: panelFrame, isAlreadyLocked: { return false - }, liveUploadInterface: LegacyLiveUploadInterface(account: account), pallete: TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor))! + }, liveUploadInterface: uploadInterface, pallete: TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor))! controller.finishedWithVideo = { videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments in guard let videoUrl = videoUrl else { return diff --git a/TelegramUI/LegacyMediaPickers.swift b/TelegramUI/LegacyMediaPickers.swift index eccb13fa4a..eaf1f5643e 100644 --- a/TelegramUI/LegacyMediaPickers.swift +++ b/TelegramUI/LegacyMediaPickers.swift @@ -173,7 +173,7 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, [Any]?, String?) -> [A } } -func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: [Any]) -> Signal<[EnqueueMessage], NoError> { +func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signal<[EnqueueMessage], NoError> { return Signal { subscriber in let disposable = SSignal.combineSignals(signals).start(next: { anyValues in var messages: [EnqueueMessage] = [] diff --git a/TelegramUI/ManagedAudioSession.swift b/TelegramUI/ManagedAudioSession.swift index ea2771dae4..bb5faad657 100644 --- a/TelegramUI/ManagedAudioSession.swift +++ b/TelegramUI/ManagedAudioSession.swift @@ -129,6 +129,7 @@ public final class ManagedAudioSession { private var isHeadsetPluggedInValue = false private let outputsToHeadphonesSubscribers = Bag<(Bool) -> Void>() private let isActiveSubscribers = Bag<(Bool) -> Void>() + private let isPlaybackActiveSubscribers = Bag<(Bool) -> Void>() init() { let queue = self.queue @@ -228,6 +229,29 @@ public final class ManagedAudioSession { } |> runOn(queue) } + public func isPlaybackActive() -> Signal { + let queue = self.queue + return Signal { [weak self] subscriber in + if let strongSelf = self { + subscriber.putNext(strongSelf.currentTypeAndOutputMode?.0 == .play) + + let index = strongSelf.isPlaybackActiveSubscribers.add({ value in + subscriber.putNext(value) + }) + + return ActionDisposable { + queue.async { + if let strongSelf = self { + strongSelf.isPlaybackActiveSubscribers.remove(index) + } + } + } + } else { + return EmptyDisposable + } + } |> runOn(queue) + } + func push(audioSessionType: ManagedAudioSessionType, outputMode: AudioSessionOutputMode = .system, once: Bool = false, activate: @escaping (AudioSessionActivationState) -> Void, deactivate: @escaping () -> Signal) -> Disposable { return self.push(audioSessionType: audioSessionType, once: once, manualActivate: { control in control.setup() @@ -423,6 +447,7 @@ public final class ManagedAudioSession { self.deactivateTimer = nil let wasActive = self.currentTypeAndOutputMode != nil + let wasPlaybackActive = self.currentTypeAndOutputMode?.0 == .play self.currentTypeAndOutputMode = nil print("ManagedAudioSession setting active false") @@ -437,6 +462,11 @@ public final class ManagedAudioSession { subscriber(false) } } + if wasPlaybackActive { + for subscriber in self.isPlaybackActiveSubscribers.copyItems() { + subscriber(false) + } + } } private func setup(type: ManagedAudioSessionType, outputMode: AudioSessionOutputMode) { @@ -444,6 +474,7 @@ public final class ManagedAudioSession { self.deactivateTimer = nil let wasActive = self.currentTypeAndOutputMode != nil + let wasPlaybackActive = self.currentTypeAndOutputMode?.0 == .play if self.currentTypeAndOutputMode == nil || self.currentTypeAndOutputMode! != (type, outputMode) { self.currentTypeAndOutputMode = (type, outputMode) @@ -463,6 +494,11 @@ public final class ManagedAudioSession { subscriber(true) } } + if !wasPlaybackActive && self.currentTypeAndOutputMode?.0 == .play { + for subscriber in self.isPlaybackActiveSubscribers.copyItems() { + subscriber(true) + } + } } private func setupOutputMode(_ outputMode: AudioSessionOutputMode) throws { diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index f292c6f07e..6e1b22a936 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -65,8 +65,8 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { } } return false - }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, navigationController: { return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings) + }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + }, presentController: { _, _ in }, navigationController: { return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings) let listNode = ChatHistoryListNode(account: account, chatLocation: .peer(updatedPlaylistPeerId), tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false)) listNode.preloadPages = true diff --git a/TelegramUI/NavigateToChatController.swift b/TelegramUI/NavigateToChatController.swift index 685e8c0d63..9afc37f5c9 100644 --- a/TelegramUI/NavigateToChatController.swift +++ b/TelegramUI/NavigateToChatController.swift @@ -41,7 +41,13 @@ public func navigateToChatController(navigationController: NavigationController, public func isOverlayControllerForChatNotificationOverlayPresentation(_ controller: ViewController) -> Bool { if controller is GalleryController || controller is AvatarGalleryController || controller is ThemeGalleryController || controller is InstantPageGalleryController { return true - } else { - return false } + + if controller.isNodeLoaded { + if let backgroundColor = controller.displayNode.backgroundColor, !backgroundColor.isEqual(UIColor.clear) { + return true + } + } + + return false } diff --git a/TelegramUI/OpenResolvedUrl.swift b/TelegramUI/OpenResolvedUrl.swift index 36623fad3d..ae0042dac9 100644 --- a/TelegramUI/OpenResolvedUrl.swift +++ b/TelegramUI/OpenResolvedUrl.swift @@ -2,6 +2,7 @@ import Foundation import TelegramCore import Postbox import Display +import SwiftSignalKit func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, navigationController: NavigationController?, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, present: (ViewController, Any?) -> Void) { switch resolvedUrl { @@ -11,8 +12,18 @@ func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, navigationCon openPeer(peerId, .chat(textInputState: nil, messageId: nil)) case let .botStart(peerId, payload): openPeer(peerId, .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive))) - case let .groupBotStart(peerId, payload): - break + case let .groupBotStart(botPeerId, payload): + 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: { + if let navigationController = navigationController { + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) + } + controller?.dismiss() + }) + } + present(controller, ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) case let .channelMessage(peerId, messageId): openPeer(peerId, .chat(textInputState: nil, messageId: messageId)) case let .stickerPack(name): diff --git a/TelegramUI/OpenUrl.swift b/TelegramUI/OpenUrl.swift index 6084eead00..2429ab050b 100644 --- a/TelegramUI/OpenUrl.swift +++ b/TelegramUI/OpenUrl.swift @@ -233,7 +233,7 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre valid = true } - if valid { + if valid && GlobalExperimentalSettings.enablePassport { if let botId = botId, let scope = scope, let publicKey = publicKey { let controller = SecureIdAuthController(account: account, mode: .form(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: botId), scope: scope, publicKey: publicKey, opaquePayload: opaquePayload)) diff --git a/TelegramUI/OverlayPlayerControllerNode.swift b/TelegramUI/OverlayPlayerControllerNode.swift index 48c52c2145..b99e78e5eb 100644 --- a/TelegramUI/OverlayPlayerControllerNode.swift +++ b/TelegramUI/OverlayPlayerControllerNode.swift @@ -54,13 +54,19 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec } else { return false } - }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, navigationController: { return nil - }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in + }, presentGlobalOverlayController: { _, _ in + }, callPeer: { _ in + }, longTap: { _ in + }, openCheckoutOrReceipt: { _ in + }, openSearch: { + }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in + }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings) self.dimNode = ASDisplayNode() diff --git a/TelegramUI/OverlayStatusController.swift b/TelegramUI/OverlayStatusController.swift new file mode 100644 index 0000000000..e62d3e223b --- /dev/null +++ b/TelegramUI/OverlayStatusController.swift @@ -0,0 +1,87 @@ +import Foundation +import Display + +import LegacyComponents + +enum OverlayStatusControllerType { + case success +} + +private final class OverlayStatusControllerNode: ViewControllerTracingNode { + private let dismissed: () -> Void + private let progressController: TGProgressWindowController + + init(theme: PresentationTheme, dismissed: @escaping () -> Void) { + self.dismissed = dismissed + self.progressController = TGProgressWindowController(light: theme.actionSheet.backgroundType == .light) + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + + self.view.addSubview(self.progressController.view) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.progressController.view.frame = CGRect(origin: CGPoint(), size: layout.size) + self.progressController.updateLayout() + } + + func begin() { + self.progressController.dismiss(success: { [weak self] in + self?.dismissed() + }) + } +} + +final class OverlayStatusController: ViewController { + private let theme: PresentationTheme + private let type: OverlayStatusControllerType + + private var animatedDidAppear = false + + private var controllerNode: OverlayStatusControllerNode { + return self.displayNode as! OverlayStatusControllerNode + } + + init(theme: PresentationTheme, type: OverlayStatusControllerType) { + self.theme = theme + self.type = type + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = OverlayStatusControllerNode(theme: self.theme, dismissed: { [weak self] in + self?.dismiss() + }) + + self.displayNodeDidLoad() + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedDidAppear { + self.animatedDidAppear = true + self.controllerNode.begin() + } + } + + override func dismiss(completion: (() -> Void)? = nil) { + self.presentingViewController?.dismiss(animated: false, completion: nil) + } +} diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 30c22128fa..39719a8fe3 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -155,6 +155,7 @@ public class PeerMediaCollectionController: TelegramController { },sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in + }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url in if let strongSelf = self { if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { @@ -218,13 +219,13 @@ public class PeerMediaCollectionController: TelegramController { }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in + }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings) self.controllerInteraction = controllerInteraction self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _ in }, setupEditMessage: { _ in - }, setupEditMessageMedia: { }, beginMessageSelection: { _ in }, deleteSelectedMessages: { [weak self] in if let strongSelf = self { @@ -277,6 +278,12 @@ public class PeerMediaCollectionController: TelegramController { })) } } + }, reportSelectedMessages: { [weak self] in + if let strongSelf = self, let messageIds = strongSelf.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { + strongSelf.present(peerReportOptionsController(account: strongSelf.account, subject: .messages(Array(messageIds).sorted()), present: { c, a in + self?.present(c, in: .window(.root), with: a) + }), in: .window(.root)) + } }, deleteMessages: { _ in }, forwardSelectedMessages: { [weak self] in if let strongSelf = self { diff --git a/TelegramUI/PeerReportController.swift b/TelegramUI/PeerReportController.swift new file mode 100644 index 0000000000..6baf8804e2 --- /dev/null +++ b/TelegramUI/PeerReportController.swift @@ -0,0 +1,255 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +enum PeerReportSubject { + case peer(PeerId) + case messages([MessageId]) +} + +private enum PeerReportOption { + case spam + case violence + case pornoghraphy + case other +} + +func peerReportOptionsController(account: Account, subject: PeerReportSubject, present: @escaping (ViewController, Any?) -> Void) -> ViewController { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme)) + + let options: [PeerReportOption] = [ + .spam, + .violence, + .pornoghraphy, + .other + ] + + var items: [ActionSheetItem] = [] + for option in options { + let title: String + switch option { + case .spam: + title = presentationData.strings.ReportPeer_ReasonSpam + case .violence: + title = presentationData.strings.ReportPeer_ReasonViolence + case .pornoghraphy: + title = presentationData.strings.ReportPeer_ReasonPornography + /*case .copyright: + title = presentationData.strings.ReportPeer_ReasonCopyright*/ + case .other: + title = presentationData.strings.ReportPeer_ReasonOther + } + items.append(ActionSheetButtonItem(title: title, action: { [weak controller] in + //account.reportPeer#ae189d5f peer:InputPeer reason:ReportReason = Bool; + var reportReason: ReportReason? + switch option { + case .spam: + reportReason = .spam + case .violence: + reportReason = .violence + case .pornoghraphy: + reportReason = .porno + case .other: + break + } + if let reportReason = reportReason { + switch subject { + case let .peer(peerId): + let _ = (reportPeer(account: account, peerId: peerId, reason: reportReason) + |> deliverOnMainQueue).start(completed: { + present(OverlayStatusController(theme: presentationData.theme, type: .success), nil) + }) + case let .messages(messageIds): + let _ = (reportPeerMessages(account: account, messageIds: messageIds, reason: reportReason) + |> deliverOnMainQueue).start(completed: { + present(OverlayStatusController(theme: presentationData.theme, type: .success), nil) + }) + } + } else { + controller?.present(peerReportController(account: account, subject: subject), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + + controller?.dismissAnimated() + })) + } + + controller.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { [weak controller] in + controller?.dismissAnimated() + }) + ]) + ]) + return controller +} + +private final class PeerReportControllerArguments { + let updateText: (String) -> Void + + init(updateText: @escaping (String) -> Void) { + self.updateText = updateText + } +} + +private enum PeerReportControllerSection: Int32 { + case text +} + +private enum PeerReportControllerEntryTag: ItemListItemTag { + case text + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? PeerReportControllerEntryTag { + switch self { + case .text: + if case .text = other { + return true + } else { + return false + } + } + } else { + return false + } + } +} + +private enum PeerReportControllerEntry: ItemListNodeEntry { + case text(PresentationTheme, String, String) + + var section: ItemListSectionId { + switch self { + case .text: + return PeerReportControllerSection.text.rawValue + } + } + + var stableId: Int32 { + switch self { + case .text: + return 0 + } + } + + static func ==(lhs: PeerReportControllerEntry, rhs: PeerReportControllerEntry) -> Bool { + switch lhs { + case let .text(lhsTheme, lhsText, lhsValue): + if case let .text(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + } + } + + static func <(lhs: PeerReportControllerEntry, rhs: PeerReportControllerEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: PeerReportControllerArguments) -> ListViewItem { + switch self { + case let .text(theme, title, value): + return ItemListMultilineInputItem(theme: theme, text: value, placeholder: title, maxLength: nil, sectionId: self.section, style: .blocks, textUpdated: { text in + arguments.updateText(text) + }, tag: PeerReportControllerEntryTag.text, action: {}) + } + } +} + +private struct PeerReportControllerState: Equatable { + var isReporting: Bool = false + var text: String = "" +} + +private func peerReportControllerEntries(presentationData: PresentationData, state: PeerReportControllerState) -> [PeerReportControllerEntry] { + var entries: [PeerReportControllerEntry] = [] + + entries.append(.text(presentationData.theme, presentationData.strings.ReportPeer_ReasonOther_Placeholder, state.text)) + + return entries +} + +private func peerReportController(account: Account, subject: PeerReportSubject) -> ViewController { + var dismissImpl: (() -> Void)? + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + + let statePromise = ValuePromise(PeerReportControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: PeerReportControllerState()) + let updateState: ((PeerReportControllerState) -> PeerReportControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let arguments = PeerReportControllerArguments(updateText: { text in + updateState { state in + var state = state + state.text = text + return state + } + }) + + let reportDisposable = MetaDisposable() + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, PeerReportControllerEntry.ItemGenerationArguments)) in + let rightButton: ItemListNavigationButton + if state.isReporting { + rightButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) + } else { + rightButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: !state.text.isEmpty, action: { + var text: String = "" + updateState { state in + var state = state + if !state.isReporting && !state.text.isEmpty { + text = state.text + state.isReporting = true + } + return state + } + + if !text.isEmpty { + let completed: () -> Void = { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .success), nil) + dismissImpl?() + } + switch subject { + case let .peer(peerId): + reportDisposable.set((reportPeer(account: account, peerId: peerId, reason: .custom(text)) + |> deliverOnMainQueue).start(completed: { + completed() + })) + case let .messages(messageIds): + reportDisposable.set((reportPeerMessages(account: account, messageIds: messageIds, reason: .custom(text)) + |> deliverOnMainQueue).start(completed: { + completed() + })) + } + } + }) + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ReportPeer_ReasonOther_Title), leftNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }), rightNavigationButton: rightButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: peerReportControllerEntries(presentationData: presentationData, state: state), style: .blocks, focusItemTag: PeerReportControllerEntryTag.text) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + reportDisposable.dispose() + } + + let controller = ItemListController(account: account, state: signal) + presentControllerImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + dismissImpl = { [weak controller] in + controller?.view.endEditing(true) + controller?.dismiss() + } + return controller +} diff --git a/TelegramUI/PresentationCall.swift b/TelegramUI/PresentationCall.swift index 36dd018461..7a0784a185 100644 --- a/TelegramUI/PresentationCall.swift +++ b/TelegramUI/PresentationCall.swift @@ -378,10 +378,14 @@ public final class PresentationCall { self.reportedIncomingCall = true self.callKitIntegration?.reportIncomingCall(uuid: self.internalId, handle: "\(self.peerId.id)", displayTitle: self.peer?.displayTitle ?? "Unknown", completion: { [weak self] error in if let error = error { - Logger.shared.log("PresentationCall", "reportIncomingCall error \(error)") - Queue.mainQueue().async { - if let strongSelf = self { - strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp) + if error.domain == "com.apple.CallKit.error.incomingcall" && error.code == -3 { + Logger.shared.log("PresentationCall", "reportIncomingCall device in DND mode") + } else { + Logger.shared.log("PresentationCall", "reportIncomingCall error \(error)") + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp) + } } } } diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index 163ed93289..b0e1f27c97 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -40,6 +40,7 @@ enum PresentationResourceKey: Int32 { case itemListReorderIndicatorIcon case itemListAddPersonIcon case itemListAddPhoneIcon + case itemListClearInputIcon case itemListStickerItemUnreadDot case itemListVerifiedPeerIcon @@ -120,6 +121,7 @@ enum PresentationResourceKey: Int32 { case chatInputMediaPanelTrendingIconImage case chatInputMediaPanelSettingsIconImage case chatInputMediaPanelAddPackButtonImage + case chatInputMediaPanelGridSetupImage case chatInputButtonPanelButtonImage case chatInputButtonPanelButtonHighlightedImage diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index acf8d172dc..b7eb96a629 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -365,6 +365,12 @@ struct PresentationResourcesChat { }) } + static func chatInputMediaPanelGridSetupImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputMediaPanelGridSetupImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/GridSetupIcon"), color: theme.chat.inputMediaPanel.panelIconColor) + }) + } + static func chatInputButtonPanelButtonImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputButtonPanelButtonImage.rawValue, { theme in return generateInputPanelButtonBackgroundImage(fillColor: theme.chat.inputButtonPanel.buttonFillColor, strokeColor: theme.chat.inputButtonPanel.buttonStrokeColor) diff --git a/TelegramUI/PresentationResourcesItemList.swift b/TelegramUI/PresentationResourcesItemList.swift index dad68a454a..16f8ab5fad 100644 --- a/TelegramUI/PresentationResourcesItemList.swift +++ b/TelegramUI/PresentationResourcesItemList.swift @@ -117,4 +117,10 @@ struct PresentationResourcesItemList { }) }) } + + static func itemListClearInputIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListClearInputIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.rootController.activeNavigationSearchBar.inputIconColor) + }) + } } diff --git a/TelegramUI/PresentationStrings.swift b/TelegramUI/PresentationStrings.swift index d154807223..21da78f2f8 100644 --- a/TelegramUI/PresentationStrings.swift +++ b/TelegramUI/PresentationStrings.swift @@ -113,6 +113,7 @@ public final class PresentationStrings { } public let FastTwoStepSetup_PasswordSection: String public let FastTwoStepSetup_EmailSection: String + public let ChatList_MarkAsRead: String public let Cache_ClearCache: String public let Common_Close: String public let ChangePhoneNumberCode_Called: String @@ -179,13 +180,13 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Channel_AdminLog_MessageEdited, self._Channel_AdminLog_MessageEdited_r, [_0]) } public let Group_Setup_HistoryHidden: String - public let Your_cards_expiration_year_is_invalid: String - public let AccessDenied_MicrophoneRestricted: String private let _PHONE_CALL_REQUEST: String private let _PHONE_CALL_REQUEST_r: [(Int, NSRange)] public func PHONE_CALL_REQUEST(_ _1: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_PHONE_CALL_REQUEST, self._PHONE_CALL_REQUEST_r, [_1]) } + public let AccessDenied_MicrophoneRestricted: String + public let Your_cards_expiration_year_is_invalid: String public let GroupInfo_InviteByLink: String private let _Notification_LeftChat: String private let _Notification_LeftChat_r: [(Int, NSRange)] @@ -312,6 +313,7 @@ public final class PresentationStrings { } public let Month_ShortDecember: String public let Channel_SignMessages: String + public let ReportPeer_ReasonCopyright: String public let Appearance_Title: String public let Conversation_Moderate_Delete: String public let Conversation_CloudStorage_ChatStatus: String @@ -336,6 +338,7 @@ public final class PresentationStrings { public func DialogList_SingleRecordingAudioSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_DialogList_SingleRecordingAudioSuffix, self._DialogList_SingleRecordingAudioSuffix_r, [_0]) } + public let PrivacySettings_SyncContactsInfo: String public let Checkout_NewCard_CardholderNameTitle: String public let Settings_FAQ_Button: String private let _GroupInfo_AddParticipantConfirmation: String @@ -588,6 +591,7 @@ public final class PresentationStrings { } public let Checkout_NewCard_PostcodePlaceholder: String public let DialogList_DeleteConversationConfirmation: String + public let PrivacySettings_DeleteContactsSuccess: String public let AttachmentMenu_SendAsFile: String public let Watch_Conversation_Unblock: String public let Channel_AdminLog_MessagePreviousLink: String @@ -675,6 +679,7 @@ public final class PresentationStrings { public let Channel_EditAdmin_PermissionAddAdmins: String public let Conversation_SendMessage: String public let Notification_CallIncoming: String + public let PrivacySettings_SuggestFrequentContacts: String private let _MESSAGE_FWDS: String private let _MESSAGE_FWDS_r: [(Int, NSRange)] public func MESSAGE_FWDS(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { @@ -1039,6 +1044,7 @@ public final class PresentationStrings { public let Call_Decline: String public let UserInfo_AddPhone: String public let AutoNightTheme_Title: String + public let PrivacySettings_LinkPreviews: String public let Activity_PlayingGame: String public let CheckoutInfo_ShippingInfoStatePlaceholder: String public let SaveIncomingPhotosSettings_From: String @@ -1126,6 +1132,7 @@ public final class PresentationStrings { public let Privacy_Calls_NeverAllow: String public let Settings_About_Title: String public let PhoneNumberHelp_Help: String + public let PrivacySettings_SecretChats: String public let Channel_LinkItem: String public let Camera_Retake: String public let StickerPack_ShowStickers: String @@ -1590,9 +1597,11 @@ public final class PresentationStrings { } public let EnterPasscode_TouchId: String public let AuthSessions_LoggedInWithTelegram: String + public let PrivacySettings_SuggestFrequentContactsDisableNotice: String public let Checkout_ErrorInvoiceAlreadyPaid: String public let ChatAdmins_Title: String public let ChannelMembers_WhoCanAddMembers: String + public let PrivacySettings_SuggestFrequentContactsInfo: String public let PasscodeSettings_Help: String public let Conversation_EditingMessagePanelTitle: String public let Settings_AboutEmpty: String @@ -1806,6 +1815,7 @@ public final class PresentationStrings { public let Message_Photo: String public let Conversation_ReportSpam: String public let Camera_FlashAuto: String + public let PrivacySettings_LinkPreviewsInfo: String public let Call_ConnectionErrorMessage: String public let Stickers_FrequentlyUsed: String public let LastSeen_ALongTimeAgo: String @@ -2246,11 +2256,13 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Channel_AdminLog_MessageInvitedNameUsername, self._Channel_AdminLog_MessageInvitedNameUsername_r, [_1, _2]) } public let Compose_GroupTokenListPlaceholder: String + public let PrivacySettings_FrequentContacts: String public let Conversation_MessageDeliveryFailed: String public let Privacy_PaymentsClear_PaymentInfo: String public let Notifications_GroupNotifications: String public let CheckoutInfo_SaveInfoHelp: String public let Notification_Mute1hMin: String + public let PrivacySettings_SyncContacts: String public let StickerPacksSettings_ArchivedMasks_Info: String public let ChannelMembers_WhoCanAddMembers_AllMembers: String public let Channel_Edit_PrivatePublicLinkAlert: String @@ -2751,6 +2763,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_InviteText_SingleContact, self._InviteText_SingleContact_r, [_0]) } public let Channel_EditAdmin_CannotEdit: String + public let PrivacySettings_DeleteContacts: String public let LoginPassword_PasswordHelp: String public let BlockedUsers_Unblock: String public let AutoDownloadSettings_Cellular: String @@ -2875,6 +2888,7 @@ public final class PresentationStrings { public func Notification_ChangedGroupPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { return formatWithArgumentRanges(_Notification_ChangedGroupPhoto, self._Notification_ChangedGroupPhoto_r, [_0]) } + public let PrivacySettings_Contacts: String public let TwoStepAuth_RemovePassword: String public let Privacy_GroupsAndChannels_CustomHelp: String public let UserInfo_NotificationsDisable: String @@ -3005,6 +3019,7 @@ public final class PresentationStrings { } public let ChatSettings_Appearance: String public let Tour_Title1: String + public let Conversation_EditingCaptionPanelTitle: String public let Conversation_LinkDialogCopy: String private let _Notification_PinnedLocationMessage: String private let _Notification_PinnedLocationMessage_r: [(Int, NSRange)] @@ -3144,6 +3159,7 @@ public final class PresentationStrings { return formatWithArgumentRanges(_Conversation_Kilobytes, self._Conversation_Kilobytes_r, ["\(_0)"]) } public let Group_ErrorAddBlocked: String + public let ChatList_MarkAsUnread: String public let TwoStepAuth_AdditionalPassword: String public let MediaPicker_Videos: String public let BlockedUsers_AddNew: String @@ -3175,334 +3191,70 @@ public final class PresentationStrings { public let PrivacySettings_PasscodeAndFaceId: String public let Settings_ChatBackground: String public let TermsOfService_Confirm: String - 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 _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._PrivacyLastSeenSettings_AddUsers_zero, "\(value)") + return String(format: self._Watch_UserInfo_Mute_zero, "\(value)") case .one: - return String(format: self._PrivacyLastSeenSettings_AddUsers_one, "\(value)") + return String(format: self._Watch_UserInfo_Mute_one, "\(value)") case .two: - return String(format: self._PrivacyLastSeenSettings_AddUsers_two, "\(value)") + return String(format: self._Watch_UserInfo_Mute_two, "\(value)") case .few: - return String(format: self._PrivacyLastSeenSettings_AddUsers_few, "\(value)") + return String(format: self._Watch_UserInfo_Mute_few, "\(value)") case .many: - return String(format: self._PrivacyLastSeenSettings_AddUsers_many, "\(value)") + return String(format: self._Watch_UserInfo_Mute_many, "\(value)") case .other: - return String(format: self._PrivacyLastSeenSettings_AddUsers_other, "\(value)") + return String(format: self._Watch_UserInfo_Mute_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 { + 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._MuteFor_Days_zero, "\(value)") + return String(format: self._SharedMedia_Link_zero, "\(value)") case .one: - return String(format: self._MuteFor_Days_one, "\(value)") + return String(format: self._SharedMedia_Link_one, "\(value)") case .two: - return String(format: self._MuteFor_Days_two, "\(value)") + return String(format: self._SharedMedia_Link_two, "\(value)") case .few: - return String(format: self._MuteFor_Days_few, "\(value)") + return String(format: self._SharedMedia_Link_few, "\(value)") case .many: - return String(format: self._MuteFor_Days_many, "\(value)") + return String(format: self._SharedMedia_Link_many, "\(value)") case .other: - return String(format: self._MuteFor_Days_other, "\(value)") + return String(format: self._SharedMedia_Link_other, "\(value)") } } - private let _MessageTimer_Weeks_zero: String - private let _MessageTimer_Weeks_one: String - private let _MessageTimer_Weeks_two: String - private let _MessageTimer_Weeks_few: String - private let _MessageTimer_Weeks_many: String - private let _MessageTimer_Weeks_other: String - public func MessageTimer_Weeks(_ 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._MessageTimer_Weeks_zero, "\(value)") + return String(format: self._MessageTimer_Days_zero, "\(value)") case .one: - return String(format: self._MessageTimer_Weeks_one, "\(value)") + return String(format: self._MessageTimer_Days_one, "\(value)") case .two: - return String(format: self._MessageTimer_Weeks_two, "\(value)") + return String(format: self._MessageTimer_Days_two, "\(value)") case .few: - return String(format: self._MessageTimer_Weeks_few, "\(value)") + return String(format: self._MessageTimer_Days_few, "\(value)") case .many: - return String(format: self._MessageTimer_Weeks_many, "\(value)") + return String(format: self._MessageTimer_Days_many, "\(value)") case .other: - return String(format: self._MessageTimer_Weeks_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 _ForwardedVideos_zero: String - private let _ForwardedVideos_one: String - private let _ForwardedVideos_two: String - private let _ForwardedVideos_few: String - private let _ForwardedVideos_many: String - private let _ForwardedVideos_other: String - public func ForwardedVideos(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedVideos_zero, "\(value)") - case .one: - return String(format: self._ForwardedVideos_one, "\(value)") - case .two: - return String(format: self._ForwardedVideos_two, "\(value)") - case .few: - return String(format: self._ForwardedVideos_few, "\(value)") - case .many: - return String(format: self._ForwardedVideos_many, "\(value)") - case .other: - return String(format: self._ForwardedVideos_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 _SharedMedia_Generic_zero: String - private let _SharedMedia_Generic_one: String - private let _SharedMedia_Generic_two: String - private let _SharedMedia_Generic_few: String - private let _SharedMedia_Generic_many: String - private let _SharedMedia_Generic_other: String - public func SharedMedia_Generic(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._SharedMedia_Generic_zero, "\(value)") - case .one: - return String(format: self._SharedMedia_Generic_one, "\(value)") - case .two: - return String(format: self._SharedMedia_Generic_two, "\(value)") - case .few: - return String(format: self._SharedMedia_Generic_few, "\(value)") - case .many: - return String(format: self._SharedMedia_Generic_many, "\(value)") - case .other: - return String(format: self._SharedMedia_Generic_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 _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 _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 _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 _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 _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 _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)") + return String(format: self._MessageTimer_Days_other, "\(value)") } } private let _LiveLocationUpdated_MinutesAgo_zero: String @@ -3527,6 +3279,50 @@ public final class PresentationStrings { 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 @@ -3571,246 +3367,26 @@ public final class PresentationStrings { return String(format: self._GroupInfo_ParticipantCount_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 { + 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._SharedMedia_DeleteItemsConfirmation_zero, "\(value)") + return String(format: self._DialogList_LiveLocationChatsCount_zero, "\(value)") case .one: - return String(format: self._SharedMedia_DeleteItemsConfirmation_one, "\(value)") + return String(format: self._DialogList_LiveLocationChatsCount_one, "\(value)") case .two: - return String(format: self._SharedMedia_DeleteItemsConfirmation_two, "\(value)") + return String(format: self._DialogList_LiveLocationChatsCount_two, "\(value)") case .few: - return String(format: self._SharedMedia_DeleteItemsConfirmation_few, "\(value)") + return String(format: self._DialogList_LiveLocationChatsCount_few, "\(value)") case .many: - return String(format: self._SharedMedia_DeleteItemsConfirmation_many, "\(value)") + return String(format: self._DialogList_LiveLocationChatsCount_many, "\(value)") case .other: - return String(format: self._SharedMedia_DeleteItemsConfirmation_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 _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 _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 _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 _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 _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 _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 _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 _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 _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)") + return String(format: self._DialogList_LiveLocationChatsCount_other, "\(value)") } } private let _MessageTimer_Years_zero: String @@ -3835,26 +3411,26 @@ public final class PresentationStrings { return String(format: self._MessageTimer_Years_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 { + 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._MessageTimer_ShortDays_zero, "\(value)") + return String(format: self._ServiceMessage_GameScoreExtended_zero, "\(value)") case .one: - return String(format: self._MessageTimer_ShortDays_one, "\(value)") + return String(format: self._ServiceMessage_GameScoreExtended_one, "\(value)") case .two: - return String(format: self._MessageTimer_ShortDays_two, "\(value)") + return String(format: self._ServiceMessage_GameScoreExtended_two, "\(value)") case .few: - return String(format: self._MessageTimer_ShortDays_few, "\(value)") + return String(format: self._ServiceMessage_GameScoreExtended_few, "\(value)") case .many: - return String(format: self._MessageTimer_ShortDays_many, "\(value)") + return String(format: self._ServiceMessage_GameScoreExtended_many, "\(value)") case .other: - return String(format: self._MessageTimer_ShortDays_other, "\(value)") + return String(format: self._ServiceMessage_GameScoreExtended_other, "\(value)") } } private let _InviteText_ContactsCount_zero: String @@ -3879,246 +3455,70 @@ public final class PresentationStrings { return String(format: self._InviteText_ContactsCount_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 { + 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._SharedMedia_Video_zero, "\(value)") + return String(format: self._MessageTimer_ShortMinutes_zero, "\(value)") case .one: - return String(format: self._SharedMedia_Video_one, "\(value)") + return String(format: self._MessageTimer_ShortMinutes_one, "\(value)") case .two: - return String(format: self._SharedMedia_Video_two, "\(value)") + return String(format: self._MessageTimer_ShortMinutes_two, "\(value)") case .few: - return String(format: self._SharedMedia_Video_few, "\(value)") + return String(format: self._MessageTimer_ShortMinutes_few, "\(value)") case .many: - return String(format: self._SharedMedia_Video_many, "\(value)") + return String(format: self._MessageTimer_ShortMinutes_many, "\(value)") case .other: - return String(format: self._SharedMedia_Video_other, "\(value)") + return String(format: self._MessageTimer_ShortMinutes_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 { + 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._MessageTimer_Seconds_zero, "\(value)") + return String(format: self._Call_Seconds_zero, "\(value)") case .one: - return String(format: self._MessageTimer_Seconds_one, "\(value)") + return String(format: self._Call_Seconds_one, "\(value)") case .two: - return String(format: self._MessageTimer_Seconds_two, "\(value)") + return String(format: self._Call_Seconds_two, "\(value)") case .few: - return String(format: self._MessageTimer_Seconds_few, "\(value)") + return String(format: self._Call_Seconds_few, "\(value)") case .many: - return String(format: self._MessageTimer_Seconds_many, "\(value)") + return String(format: self._Call_Seconds_many, "\(value)") case .other: - return String(format: self._MessageTimer_Seconds_other, "\(value)") + return String(format: self._Call_Seconds_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 { + 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._MessageTimer_Months_zero, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_zero, "\(value)") case .one: - return String(format: self._MessageTimer_Months_one, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_one, "\(value)") case .two: - return String(format: self._MessageTimer_Months_two, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_two, "\(value)") case .few: - return String(format: self._MessageTimer_Months_few, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_few, "\(value)") case .many: - return String(format: self._MessageTimer_Months_many, "\(value)") + return String(format: self._Watch_LastSeen_MinutesAgo_many, "\(value)") case .other: - return String(format: self._MessageTimer_Months_other, "\(value)") - } - } - private let _MessageTimer_Hours_zero: String - private let _MessageTimer_Hours_one: String - private let _MessageTimer_Hours_two: String - private let _MessageTimer_Hours_few: String - private let _MessageTimer_Hours_many: String - private let _MessageTimer_Hours_other: String - public func MessageTimer_Hours(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MessageTimer_Hours_zero, "\(value)") - case .one: - return String(format: self._MessageTimer_Hours_one, "\(value)") - case .two: - return String(format: self._MessageTimer_Hours_two, "\(value)") - case .few: - return String(format: self._MessageTimer_Hours_few, "\(value)") - case .many: - return String(format: self._MessageTimer_Hours_many, "\(value)") - case .other: - return String(format: self._MessageTimer_Hours_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 _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 _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 _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)") - } - } - private let _ForwardedContacts_zero: String - private let _ForwardedContacts_one: String - private let _ForwardedContacts_two: String - private let _ForwardedContacts_few: String - private let _ForwardedContacts_many: String - private let _ForwardedContacts_other: String - public func ForwardedContacts(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedContacts_zero, "\(value)") - case .one: - return String(format: self._ForwardedContacts_one, "\(value)") - case .two: - return String(format: self._ForwardedContacts_two, "\(value)") - case .few: - return String(format: self._ForwardedContacts_few, "\(value)") - case .many: - return String(format: self._ForwardedContacts_many, "\(value)") - case .other: - return String(format: self._ForwardedContacts_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 _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)") + return String(format: self._Watch_LastSeen_MinutesAgo_other, "\(value)") } } private let _Media_SharePhoto_zero: String @@ -4143,26 +3543,26 @@ public final class PresentationStrings { return String(format: self._Media_SharePhoto_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 { + 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._AttachmentMenu_SendVideo_zero, "\(value)") + return String(format: self._Notification_GameScoreSelfSimple_zero, "\(value)") case .one: - return String(format: self._AttachmentMenu_SendVideo_one, "\(value)") + return String(format: self._Notification_GameScoreSelfSimple_one, "\(value)") case .two: - return String(format: self._AttachmentMenu_SendVideo_two, "\(value)") + return String(format: self._Notification_GameScoreSelfSimple_two, "\(value)") case .few: - return String(format: self._AttachmentMenu_SendVideo_few, "\(value)") + return String(format: self._Notification_GameScoreSelfSimple_few, "\(value)") case .many: - return String(format: self._AttachmentMenu_SendVideo_many, "\(value)") + return String(format: self._Notification_GameScoreSelfSimple_many, "\(value)") case .other: - return String(format: self._AttachmentMenu_SendVideo_other, "\(value)") + return String(format: self._Notification_GameScoreSelfSimple_other, "\(value)") } } private let _SharedMedia_Photo_zero: String @@ -4187,26 +3587,356 @@ public final class PresentationStrings { return String(format: self._SharedMedia_Photo_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 { + 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._MessageTimer_ShortWeeks_zero, "\(value)") + return String(format: self._StickerPack_RemoveStickerCount_zero, "\(value)") case .one: - return String(format: self._MessageTimer_ShortWeeks_one, "\(value)") + return String(format: self._StickerPack_RemoveStickerCount_one, "\(value)") case .two: - return String(format: self._MessageTimer_ShortWeeks_two, "\(value)") + return String(format: self._StickerPack_RemoveStickerCount_two, "\(value)") case .few: - return String(format: self._MessageTimer_ShortWeeks_few, "\(value)") + return String(format: self._StickerPack_RemoveStickerCount_few, "\(value)") case .many: - return String(format: self._MessageTimer_ShortWeeks_many, "\(value)") + return String(format: self._StickerPack_RemoveStickerCount_many, "\(value)") case .other: - return String(format: self._MessageTimer_ShortWeeks_other, "\(value)") + 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 @@ -4231,48 +3961,268 @@ public final class PresentationStrings { return String(format: self._Media_ShareItem_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 { + 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._StickerPack_RemoveStickerCount_zero, "\(value)") + return String(format: self._SharedMedia_Video_zero, "\(value)") case .one: - return String(format: self._StickerPack_RemoveStickerCount_one, "\(value)") + return String(format: self._SharedMedia_Video_one, "\(value)") case .two: - return String(format: self._StickerPack_RemoveStickerCount_two, "\(value)") + return String(format: self._SharedMedia_Video_two, "\(value)") case .few: - return String(format: self._StickerPack_RemoveStickerCount_few, "\(value)") + return String(format: self._SharedMedia_Video_few, "\(value)") case .many: - return String(format: self._StickerPack_RemoveStickerCount_many, "\(value)") + return String(format: self._SharedMedia_Video_many, "\(value)") case .other: - return String(format: self._StickerPack_RemoveStickerCount_other, "\(value)") + return String(format: self._SharedMedia_Video_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 { + 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._MessageTimer_ShortHours_zero, "\(value)") + return String(format: self._Forward_ConfirmMultipleFiles_zero, "\(value)") case .one: - return String(format: self._MessageTimer_ShortHours_one, "\(value)") + return String(format: self._Forward_ConfirmMultipleFiles_one, "\(value)") case .two: - return String(format: self._MessageTimer_ShortHours_two, "\(value)") + return String(format: self._Forward_ConfirmMultipleFiles_two, "\(value)") case .few: - return String(format: self._MessageTimer_ShortHours_few, "\(value)") + return String(format: self._Forward_ConfirmMultipleFiles_few, "\(value)") case .many: - return String(format: self._MessageTimer_ShortHours_many, "\(value)") + return String(format: self._Forward_ConfirmMultipleFiles_many, "\(value)") case .other: - return String(format: self._MessageTimer_ShortHours_other, "\(value)") + 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 @@ -4319,400 +4269,70 @@ public final class PresentationStrings { return String(format: self._UserCount_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 { + 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_AddMaskCount_zero, "\(value)") + return String(format: self._StickerPack_StickerCount_zero, "\(value)") case .one: - return String(format: self._StickerPack_AddMaskCount_one, "\(value)") + return String(format: self._StickerPack_StickerCount_one, "\(value)") case .two: - return String(format: self._StickerPack_AddMaskCount_two, "\(value)") + return String(format: self._StickerPack_StickerCount_two, "\(value)") case .few: - return String(format: self._StickerPack_AddMaskCount_few, "\(value)") + return String(format: self._StickerPack_StickerCount_few, "\(value)") case .many: - return String(format: self._StickerPack_AddMaskCount_many, "\(value)") + return String(format: self._StickerPack_StickerCount_many, "\(value)") case .other: - return String(format: self._StickerPack_AddMaskCount_other, "\(value)") + return String(format: self._StickerPack_StickerCount_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 { + 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._ForwardedMessages_zero, "\(value)") + return String(format: self._MessageTimer_Seconds_zero, "\(value)") case .one: - return String(format: self._ForwardedMessages_one, "\(value)") + return String(format: self._MessageTimer_Seconds_one, "\(value)") case .two: - return String(format: self._ForwardedMessages_two, "\(value)") + return String(format: self._MessageTimer_Seconds_two, "\(value)") case .few: - return String(format: self._ForwardedMessages_few, "\(value)") + return String(format: self._MessageTimer_Seconds_few, "\(value)") case .many: - return String(format: self._ForwardedMessages_many, "\(value)") + return String(format: self._MessageTimer_Seconds_many, "\(value)") case .other: - return String(format: self._ForwardedMessages_other, "\(value)") + return String(format: self._MessageTimer_Seconds_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 { + 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._Notification_GameScoreSimple_zero, "\(value)") + return String(format: self._LastSeen_HoursAgo_zero, "\(value)") case .one: - return String(format: self._Notification_GameScoreSimple_one, "\(value)") + return String(format: self._LastSeen_HoursAgo_one, "\(value)") case .two: - return String(format: self._Notification_GameScoreSimple_two, "\(value)") + return String(format: self._LastSeen_HoursAgo_two, "\(value)") case .few: - return String(format: self._Notification_GameScoreSimple_few, "\(value)") + return String(format: self._LastSeen_HoursAgo_few, "\(value)") case .many: - return String(format: self._Notification_GameScoreSimple_many, "\(value)") + return String(format: self._LastSeen_HoursAgo_many, "\(value)") case .other: - return String(format: self._Notification_GameScoreSimple_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 _ForwardedVideoMessages_zero: String - private let _ForwardedVideoMessages_one: String - private let _ForwardedVideoMessages_two: String - private let _ForwardedVideoMessages_few: String - private let _ForwardedVideoMessages_many: String - private let _ForwardedVideoMessages_other: String - public func ForwardedVideoMessages(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._ForwardedVideoMessages_zero, "\(value)") - case .one: - return String(format: self._ForwardedVideoMessages_one, "\(value)") - case .two: - return String(format: self._ForwardedVideoMessages_two, "\(value)") - case .few: - return String(format: self._ForwardedVideoMessages_few, "\(value)") - case .many: - return String(format: self._ForwardedVideoMessages_many, "\(value)") - case .other: - return String(format: self._ForwardedVideoMessages_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 _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 _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 _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 _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 _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 _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 _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 _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 _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 _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 _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 _MuteExpires_Minutes_zero: String - private let _MuteExpires_Minutes_one: String - private let _MuteExpires_Minutes_two: String - private let _MuteExpires_Minutes_few: String - private let _MuteExpires_Minutes_many: String - private let _MuteExpires_Minutes_other: String - public func MuteExpires_Minutes(_ value: Int32) -> String { - switch presentationStringsPluralizationForm(self.lc, value) { - case .zero: - return String(format: self._MuteExpires_Minutes_zero, "\(value)") - case .one: - return String(format: self._MuteExpires_Minutes_one, "\(value)") - case .two: - return String(format: self._MuteExpires_Minutes_two, "\(value)") - case .few: - return String(format: self._MuteExpires_Minutes_few, "\(value)") - case .many: - return String(format: self._MuteExpires_Minutes_many, "\(value)") - case .other: - return String(format: self._MuteExpires_Minutes_other, "\(value)") + return String(format: self._LastSeen_HoursAgo_other, "\(value)") } } private let _Map_ETAHours_zero: String @@ -4737,6 +4357,72 @@ public final class PresentationStrings { 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 @@ -4759,26 +4445,136 @@ public final class PresentationStrings { return String(format: self._Media_ShareVideo_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 { + 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._StickerPack_StickerCount_zero, "\(value)") + return String(format: self._Call_ShortMinutes_zero, "\(value)") case .one: - return String(format: self._StickerPack_StickerCount_one, "\(value)") + return String(format: self._Call_ShortMinutes_one, "\(value)") case .two: - return String(format: self._StickerPack_StickerCount_two, "\(value)") + return String(format: self._Call_ShortMinutes_two, "\(value)") case .few: - return String(format: self._StickerPack_StickerCount_few, "\(value)") + return String(format: self._Call_ShortMinutes_few, "\(value)") case .many: - return String(format: self._StickerPack_StickerCount_many, "\(value)") + return String(format: self._Call_ShortMinutes_many, "\(value)") case .other: - return String(format: self._StickerPack_StickerCount_other, "\(value)") + return String(format: self._Call_ShortMinutes_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 _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 _ForwardedVideoMessages_zero: String + private let _ForwardedVideoMessages_one: String + private let _ForwardedVideoMessages_two: String + private let _ForwardedVideoMessages_few: String + private let _ForwardedVideoMessages_many: String + private let _ForwardedVideoMessages_other: String + public func ForwardedVideoMessages(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedVideoMessages_zero, "\(value)") + case .one: + return String(format: self._ForwardedVideoMessages_one, "\(value)") + case .two: + return String(format: self._ForwardedVideoMessages_two, "\(value)") + case .few: + return String(format: self._ForwardedVideoMessages_few, "\(value)") + case .many: + return String(format: self._ForwardedVideoMessages_many, "\(value)") + case .other: + 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 { + 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 _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 _AttachmentMenu_SendItem_zero: String @@ -4803,158 +4599,378 @@ public final class PresentationStrings { return String(format: self._AttachmentMenu_SendItem_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 { + 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._Notification_GameScoreExtended_zero, "\(value)") + return String(format: self._StickerPack_RemoveMaskCount_zero, "\(value)") case .one: - return String(format: self._Notification_GameScoreExtended_one, "\(value)") + return String(format: self._StickerPack_RemoveMaskCount_one, "\(value)") case .two: - return String(format: self._Notification_GameScoreExtended_two, "\(value)") + return String(format: self._StickerPack_RemoveMaskCount_two, "\(value)") case .few: - return String(format: self._Notification_GameScoreExtended_few, "\(value)") + return String(format: self._StickerPack_RemoveMaskCount_few, "\(value)") case .many: - return String(format: self._Notification_GameScoreExtended_many, "\(value)") + return String(format: self._StickerPack_RemoveMaskCount_many, "\(value)") case .other: - return String(format: self._Notification_GameScoreExtended_other, "\(value)") + return String(format: self._StickerPack_RemoveMaskCount_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 { + private let _ForwardedContacts_zero: String + private let _ForwardedContacts_one: String + private let _ForwardedContacts_two: String + private let _ForwardedContacts_few: String + private let _ForwardedContacts_many: String + private let _ForwardedContacts_other: String + public func ForwardedContacts(_ value: Int32) -> String { switch presentationStringsPluralizationForm(self.lc, value) { case .zero: - return String(format: self._Notification_GameScoreSelfExtended_zero, "\(value)") + return String(format: self._ForwardedContacts_zero, "\(value)") case .one: - return String(format: self._Notification_GameScoreSelfExtended_one, "\(value)") + return String(format: self._ForwardedContacts_one, "\(value)") case .two: - return String(format: self._Notification_GameScoreSelfExtended_two, "\(value)") + return String(format: self._ForwardedContacts_two, "\(value)") case .few: - return String(format: self._Notification_GameScoreSelfExtended_few, "\(value)") + return String(format: self._ForwardedContacts_few, "\(value)") case .many: - return String(format: self._Notification_GameScoreSelfExtended_many, "\(value)") + return String(format: self._ForwardedContacts_many, "\(value)") case .other: - return String(format: self._Notification_GameScoreSelfExtended_other, "\(value)") + return String(format: self._ForwardedContacts_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 { + 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._SharedMedia_Link_zero, "\(value)") + return String(format: self._Conversation_StatusOnline_zero, "\(value)") case .one: - return String(format: self._SharedMedia_Link_one, "\(value)") + return String(format: self._Conversation_StatusOnline_one, "\(value)") case .two: - return String(format: self._SharedMedia_Link_two, "\(value)") + return String(format: self._Conversation_StatusOnline_two, "\(value)") case .few: - return String(format: self._SharedMedia_Link_few, "\(value)") + return String(format: self._Conversation_StatusOnline_few, "\(value)") case .many: - return String(format: self._SharedMedia_Link_many, "\(value)") + return String(format: self._Conversation_StatusOnline_many, "\(value)") case .other: - return String(format: self._SharedMedia_Link_other, "\(value)") + return String(format: self._Conversation_StatusOnline_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 { + 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._LastSeen_MinutesAgo_zero, "\(value)") + return String(format: self._Call_ShortSeconds_zero, "\(value)") case .one: - return String(format: self._LastSeen_MinutesAgo_one, "\(value)") + return String(format: self._Call_ShortSeconds_one, "\(value)") case .two: - return String(format: self._LastSeen_MinutesAgo_two, "\(value)") + return String(format: self._Call_ShortSeconds_two, "\(value)") case .few: - return String(format: self._LastSeen_MinutesAgo_few, "\(value)") + return String(format: self._Call_ShortSeconds_few, "\(value)") case .many: - return String(format: self._LastSeen_MinutesAgo_many, "\(value)") + return String(format: self._Call_ShortSeconds_many, "\(value)") case .other: - return String(format: self._LastSeen_MinutesAgo_other, "\(value)") + return String(format: self._Call_ShortSeconds_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 { + 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._Call_Minutes_zero, "\(value)") + return String(format: self._ForwardedLocations_zero, "\(value)") case .one: - return String(format: self._Call_Minutes_one, "\(value)") + return String(format: self._ForwardedLocations_one, "\(value)") case .two: - return String(format: self._Call_Minutes_two, "\(value)") + return String(format: self._ForwardedLocations_two, "\(value)") case .few: - return String(format: self._Call_Minutes_few, "\(value)") + return String(format: self._ForwardedLocations_few, "\(value)") case .many: - return String(format: self._Call_Minutes_many, "\(value)") + return String(format: self._ForwardedLocations_many, "\(value)") case .other: - return String(format: self._Call_Minutes_other, "\(value)") + return String(format: self._ForwardedLocations_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 { + 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._MessageTimer_ShortSeconds_zero, "\(value)") + return String(format: self._SharedMedia_DeleteItemsConfirmation_zero, "\(value)") case .one: - return String(format: self._MessageTimer_ShortSeconds_one, "\(value)") + return String(format: self._SharedMedia_DeleteItemsConfirmation_one, "\(value)") case .two: - return String(format: self._MessageTimer_ShortSeconds_two, "\(value)") + return String(format: self._SharedMedia_DeleteItemsConfirmation_two, "\(value)") case .few: - return String(format: self._MessageTimer_ShortSeconds_few, "\(value)") + return String(format: self._SharedMedia_DeleteItemsConfirmation_few, "\(value)") case .many: - return String(format: self._MessageTimer_ShortSeconds_many, "\(value)") + return String(format: self._SharedMedia_DeleteItemsConfirmation_many, "\(value)") case .other: - return String(format: self._MessageTimer_ShortSeconds_other, "\(value)") + return String(format: self._SharedMedia_DeleteItemsConfirmation_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 { + 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._SharedMedia_File_zero, "\(value)") + return String(format: self._ForwardedMessages_zero, "\(value)") case .one: - return String(format: self._SharedMedia_File_one, "\(value)") + return String(format: self._ForwardedMessages_one, "\(value)") case .two: - return String(format: self._SharedMedia_File_two, "\(value)") + return String(format: self._ForwardedMessages_two, "\(value)") case .few: - return String(format: self._SharedMedia_File_few, "\(value)") + return String(format: self._ForwardedMessages_few, "\(value)") case .many: - return String(format: self._SharedMedia_File_many, "\(value)") + return String(format: self._ForwardedMessages_many, "\(value)") case .other: - return String(format: self._SharedMedia_File_other, "\(value)") + 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 + private let _SharedMedia_Generic_few: String + private let _SharedMedia_Generic_many: String + private let _SharedMedia_Generic_other: String + public func SharedMedia_Generic(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_Generic_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_Generic_one, "\(value)") + case .two: + return String(format: self._SharedMedia_Generic_two, "\(value)") + case .few: + return String(format: self._SharedMedia_Generic_few, "\(value)") + case .many: + return String(format: self._SharedMedia_Generic_many, "\(value)") + case .other: + return String(format: self._SharedMedia_Generic_other, "\(value)") + } + } + private let _MuteExpires_Minutes_zero: String + private let _MuteExpires_Minutes_one: String + private let _MuteExpires_Minutes_two: String + private let _MuteExpires_Minutes_few: String + private let _MuteExpires_Minutes_many: String + private let _MuteExpires_Minutes_other: String + public func MuteExpires_Minutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteExpires_Minutes_zero, "\(value)") + case .one: + return String(format: self._MuteExpires_Minutes_one, "\(value)") + case .two: + return String(format: self._MuteExpires_Minutes_two, "\(value)") + case .few: + return String(format: self._MuteExpires_Minutes_few, "\(value)") + case .many: + return String(format: self._MuteExpires_Minutes_many, "\(value)") + case .other: + 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 { + 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 _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)") + } + } + private let _ForwardedVideos_zero: String + private let _ForwardedVideos_one: String + private let _ForwardedVideos_two: String + private let _ForwardedVideos_few: String + private let _ForwardedVideos_many: String + private let _ForwardedVideos_other: String + public func ForwardedVideos(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedVideos_zero, "\(value)") + case .one: + return String(format: self._ForwardedVideos_one, "\(value)") + case .two: + return String(format: self._ForwardedVideos_two, "\(value)") + case .few: + return String(format: self._ForwardedVideos_few, "\(value)") + case .many: + return String(format: self._ForwardedVideos_many, "\(value)") + case .other: + return String(format: self._ForwardedVideos_other, "\(value)") + } + } + private let _MessageTimer_Weeks_zero: String + private let _MessageTimer_Weeks_one: String + private let _MessageTimer_Weeks_two: String + private let _MessageTimer_Weeks_few: String + private let _MessageTimer_Weeks_many: String + private let _MessageTimer_Weeks_other: String + public func MessageTimer_Weeks(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Weeks_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Weeks_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Weeks_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Weeks_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Weeks_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Weeks_other, "\(value)") + } + } + private let _MessageTimer_Hours_zero: String + private let _MessageTimer_Hours_one: String + private let _MessageTimer_Hours_two: String + private let _MessageTimer_Hours_few: String + private let _MessageTimer_Hours_many: String + private let _MessageTimer_Hours_other: String + public func MessageTimer_Hours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Hours_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Hours_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Hours_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Hours_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Hours_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Hours_other, "\(value)") } } @@ -4989,6 +5005,7 @@ public final class PresentationStrings { self._DialogList_PinLimitError_r = extractArgumentRanges(self._DialogList_PinLimitError) self.FastTwoStepSetup_PasswordSection = getValue(dict, "FastTwoStepSetup.PasswordSection") self.FastTwoStepSetup_EmailSection = getValue(dict, "FastTwoStepSetup.EmailSection") + self.ChatList_MarkAsRead = getValue(dict, "ChatList.MarkAsRead") self.Cache_ClearCache = getValue(dict, "Cache.ClearCache") self.Common_Close = getValue(dict, "Common.Close") self.ChangePhoneNumberCode_Called = getValue(dict, "ChangePhoneNumberCode.Called") @@ -5034,10 +5051,10 @@ public final class PresentationStrings { self._Channel_AdminLog_MessageEdited = getValue(dict, "Channel.AdminLog.MessageEdited") self._Channel_AdminLog_MessageEdited_r = extractArgumentRanges(self._Channel_AdminLog_MessageEdited) self.Group_Setup_HistoryHidden = getValue(dict, "Group.Setup.HistoryHidden") - self.Your_cards_expiration_year_is_invalid = getValue(dict, "Your_cards_expiration_year_is_invalid") - self.AccessDenied_MicrophoneRestricted = getValue(dict, "AccessDenied.MicrophoneRestricted") self._PHONE_CALL_REQUEST = getValue(dict, "PHONE_CALL_REQUEST") self._PHONE_CALL_REQUEST_r = extractArgumentRanges(self._PHONE_CALL_REQUEST) + self.AccessDenied_MicrophoneRestricted = getValue(dict, "AccessDenied.MicrophoneRestricted") + self.Your_cards_expiration_year_is_invalid = getValue(dict, "Your_cards_expiration_year_is_invalid") self.GroupInfo_InviteByLink = getValue(dict, "GroupInfo.InviteByLink") self._Notification_LeftChat = getValue(dict, "Notification.LeftChat") self._Notification_LeftChat_r = extractArgumentRanges(self._Notification_LeftChat) @@ -5122,6 +5139,7 @@ public final class PresentationStrings { self._Channel_AdminLog_MessageToggleSignaturesOff_r = extractArgumentRanges(self._Channel_AdminLog_MessageToggleSignaturesOff) self.Month_ShortDecember = getValue(dict, "Month.ShortDecember") self.Channel_SignMessages = getValue(dict, "Channel.SignMessages") + self.ReportPeer_ReasonCopyright = getValue(dict, "ReportPeer.ReasonCopyright") self.Appearance_Title = getValue(dict, "Appearance.Title") self.Conversation_Moderate_Delete = getValue(dict, "Conversation.Moderate.Delete") self.Conversation_CloudStorage_ChatStatus = getValue(dict, "Conversation.CloudStorage.ChatStatus") @@ -5140,6 +5158,7 @@ public final class PresentationStrings { self.TwoStepAuth_GenericHelp = getValue(dict, "TwoStepAuth.GenericHelp") self._DialogList_SingleRecordingAudioSuffix = getValue(dict, "DialogList.SingleRecordingAudioSuffix") self._DialogList_SingleRecordingAudioSuffix_r = extractArgumentRanges(self._DialogList_SingleRecordingAudioSuffix) + self.PrivacySettings_SyncContactsInfo = getValue(dict, "PrivacySettings.SyncContactsInfo") self.Checkout_NewCard_CardholderNameTitle = getValue(dict, "Checkout.NewCard.CardholderNameTitle") self.Settings_FAQ_Button = getValue(dict, "Settings.FAQ_Button") self._GroupInfo_AddParticipantConfirmation = getValue(dict, "GroupInfo.AddParticipantConfirmation") @@ -5320,6 +5339,7 @@ public final class PresentationStrings { self._Target_ShareGameConfirmationPrivate_r = extractArgumentRanges(self._Target_ShareGameConfirmationPrivate) self.Checkout_NewCard_PostcodePlaceholder = getValue(dict, "Checkout.NewCard.PostcodePlaceholder") self.DialogList_DeleteConversationConfirmation = getValue(dict, "DialogList.DeleteConversationConfirmation") + self.PrivacySettings_DeleteContactsSuccess = getValue(dict, "PrivacySettings.DeleteContactsSuccess") self.AttachmentMenu_SendAsFile = getValue(dict, "AttachmentMenu.SendAsFile") self.Watch_Conversation_Unblock = getValue(dict, "Watch.Conversation.Unblock") self.Channel_AdminLog_MessagePreviousLink = getValue(dict, "Channel.AdminLog.MessagePreviousLink") @@ -5386,6 +5406,7 @@ public final class PresentationStrings { self.Channel_EditAdmin_PermissionAddAdmins = getValue(dict, "Channel.EditAdmin.PermissionAddAdmins") self.Conversation_SendMessage = getValue(dict, "Conversation.SendMessage") self.Notification_CallIncoming = getValue(dict, "Notification.CallIncoming") + self.PrivacySettings_SuggestFrequentContacts = getValue(dict, "PrivacySettings.SuggestFrequentContacts") self._MESSAGE_FWDS = getValue(dict, "MESSAGE_FWDS") self._MESSAGE_FWDS_r = extractArgumentRanges(self._MESSAGE_FWDS) self.Map_OpenInYandexMaps = getValue(dict, "Map.OpenInYandexMaps") @@ -5657,6 +5678,7 @@ public final class PresentationStrings { self.Call_Decline = getValue(dict, "Call.Decline") self.UserInfo_AddPhone = getValue(dict, "UserInfo.AddPhone") self.AutoNightTheme_Title = getValue(dict, "AutoNightTheme.Title") + self.PrivacySettings_LinkPreviews = getValue(dict, "PrivacySettings.LinkPreviews") self.Activity_PlayingGame = getValue(dict, "Activity.PlayingGame") self.CheckoutInfo_ShippingInfoStatePlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoStatePlaceholder") self.SaveIncomingPhotosSettings_From = getValue(dict, "SaveIncomingPhotosSettings.From") @@ -5723,6 +5745,7 @@ public final class PresentationStrings { self.Privacy_Calls_NeverAllow = getValue(dict, "Privacy.Calls.NeverAllow") self.Settings_About_Title = getValue(dict, "Settings.About.Title") self.PhoneNumberHelp_Help = getValue(dict, "PhoneNumberHelp.Help") + self.PrivacySettings_SecretChats = getValue(dict, "PrivacySettings.SecretChats") self.Channel_LinkItem = getValue(dict, "Channel.LinkItem") self.Camera_Retake = getValue(dict, "Camera.Retake") self.StickerPack_ShowStickers = getValue(dict, "StickerPack.ShowStickers") @@ -6037,9 +6060,11 @@ public final class PresentationStrings { self._CHANNEL_MESSAGE_VIDEO_r = extractArgumentRanges(self._CHANNEL_MESSAGE_VIDEO) self.EnterPasscode_TouchId = getValue(dict, "EnterPasscode.TouchId") self.AuthSessions_LoggedInWithTelegram = getValue(dict, "AuthSessions.LoggedInWithTelegram") + self.PrivacySettings_SuggestFrequentContactsDisableNotice = getValue(dict, "PrivacySettings.SuggestFrequentContactsDisableNotice") self.Checkout_ErrorInvoiceAlreadyPaid = getValue(dict, "Checkout.ErrorInvoiceAlreadyPaid") self.ChatAdmins_Title = getValue(dict, "ChatAdmins.Title") self.ChannelMembers_WhoCanAddMembers = getValue(dict, "ChannelMembers.WhoCanAddMembers") + self.PrivacySettings_SuggestFrequentContactsInfo = getValue(dict, "PrivacySettings.SuggestFrequentContactsInfo") self.PasscodeSettings_Help = getValue(dict, "PasscodeSettings.Help") self.Conversation_EditingMessagePanelTitle = getValue(dict, "Conversation.EditingMessagePanelTitle") self.Settings_AboutEmpty = getValue(dict, "Settings.AboutEmpty") @@ -6184,6 +6209,7 @@ public final class PresentationStrings { self.Message_Photo = getValue(dict, "Message.Photo") self.Conversation_ReportSpam = getValue(dict, "Conversation.ReportSpam") self.Camera_FlashAuto = getValue(dict, "Camera.FlashAuto") + self.PrivacySettings_LinkPreviewsInfo = getValue(dict, "PrivacySettings.LinkPreviewsInfo") self.Call_ConnectionErrorMessage = getValue(dict, "Call.ConnectionErrorMessage") self.Stickers_FrequentlyUsed = getValue(dict, "Stickers.FrequentlyUsed") self.LastSeen_ALongTimeAgo = getValue(dict, "LastSeen.ALongTimeAgo") @@ -6477,11 +6503,13 @@ public final class PresentationStrings { self._Channel_AdminLog_MessageInvitedNameUsername = getValue(dict, "Channel.AdminLog.MessageInvitedNameUsername") self._Channel_AdminLog_MessageInvitedNameUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageInvitedNameUsername) self.Compose_GroupTokenListPlaceholder = getValue(dict, "Compose.GroupTokenListPlaceholder") + self.PrivacySettings_FrequentContacts = getValue(dict, "PrivacySettings.FrequentContacts") self.Conversation_MessageDeliveryFailed = getValue(dict, "Conversation.MessageDeliveryFailed") self.Privacy_PaymentsClear_PaymentInfo = getValue(dict, "Privacy.PaymentsClear.PaymentInfo") self.Notifications_GroupNotifications = getValue(dict, "Notifications.GroupNotifications") self.CheckoutInfo_SaveInfoHelp = getValue(dict, "CheckoutInfo.SaveInfoHelp") self.Notification_Mute1hMin = getValue(dict, "Notification.Mute1hMin") + self.PrivacySettings_SyncContacts = getValue(dict, "PrivacySettings.SyncContacts") self.StickerPacksSettings_ArchivedMasks_Info = getValue(dict, "StickerPacksSettings.ArchivedMasks.Info") self.ChannelMembers_WhoCanAddMembers_AllMembers = getValue(dict, "ChannelMembers.WhoCanAddMembers.AllMembers") self.Channel_Edit_PrivatePublicLinkAlert = getValue(dict, "Channel.Edit.PrivatePublicLinkAlert") @@ -6820,6 +6848,7 @@ public final class PresentationStrings { self._InviteText_SingleContact = getValue(dict, "InviteText.SingleContact") self._InviteText_SingleContact_r = extractArgumentRanges(self._InviteText_SingleContact) self.Channel_EditAdmin_CannotEdit = getValue(dict, "Channel.EditAdmin.CannotEdit") + self.PrivacySettings_DeleteContacts = getValue(dict, "PrivacySettings.DeleteContacts") self.LoginPassword_PasswordHelp = getValue(dict, "LoginPassword.PasswordHelp") self.BlockedUsers_Unblock = getValue(dict, "BlockedUsers.Unblock") self.AutoDownloadSettings_Cellular = getValue(dict, "AutoDownloadSettings.Cellular") @@ -6908,6 +6937,7 @@ public final class PresentationStrings { self.Conversation_DiscardVoiceMessageDescription = getValue(dict, "Conversation.DiscardVoiceMessageDescription") self._Notification_ChangedGroupPhoto = getValue(dict, "Notification.ChangedGroupPhoto") self._Notification_ChangedGroupPhoto_r = extractArgumentRanges(self._Notification_ChangedGroupPhoto) + self.PrivacySettings_Contacts = getValue(dict, "PrivacySettings.Contacts") self.TwoStepAuth_RemovePassword = getValue(dict, "TwoStepAuth.RemovePassword") self.Privacy_GroupsAndChannels_CustomHelp = getValue(dict, "Privacy.GroupsAndChannels.CustomHelp") self.UserInfo_NotificationsDisable = getValue(dict, "UserInfo.NotificationsDisable") @@ -7005,6 +7035,7 @@ public final class PresentationStrings { self._DialogList_AwaitingEncryption_r = extractArgumentRanges(self._DialogList_AwaitingEncryption) self.ChatSettings_Appearance = getValue(dict, "ChatSettings.Appearance") self.Tour_Title1 = getValue(dict, "Tour.Title1") + self.Conversation_EditingCaptionPanelTitle = getValue(dict, "Conversation.EditingCaptionPanelTitle") self.Conversation_LinkDialogCopy = getValue(dict, "Conversation.LinkDialogCopy") self._Notification_PinnedLocationMessage = getValue(dict, "Notification.PinnedLocationMessage") self._Notification_PinnedLocationMessage_r = extractArgumentRanges(self._Notification_PinnedLocationMessage) @@ -7099,6 +7130,7 @@ public final class PresentationStrings { self._Conversation_Kilobytes = getValue(dict, "Conversation.Kilobytes") self._Conversation_Kilobytes_r = extractArgumentRanges(self._Conversation_Kilobytes) self.Group_ErrorAddBlocked = getValue(dict, "Group.ErrorAddBlocked") + self.ChatList_MarkAsUnread = getValue(dict, "ChatList.MarkAsUnread") self.TwoStepAuth_AdditionalPassword = getValue(dict, "TwoStepAuth.AdditionalPassword") self.MediaPicker_Videos = getValue(dict, "MediaPicker.Videos") self.BlockedUsers_AddNew = getValue(dict, "BlockedUsers.AddNew") @@ -7127,102 +7159,42 @@ 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._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_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._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._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._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._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._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._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._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._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._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._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._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._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._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) @@ -7235,198 +7207,240 @@ public final class PresentationStrings { 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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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) @@ -7439,180 +7453,198 @@ public final class PresentationStrings { self._UserCount_few = getValueWithForm(dict, "UserCount", .few) self._UserCount_many = getValueWithForm(dict, "UserCount", .many) self._UserCount_other = getValueWithForm(dict, "UserCount", .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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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._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_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._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._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._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._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._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_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._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._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._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._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._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._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._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._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._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._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._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._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) } } diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index 964aa4d95d..39f1ce5368 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -440,7 +440,9 @@ public final class PresentationThemeChatBubble { public let selectionControlFillColor: UIColor public let selectionControlForegroundColor: UIColor - public init(incomingFillColor: UIColor, incomingFillHighlightedColor: UIColor, incomingStrokeColor: UIColor, outgoingFillColor: UIColor, outgoingFillHighlightedColor: UIColor, outgoingStrokeColor: UIColor, freeformFillColor: UIColor, freeformFillHighlightedColor: UIColor, freeformStrokeColor: UIColor, infoFillColor: UIColor, infoStrokeColor: UIColor, incomingPrimaryTextColor: UIColor, incomingSecondaryTextColor: UIColor, incomingLinkTextColor: UIColor, incomingLinkHighlightColor: UIColor, outgoingPrimaryTextColor: UIColor, outgoingSecondaryTextColor: UIColor, outgoingLinkTextColor: UIColor, outgoingLinkHighlightColor: UIColor, infoPrimaryTextColor: UIColor, infoLinkTextColor: UIColor, incomingAccentTextColor: UIColor, outgoingAccentTextColor: UIColor, incomingAccentControlColor: UIColor, outgoingAccentControlColor: UIColor, incomingMediaActiveControlColor: UIColor, outgoingMediaActiveControlColor: UIColor, incomingMediaInactiveControlColor: UIColor, outgoingMediaInactiveControlColor: UIColor, outgoingCheckColor: UIColor, incomingPendingActivityColor: UIColor, outgoingPendingActivityColor: UIColor, mediaDateAndStatusFillColor: UIColor, mediaDateAndStatusTextColor: UIColor, incomingFileTitleColor: UIColor, outgoingFileTitleColor: UIColor, incomingFileDescriptionColor: UIColor, outgoingFileDescriptionColor: UIColor, incomingFileDurationColor: UIColor, outgoingFileDurationColor: UIColor, shareButtonFillColor: UIColor, shareButtonStrokeColor: UIColor, shareButtonForegroundColor: UIColor, mediaOverlayControlBackgroundColor: UIColor, mediaOverlayControlForegroundColor: UIColor, actionButtonsIncomingFillColor: UIColor, actionButtonsIncomingStrokeColor: UIColor, actionButtonsIncomingTextColor: UIColor, actionButtonsOutgoingFillColor: UIColor, actionButtonsOutgoingStrokeColor: UIColor, actionButtonsOutgoingTextColor: UIColor, selectionControlBorderColor: UIColor, selectionControlFillColor: UIColor, selectionControlForegroundColor: UIColor) { + public let mediaHighlightOverlayColor: UIColor + + public init(incomingFillColor: UIColor, incomingFillHighlightedColor: UIColor, incomingStrokeColor: UIColor, outgoingFillColor: UIColor, outgoingFillHighlightedColor: UIColor, outgoingStrokeColor: UIColor, freeformFillColor: UIColor, freeformFillHighlightedColor: UIColor, freeformStrokeColor: UIColor, infoFillColor: UIColor, infoStrokeColor: UIColor, incomingPrimaryTextColor: UIColor, incomingSecondaryTextColor: UIColor, incomingLinkTextColor: UIColor, incomingLinkHighlightColor: UIColor, outgoingPrimaryTextColor: UIColor, outgoingSecondaryTextColor: UIColor, outgoingLinkTextColor: UIColor, outgoingLinkHighlightColor: UIColor, infoPrimaryTextColor: UIColor, infoLinkTextColor: UIColor, incomingAccentTextColor: UIColor, outgoingAccentTextColor: UIColor, incomingAccentControlColor: UIColor, outgoingAccentControlColor: UIColor, incomingMediaActiveControlColor: UIColor, outgoingMediaActiveControlColor: UIColor, incomingMediaInactiveControlColor: UIColor, outgoingMediaInactiveControlColor: UIColor, outgoingCheckColor: UIColor, incomingPendingActivityColor: UIColor, outgoingPendingActivityColor: UIColor, mediaDateAndStatusFillColor: UIColor, mediaDateAndStatusTextColor: UIColor, incomingFileTitleColor: UIColor, outgoingFileTitleColor: UIColor, incomingFileDescriptionColor: UIColor, outgoingFileDescriptionColor: UIColor, incomingFileDurationColor: UIColor, outgoingFileDurationColor: UIColor, shareButtonFillColor: UIColor, shareButtonStrokeColor: UIColor, shareButtonForegroundColor: UIColor, mediaOverlayControlBackgroundColor: UIColor, mediaOverlayControlForegroundColor: UIColor, actionButtonsIncomingFillColor: UIColor, actionButtonsIncomingStrokeColor: UIColor, actionButtonsIncomingTextColor: UIColor, actionButtonsOutgoingFillColor: UIColor, actionButtonsOutgoingStrokeColor: UIColor, actionButtonsOutgoingTextColor: UIColor, selectionControlBorderColor: UIColor, selectionControlFillColor: UIColor, selectionControlForegroundColor: UIColor, mediaHighlightOverlayColor: UIColor) { self.incomingFillColor = incomingFillColor self.incomingFillHighlightedColor = incomingFillHighlightedColor self.incomingStrokeColor = incomingStrokeColor @@ -505,6 +507,8 @@ public final class PresentationThemeChatBubble { self.selectionControlBorderColor = selectionControlBorderColor self.selectionControlFillColor = selectionControlFillColor self.selectionControlForegroundColor = selectionControlForegroundColor + + self.mediaHighlightOverlayColor = mediaHighlightOverlayColor } } diff --git a/TelegramUI/PrivacyAndSecurityController.swift b/TelegramUI/PrivacyAndSecurityController.swift index 1be877ed3d..8bd734e6c8 100644 --- a/TelegramUI/PrivacyAndSecurityController.swift +++ b/TelegramUI/PrivacyAndSecurityController.swift @@ -18,8 +18,9 @@ private final class PrivacyAndSecurityControllerArguments { let updateSecretChatLinkPreviews: (Bool) -> Void let deleteContacts: () -> Void let updateSyncContacts: (Bool) -> Void + let updateSuggestFrequentContacts: (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, clearPaymentInfo: @escaping () -> Void, updateSecretChatLinkPreviews: @escaping (Bool) -> Void, deleteContacts: @escaping () -> Void, updateSyncContacts: @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, clearPaymentInfo: @escaping () -> Void, updateSecretChatLinkPreviews: @escaping (Bool) -> Void, deleteContacts: @escaping () -> Void, updateSyncContacts: @escaping (Bool) -> Void, updateSuggestFrequentContacts: @escaping (Bool) -> Void) { self.account = account self.openBlockedUsers = openBlockedUsers self.openLastSeenPrivacy = openLastSeenPrivacy @@ -33,6 +34,7 @@ private final class PrivacyAndSecurityControllerArguments { self.updateSecretChatLinkPreviews = updateSecretChatLinkPreviews self.deleteContacts = deleteContacts self.updateSyncContacts = updateSyncContacts + self.updateSuggestFrequentContacts = updateSuggestFrequentContacts } } @@ -43,6 +45,7 @@ private enum PrivacyAndSecuritySection: Int32 { case payment case secretChatLinkPreviews case contacts + case frequentContacts } private enum PrivacyAndSecurityEntry: ItemListNodeEntry { @@ -68,6 +71,9 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { 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) var section: ItemListSectionId { switch self { @@ -83,6 +89,8 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return PrivacyAndSecuritySection.secretChatLinkPreviews.rawValue case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo: return PrivacyAndSecuritySection.contacts.rawValue + case .frequentContactsHeader, .frequentContacts, .frequentContactsInfo: + return PrivacyAndSecuritySection.frequentContacts.rawValue } } @@ -132,6 +140,12 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return 20 case .syncContactsInfo: return 21 + case .frequentContactsHeader: + return 22 + case .frequentContacts: + return 23 + case .frequentContactsInfo: + return 24 } } @@ -263,12 +277,30 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { } 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 .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 { + return true + } else { + return false + } } } @@ -346,6 +378,14 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { }) 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): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } @@ -355,12 +395,14 @@ private struct PrivacyAndSecurityControllerState: Equatable { 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) { + 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 { @@ -376,24 +418,31 @@ private struct PrivacyAndSecurityControllerState: Equatable { 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) + 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) + 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) + 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) + 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) } } @@ -424,7 +473,7 @@ private func stringForSelectiveSettings(strings: PresentationStrings, settings: } } -private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool) -> [PrivacyAndSecurityEntry] { +private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool) -> [PrivacyAndSecurityEntry] { var entries: [PrivacyAndSecurityEntry] = [] entries.append(.privacyHeader(presentationData.theme, presentationData.strings.PrivacySettings_PrivacyTitle)) @@ -474,14 +523,18 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD entries.append(.paymentInfo(presentationData.theme, presentationData.strings.Privacy_PaymentsClearInfoHelp)) } - entries.append(.secretChatLinkPreviewsHeader(presentationData.theme, "SECRET CHATS")) - entries.append(.secretChatLinkPreviews(presentationData.theme, "Link previews", secretChatLinkPreviews ?? true)) - entries.append(.secretChatLinkPreviewsInfo(presentationData.theme, "Link previews will be generated on Telegram servers. We do not store data about the links you send.")) + 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, "CONTACTS")) - entries.append(.deleteContacts(presentationData.theme, "Delete Synced Contacts", !state.deletingContacts)) - entries.append(.syncContacts(presentationData.theme, "Sync Contacts", synchronizeDeviceContacts)) - entries.append(.syncContactsInfo(presentationData.theme, "Turn on to continuously sync contacts from this device with your account.")) + 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)) return entries } @@ -725,7 +778,7 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign return state } let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "All your contacts were deleted from the server.", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) + 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: {})])) } @@ -735,18 +788,47 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign 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() + } }) let previousState = Atomic(value: nil) let preferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.contactSynchronizationSettings])) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, privacySettingsPromise.get(), account.postbox.combinedView(keys: [.noticeEntry(ApplicationSpecificNotice.secretChatLinkPreviewsKey()), preferencesKey])) - |> map { presentationData, state, privacySettings, combined -> (ItemListControllerState, (ItemListNodeState, PrivacyAndSecurityEntry.ItemGenerationArguments)) in + actionsDisposable.add(managedUpdatedRecentPeers(postbox: account.postbox, network: account.network).start()) + + 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: {}) @@ -762,7 +844,7 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign } } - let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts), style: .blocks, animateChanges: animateChanges) + let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts, frequentContacts: suggestRecentPeers), style: .blocks, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/ProxyServerSettingsController.swift b/TelegramUI/ProxyServerSettingsController.swift index f4411c8eb8..a2c33a938f 100644 --- a/TelegramUI/ProxyServerSettingsController.swift +++ b/TelegramUI/ProxyServerSettingsController.swift @@ -199,7 +199,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } }, action: {}) case let .credentialsSecret(theme, placeholder, text): - return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(), text: text, placeholder: placeholder, type: .regular, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.secret = value @@ -310,10 +310,10 @@ func proxyServerSettingsController(account: Account, currentSettings: ProxyServe var result: String switch state.mode { case .mtp: - result = "tg://proxy?server=\(state.host)&port=\(state.port)" + result = "https://t.me/proxy?server=\(state.host)&port=\(state.port)" result += "&secret=\((state.secret as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" case .socks5: - result = "tg://socks?server=\(state.host)&port=\(state.port)" + result = "https://t.me/socks?server=\(state.host)&port=\(state.port)" if !state.username.isEmpty { result += "&user=\((state.username as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")&pass=\((state.password as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" } diff --git a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift index 740cba0190..daed2def35 100644 --- a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift +++ b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift @@ -15,6 +15,8 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { override init() { self.button = HighlightableButtonNode() self.button.isUserInteractionEnabled = false + self.button.titleNode.maximumNumberOfLines = 2 + self.button.titleNode.truncationMode = .byTruncatingMiddle super.init() @@ -53,7 +55,7 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { case .participant: text = interfaceState.strings.Conversation_EncryptionProcessing } - self.button.setAttributedTitle(NSAttributedString(string: text, font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.primaryTextColor), for: []) + self.button.setAttributedTitle(NSAttributedString(string: text, font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.primaryTextColor, paragraphAlignment: .center), for: []) case .active, .terminated: break } @@ -70,6 +72,6 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return 47.0 + return 45.0 } } diff --git a/TelegramUI/StickerPackPreviewController.swift b/TelegramUI/StickerPackPreviewController.swift index 3155d23fba..eaf24df06e 100644 --- a/TelegramUI/StickerPackPreviewController.swift +++ b/TelegramUI/StickerPackPreviewController.swift @@ -11,6 +11,7 @@ final class StickerPackPreviewController: ViewController { } private var animatedIn = false + private var dismissed = false private let account: Account private weak var parentNavigationController: NavigationController? @@ -145,6 +146,11 @@ final class StickerPackPreviewController: ViewController { } override func dismiss(completion: (() -> Void)? = nil) { + if !self.dismissed { + self.dismissed = true + } else { + return + } self.controllerNode.animateOut(completion: completion) } diff --git a/TelegramUI/StickerPackPreviewControllerNode.swift b/TelegramUI/StickerPackPreviewControllerNode.swift index ba90e5fc8b..1420f70ef5 100644 --- a/TelegramUI/StickerPackPreviewControllerNode.swift +++ b/TelegramUI/StickerPackPreviewControllerNode.swift @@ -292,7 +292,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } } - let titleSize = self.contentTitleNode.updateLayout(CGSize(width: contentContainerFrame.size.width - 54.0, height: CGFloat.greatestFiniteMagnitude)) + let titleSize = self.contentTitleNode.updateLayout(CGSize(width: contentContainerFrame.size.width - 76.0, height: CGFloat.greatestFiniteMagnitude)) let titleFrame = CGRect(origin: CGPoint(x: contentContainerFrame.minX + floor((contentContainerFrame.size.width - titleSize.width) / 2.0), y: self.contentBackgroundNode.frame.minY + 15.0), size: titleSize) let deltaTitlePosition = CGPoint(x: titleFrame.midX - self.contentTitleNode.frame.midX, y: titleFrame.midY - self.contentTitleNode.frame.midY) self.contentTitleNode.frame = titleFrame @@ -429,7 +429,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol switch stickerPack { case let .result(info, items, installed): if installed { - let _ = removeStickerPackInteractively(postbox: self.account.postbox, id: info.id).start() + let _ = removeStickerPackInteractively(postbox: self.account.postbox, id: info.id, option: .delete).start() self.cancelButtonPressed() } else { let _ = addStickerPackInteractively(postbox: self.account.postbox, info: info, items: items).start() diff --git a/TelegramUI/StickerPaneSearchBarNode.swift b/TelegramUI/StickerPaneSearchBarNode.swift index dcd930b390..37bd4ad851 100644 --- a/TelegramUI/StickerPaneSearchBarNode.swift +++ b/TelegramUI/StickerPaneSearchBarNode.swift @@ -338,7 +338,7 @@ class StickerPaneSearchBarNode: ASDisplayNode, UITextFieldDelegate { } func transitionOut(to node: StickerPaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { - let targetTextBackgroundFrame = node.convert(node.backgroundNode.frame, to: self) + let targetTextBackgroundFrame = node.view.convert(node.backgroundNode.view.frame, to: self.view) let duration: Double = 0.5 let timingFunction = kCAMediaTimingFunctionSpring diff --git a/TelegramUI/StickerPaneSearchContainerNode.swift b/TelegramUI/StickerPaneSearchContainerNode.swift index 15ac9c56aa..1b6af6de79 100644 --- a/TelegramUI/StickerPaneSearchContainerNode.swift +++ b/TelegramUI/StickerPaneSearchContainerNode.swift @@ -144,6 +144,8 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { private let searchBar: StickerPaneSearchBarNode private let trendingPane: ChatMediaInputTrendingPane private let gridNode: GridNode + private let notFoundNode: ASImageNode + private let notFoundLabel: ImmediateTextNode private var validLayout: CGSize? @@ -169,14 +171,29 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { self.gridNode = GridNode() + self.notFoundNode = ASImageNode() + self.notFoundNode.isLayerBacked = true + self.notFoundNode.displayWithoutProcessing = true + self.notFoundNode.displaysAsynchronously = false + self.notFoundNode.clipsToBounds = false + self.notFoundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/StickersNotFoundIcon"), color: theme.list.freeMonoIcon) + + self.notFoundLabel = ImmediateTextNode() + self.notFoundLabel.displaysAsynchronously = false + self.notFoundLabel.isLayerBacked = true + self.notFoundLabel.attributedText = NSAttributedString(string: strings.Stickers_NoStickersFound, font: Font.medium(14.0), textColor: theme.list.freeTextColor) + self.notFoundNode.addSubnode(self.notFoundLabel) + self.gridNode.isHidden = true self.trendingPane.isHidden = false + self.notFoundNode.isHidden = true super.init() self.addSubnode(self.backgroundNode) self.addSubnode(self.trendingPane) self.addSubnode(self.gridNode) + self.addSubnode(self.notFoundNode) self.addSubnode(self.searchBar) self.gridNode.scrollView.alwaysBounceVertical = true @@ -184,6 +201,10 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { self?.searchBar.deactivate(clear: false) } + self.trendingPane.scrollingInitiated = { [weak self] in + self?.searchBar.deactivate(clear: false) + } + self.searchBar.placeholderString = NSAttributedString(string: strings.Stickers_Search, font: Font.regular(14.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor) self.searchBar.cancel = { cancel() @@ -192,6 +213,7 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { let interaction = StickerPaneSearchInteraction(open: { [weak self] info in if let strongSelf = self { + strongSelf.view.endEditing(true) strongSelf.controllerInteraction.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash), parentNavigationController: strongSelf.controllerInteraction.navigationController()), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } }, install: { [weak self] info in @@ -316,10 +338,17 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { index += 1 } } + + if final { + strongSelf.notFoundNode.isHidden = !entries.isEmpty + } else { + strongSelf.notFoundNode.isHidden = true + } } else { let _ = currentRemotePacks.swap(nil) strongSelf.searchBar.activity = false strongSelf.gridNode.isHidden = true + strongSelf.notFoundNode.isHidden = true strongSelf.trendingPane.isHidden = false } @@ -335,21 +364,30 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { self.searchDisposable.dispose() } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, transition: ContainedViewLayoutTransition) { let firstLayout = self.validLayout == nil self.validLayout = size transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) let searchBarHeight: CGFloat = 48.0 transition.updateFrame(node: self.searchBar, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: searchBarHeight))) - self.searchBar.updateLayout(boundingSize: CGSize(width: size.width, height: searchBarHeight), leftInset: 0.0, rightInset: 0.0, transition: transition) + self.searchBar.updateLayout(boundingSize: CGSize(width: size.width, height: searchBarHeight), leftInset: leftInset, rightInset: rightInset, transition: transition) - let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: searchBarHeight), size: CGSize(width: size.width, height: size.height - searchBarHeight)) + if let image = self.notFoundNode.image { + let areaHeight = size.height - searchBarHeight - inputHeight + + let labelSize = self.notFoundLabel.updateLayout(CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude)) + + transition.updateFrame(node: self.notFoundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: searchBarHeight + floor((areaHeight - image.size.height - labelSize.height) / 2.0)), size: image.size)) + transition.updateFrame(node: self.notFoundLabel, frame: CGRect(origin: CGPoint(x: floor((image.size.width - labelSize.width) / 2.0), y: image.size.height + 8.0), size: labelSize)) + } - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: contentFrame.size, insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: searchBarHeight), size: CGSize(width: size.width - leftInset - rightInset, height: size.height - searchBarHeight)) + + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: contentFrame.size, insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0 + bottomInset, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) transition.updateFrame(node: self.trendingPane, frame: contentFrame) - self.trendingPane.updateLayout(size: contentFrame.size, topInset: 0.0, bottomInset: 0.0, transition: transition) + self.trendingPane.updateLayout(size: contentFrame.size, topInset: 0.0, bottomInset: bottomInset, transition: transition) transition.updateFrame(node: self.gridNode, frame: contentFrame) if firstLayout { @@ -391,6 +429,8 @@ final class StickerPaneSearchContainerNode: ASDisplayNode { }) transition.updateAlpha(node: self.trendingPane, alpha: 0.0, completion: { _ in }) + transition.updateAlpha(node: self.notFoundNode, alpha: 0.0, completion: { _ in + }) self.deactivate() } diff --git a/TelegramUI/TelegramRootController.swift b/TelegramUI/TelegramRootController.swift index e7c96cbcc6..e1bb0e293c 100644 --- a/TelegramUI/TelegramRootController.swift +++ b/TelegramUI/TelegramRootController.swift @@ -87,4 +87,29 @@ public final class TelegramRootController: NavigationController { rootTabController.setControllers(controllers, selectedIndex: nil) } + + public func openChatsSearch() { + guard let rootTabController = self.rootTabController else { + return + } + + self.popToRoot(animated: false) + + if let index = rootTabController.controllers.index(where: { $0 is ChatListController}) { + rootTabController.selectedIndex = index + } + + self.chatListController?.activateSearch() + } + + public func openRootCompose() { + self.chatListController?.composePressed() + } + + public func openRootCamera() { + guard let chatListController = self.chatListController else { + return + } + presentedLegacyShortcutCamera(account: self.account, saveCapturedMedia: false, saveEditedPhotos: false, parentController: chatListController) + } } diff --git a/TelegramUI/ThemeSettingsChatPreviewItem.swift b/TelegramUI/ThemeSettingsChatPreviewItem.swift index 46e3a04e6d..14127f49bf 100644 --- a/TelegramUI/ThemeSettingsChatPreviewItem.swift +++ b/TelegramUI/ThemeSettingsChatPreviewItem.swift @@ -87,13 +87,14 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) self.controllerInteraction = ChatControllerInteraction(openMessage: { _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, navigationController: { return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in + }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings) super.init(layerBacked: false, dynamicBounce: false) @@ -145,8 +146,8 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { let chatPresentationData = ChatPresentationData(theme: item.theme, fontSize: item.fontSize, strings: item.strings, wallpaper: item.wallpaper, timeFormat: item.timeFormat) - let item2: ChatMessageItem = ChatMessageItem(presentationData: chatPresentationData, account: item.account, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, content: .message(message: Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "Ahh you kids today with techno music! Enjoy the classics, like Hasselhoff!", attributes: [ReplyMessageAttribute(messageId: replyMessageId)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), read: true, selection: .none, isAdmin: false), disableDate: true) - let item1: ChatMessageItem = ChatMessageItem(presentationData: chatPresentationData, account: item.account, chatLocation: .peer(peerId), controllerInteraction: controllerInteraction, content: .message(message: Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: TelegramUser(id: item.account.peerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []), text: "I can't take you seriously right now. Sorry..", attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), read: true, selection: .none, isAdmin: false), disableDate: true) + let item2: ChatMessageItem = ChatMessageItem(presentationData: chatPresentationData, account: item.account, chatLocation: .peer(peerId), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact), controllerInteraction: controllerInteraction, content: .message(message: Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "Ahh you kids today with techno music! Enjoy the classics, like Hasselhoff!", attributes: [ReplyMessageAttribute(messageId: replyMessageId)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), read: true, selection: .none, isAdmin: false), disableDate: true) + let item1: ChatMessageItem = ChatMessageItem(presentationData: chatPresentationData, account: item.account, chatLocation: .peer(peerId), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact), controllerInteraction: controllerInteraction, content: .message(message: Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: TelegramUser(id: item.account.peerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []), text: "I can't take you seriously right now. Sorry..", attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []), read: true, selection: .none, isAdmin: false), disableDate: true) var node1: ListViewItemNode? if let current = currentNode1 { diff --git a/TelegramUI/ThemeSettingsFontSizeItem.swift b/TelegramUI/ThemeSettingsFontSizeItem.swift index d31ed30e6a..3df7dafd3c 100644 --- a/TelegramUI/ThemeSettingsFontSizeItem.swift +++ b/TelegramUI/ThemeSettingsFontSizeItem.swift @@ -101,6 +101,7 @@ class ThemeSettingsFontSizeItemNode: ListViewItemNode { let sliderView = TGPhotoEditorSliderView() sliderView.enablePanHandling = true + sliderView.enablePanHandling = true sliderView.trackCornerRadius = 1.0 sliderView.lineSize = 2.0 sliderView.dotSize = 5.0 diff --git a/third-party/RMIntro/platform/ios/RMIntroViewController.h b/third-party/RMIntro/platform/ios/RMIntroViewController.h index 1e1378d2d6..845b715da7 100644 --- a/third-party/RMIntro/platform/ios/RMIntroViewController.h +++ b/third-party/RMIntro/platform/ios/RMIntroViewController.h @@ -19,6 +19,30 @@ typedef enum { iPadPro = 5 } DeviceScreen; +@class SSignal; + +@interface TGAvailableLocalization : NSObject + +@property (nonatomic, strong, readonly) NSString *title; +@property (nonatomic, strong, readonly) NSString *localizedTitle; +@property (nonatomic, strong, readonly) NSString *code; + +- (instancetype)initWithTitle:(NSString *)title localizedTitle:(NSString *)localizedTitle code:(NSString *)code; + +@end + +@interface TGSuggestedLocalization : NSObject + +@property (nonatomic, strong, readonly) TGAvailableLocalization *info; +@property (nonatomic, strong, readonly) NSString *continueWithLanguageString; +@property (nonatomic, strong, readonly) NSString *chooseLanguageString; +@property (nonatomic, strong, readonly) NSString *chooseLanguageOtherString; +@property (nonatomic, strong, readonly) NSString *englishLanguageNameString; + +- (instancetype)initWithInfo:(TGAvailableLocalization *)info continueWithLanguageString:(NSString *)continueWithLanguageString chooseLanguageString:(NSString *)chooseLanguageString chooseLanguageOtherString:(NSString *)chooseLanguageOtherString englishLanguageNameString:(NSString *)englishLanguageNameString; + +@end + @interface RMIntroViewController : UIViewController { EAGLContext *context; @@ -40,9 +64,12 @@ typedef enum { BOOL _isOpenGLLoaded; } -- (instancetype)initWithBackroundColor:(UIColor *)backgroundColor primaryColor:(UIColor *)primaryColor accentColor:(UIColor *)accentColor regularDotColor:(UIColor *)regularDotColor highlightedDotColor:(UIColor *)highlightedDotColor; +- (instancetype)initWithBackroundColor:(UIColor *)backgroundColor primaryColor:(UIColor *)primaryColor accentColor:(UIColor *)accentColor regularDotColor:(UIColor *)regularDotColor highlightedDotColor:(UIColor *)highlightedDotColor suggestedLocalizationSignal:(SSignal *)suggestedLocalizationSignal; -@property (nonatomic, copy) void (^startMessaging)(); +@property (nonatomic, copy) void (^startMessaging)(void); +@property (nonatomic, copy) void (^startMessagingInAlternativeLanguage)(NSString *); + +@property (nonatomic) bool isEnabled; - (void)startTimer; - (void)stopTimer; diff --git a/third-party/RMIntro/platform/ios/RMIntroViewController.m b/third-party/RMIntro/platform/ios/RMIntroViewController.m index 47e10c4d03..fbe8997ba4 100644 --- a/third-party/RMIntro/platform/ios/RMIntroViewController.m +++ b/third-party/RMIntro/platform/ios/RMIntroViewController.m @@ -18,6 +18,7 @@ #import #import +#import #define TGLog NSLog #define TGLocalized(x) NSLocalizedString(x, @"") @@ -83,7 +84,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { TGModernButton *_alternativeLanguageButton; SMetaDisposable *_localizationsDisposable; - NSDictionary *_alternativeLocalizationInfo; + TGSuggestedLocalization *_alternativeLocalizationInfo; SVariable *_alternativeLocalization; } @@ -92,11 +93,13 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { @implementation RMIntroViewController -- (instancetype)initWithBackroundColor:(UIColor *)backgroundColor primaryColor:(UIColor *)primaryColor accentColor:(UIColor *)accentColor regularDotColor:(UIColor *)regularDotColor highlightedDotColor:(UIColor *)highlightedDotColor +- (instancetype)initWithBackroundColor:(UIColor *)backgroundColor primaryColor:(UIColor *)primaryColor accentColor:(UIColor *)accentColor regularDotColor:(UIColor *)regularDotColor highlightedDotColor:(UIColor *)highlightedDotColor suggestedLocalizationSignal:(SSignal *)suggestedLocalizationSignal { self = [super init]; if (self != nil) { + _isEnabled = true; + _backgroundColor = backgroundColor; _primaryColor = primaryColor; _accentColor = accentColor; @@ -132,9 +135,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { _alternativeLocalization = [[SVariable alloc] init]; - /*SSignal *localizationSignal = [TGLocalizationSignals suggestedLocalization]; - - _localizationsDisposable = [[localizationSignal deliverOn:[SQueue mainQueue]] startWithNext:^(TGSuggestedLocalization *next) { + _localizationsDisposable = [[suggestedLocalizationSignal deliverOn:[SQueue mainQueue]] startWithNext:^(TGSuggestedLocalization *next) { __strong RMIntroViewController *strongSelf = weakSelf; if (strongSelf != nil && next != nil) { if (strongSelf->_alternativeLocalizationInfo == nil) { @@ -145,14 +146,14 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { [strongSelf->_alternativeLanguageButton sizeToFit]; if ([strongSelf isViewLoaded]) { - [strongSelf->_alternativeLanguageButton.layer animateAlphaFrom:0.0f to:1.0f duration:0.3f timingFunction:kCAMediaTimingFunctionEaseInEaseOut removeOnCompletion:true completion:nil]; + [strongSelf->_alternativeLanguageButton.layer animateAlphaFrom:0.0f to:strongSelf->_isEnabled ? 1.0 : 0.6 duration:0.3f timingFunction:kCAMediaTimingFunctionEaseInEaseOut removeOnCompletion:true completion:nil]; [UIView animateWithDuration:0.3 animations:^{ [strongSelf viewWillLayoutSubviews]; }]; } } } - }];*/ + }]; } return self; } @@ -314,6 +315,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) { [_startButton setBackgroundImage:buttonBackgroundImage forState:UIControlStateNormal]; [_startButton setBackgroundImage:buttonHighlightedBackgroundImage forState:UIControlStateHighlighted]; [self.view addSubview:_startButton]; + [self.view addSubview:_alternativeLanguageButton]; _pageControl = [[UIPageControl alloc] init]; _pageControl.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin; @@ -685,7 +687,59 @@ NSInteger _current_page_end; } - (void)alternativeLanguageButtonPressed { - + if (_startMessagingInAlternativeLanguage && _alternativeLocalizationInfo.info.code.length != 0) { + _startMessagingInAlternativeLanguage(_alternativeLocalizationInfo.info.code); + } +} + +- (void)setIsEnabled:(bool)isEnabled { + if (_isEnabled != isEnabled) { + _isEnabled = isEnabled; + _startButton.alpha = _isEnabled ? 1.0 : 0.6; + _alternativeLanguageButton.alpha = _isEnabled ? 1.0 : 0.6; + } +} + +@end + +@implementation TGAvailableLocalization + +- (instancetype)initWithTitle:(NSString *)title localizedTitle:(NSString *)localizedTitle code:(NSString *)code { + self = [super init]; + if (self != nil) { + _title = title; + _localizedTitle = localizedTitle; + _code = code; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + return [self initWithTitle:[aDecoder decodeObjectForKey:@"title"] localizedTitle:[aDecoder decodeObjectForKey:@"localizedTitle"] code:[aDecoder decodeObjectForKey:@"code"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:_title forKey:@"title"]; + [aCoder encodeObject:_localizedTitle forKey:@"localizedTitle"]; + [aCoder encodeObject:_code forKey:@"code"]; +} + +@end + +@implementation TGSuggestedLocalization + +- (instancetype)initWithInfo:(TGAvailableLocalization *)info continueWithLanguageString:(NSString *)continueWithLanguageString chooseLanguageString:(NSString *)chooseLanguageString chooseLanguageOtherString:(NSString *)chooseLanguageOtherString englishLanguageNameString:(NSString *)englishLanguageNameString { + self = [super init]; + if (self != nil) { + _info = info; + _continueWithLanguageString = continueWithLanguageString; + _chooseLanguageString = chooseLanguageString; + _chooseLanguageOtherString = chooseLanguageOtherString; + _englishLanguageNameString = englishLanguageNameString; + } + return self; } @end