diff --git a/Images.xcassets/Chat List/NavigationShare.imageset/Contents.json b/Images.xcassets/Chat List/NavigationShare.imageset/Contents.json new file mode 100644 index 0000000000..f8f827e40b --- /dev/null +++ b/Images.xcassets/Chat List/NavigationShare.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 40429c4ce0..59b8d8ee7b 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -21,6 +21,17 @@ D01BAA241ECE173200295217 /* PresentationResourcesCallList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA231ECE173200295217 /* PresentationResourcesCallList.swift */; }; D01BAA581ED3283D00295217 /* AddFormatToStringWithRanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA571ED3283D00295217 /* AddFormatToStringWithRanges.swift */; }; D03E838F1EC10FE5001A6ED9 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D0FC40831D5B8E7400261D9D /* Info.plist */; }; + D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D101EEA04D400711AF6 /* MapResources.swift */; }; + D04B4D131EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D121EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift */; }; + D04B4D661EEA993A00711AF6 /* LegacyLocationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B4D651EEA993A00711AF6 /* LegacyLocationController.swift */; }; + D0754D1E1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D1D1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift */; }; + D0754D201EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D1F1EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift */; }; + D0754D221EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D211EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift */; }; + D0754D241EEE0F4100884F6E /* ChatMessageInteractiveMediaLabelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D231EEE0F4100884F6E /* ChatMessageInteractiveMediaLabelNode.swift */; }; + D0754D271EEE10C800884F6E /* BotCheckoutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0754D261EEE10C800884F6E /* BotCheckoutController.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 */; }; D0ACCB1A1EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ACCB191EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift */; }; D0ACCB1C1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ACCB1B1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift */; }; D0AF7C461ED84BC500CD8E0F /* LanguageSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF7C451ED84BC500CD8E0F /* LanguageSelectionController.swift */; }; @@ -702,6 +713,10 @@ D0F0AAE61EC21B68005EE2A5 /* CallControllerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0AAE51EC21B68005EE2A5 /* CallControllerButton.swift */; }; D0F0AAE91EC22658005EE2A5 /* SecretChatKeyVisualization.h in Headers */ = {isa = PBXBuildFile; fileRef = D00C7CF51E37BF680080C3D5 /* SecretChatKeyVisualization.h */; settings = {ATTRIBUTES = (Public, ); }; }; D0F0AAEA1EC254A8005EE2A5 /* NumberPluralizationForm.h in Headers */ = {isa = PBXBuildFile; fileRef = D0EAE0A11EB212DE005296C1 /* NumberPluralizationForm.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 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 */; }; + D0F6800A1EE750EE000E5906 /* ChannelBannedMemberController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F680091EE750EE000E5906 /* ChannelBannedMemberController.swift */; }; D0FC408E1D5B8E7500261D9D /* TelegramUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC408D1D5B8E7500261D9D /* TelegramUITests.swift */; }; /* End PBXBuildFile section */ @@ -847,6 +862,9 @@ D049EAE51E44AD5600A2CD3A /* ChatMediaInputRecentStickerPacksItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputRecentStickerPacksItem.swift; sourceTree = ""; }; D049EAED1E44BB3200A2CD3A /* ChatListRecentPeersListItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListRecentPeersListItem.swift; sourceTree = ""; }; D049EAF21E44DE2500A2CD3A /* AuthorizationSequenceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceController.swift; sourceTree = ""; }; + D04B4D101EEA04D400711AF6 /* MapResources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapResources.swift; sourceTree = ""; }; + D04B4D121EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageMapBubbleContentNode.swift; sourceTree = ""; }; + D04B4D651EEA993A00711AF6 /* LegacyLocationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyLocationController.swift; sourceTree = ""; }; D04B66B71DD672D00049C3D2 /* GeoLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoLocation.swift; sourceTree = ""; }; D04BB2B21E44E56200650E93 /* AuthorizationSequenceSplashController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceSplashController.swift; sourceTree = ""; }; D04BB2B41E44E58E00650E93 /* AuthorizationSequencePhoneEntryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequencePhoneEntryController.swift; sourceTree = ""; }; @@ -975,6 +993,11 @@ D073CE621DCBBE5D007511FD /* MessageSent.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = MessageSent.caf; path = TelegramUI/Sounds/MessageSent.caf; sourceTree = ""; }; D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceSoundManager.swift; sourceTree = ""; }; D073CE701DCBF23F007511FD /* DeclareEncodables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeclareEncodables.swift; sourceTree = ""; }; + D0754D1D1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageAttachedContentNode.swift; sourceTree = ""; }; + D0754D1F1EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageGameBubbleContentNode.swift; sourceTree = ""; }; + D0754D211EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageInvoiceBubbleContentNode.swift; sourceTree = ""; }; + D0754D231EEE0F4100884F6E /* ChatMessageInteractiveMediaLabelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageInteractiveMediaLabelNode.swift; sourceTree = ""; }; + D0754D261EEE10C800884F6E /* BotCheckoutController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutController.swift; sourceTree = ""; }; D07551871DDA4BB50073E051 /* TelegramLegacyComponents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TelegramLegacyComponents.framework; path = "../TelegramLegacyComponents/build/Debug-iphoneos/TelegramLegacyComponents.framework"; sourceTree = ""; }; D075518A1DDA4D7D0073E051 /* LegacyController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyController.swift; sourceTree = ""; }; D075518C1DDA4E0B0073E051 /* LegacyControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyControllerNode.swift; sourceTree = ""; }; @@ -1016,6 +1039,9 @@ 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 = ""; }; 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 = ""; }; + D099D7501EEFF91E00A3128C /* GameControllerTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameControllerTitleView.swift; sourceTree = ""; }; D099EA1E1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputContextPanelNode.swift; sourceTree = ""; }; D099EA201DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputPanelItem.swift; sourceTree = ""; }; D099EA261DE765DB001AF5A8 /* ManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedMediaId.swift; sourceTree = ""; }; @@ -1503,6 +1529,10 @@ D0F53BEB1E784DA900117362 /* ChangePhoneNumberCodeController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberCodeController.swift; sourceTree = ""; }; D0F53BF61E79593500117362 /* AuthorizationSequenceSignUpController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceSignUpController.swift; sourceTree = ""; }; D0F53BF81E79593F00117362 /* AuthorizationSequenceSignUpControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationSequenceSignUpControllerNode.swift; sourceTree = ""; }; + D0F67FEF1EE6B8A8000E5906 /* ChannelMembersSearchController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelMembersSearchController.swift; sourceTree = ""; }; + D0F67FF11EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelMembersSearchControllerNode.swift; sourceTree = ""; }; + D0F67FF31EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelMembersSearchContainerNode.swift; sourceTree = ""; }; + D0F680091EE750EE000E5906 /* ChannelBannedMemberController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelBannedMemberController.swift; sourceTree = ""; }; D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaFrameSourceContext.swift; sourceTree = ""; }; D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerAudioRenderer.swift; sourceTree = ""; }; D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaManager.swift; sourceTree = ""; }; @@ -2052,6 +2082,14 @@ name = Sounds; sourceTree = ""; }; + D0754D251EEE10A100884F6E /* Bot Payments */ = { + isa = PBXGroup; + children = ( + D0754D261EEE10C800884F6E /* BotCheckoutController.swift */, + ); + name = "Bot Payments"; + sourceTree = ""; + }; D07551891DDA4C7C0073E051 /* Legacy Components */ = { isa = PBXGroup; children = ( @@ -2065,6 +2103,7 @@ D023ED2F1DDB605D00BD496D /* LegacyEmptyController.swift */, D023ED311DDB60CF00BD496D /* LegacyNavigationController.swift */, D0EFD8951DDE8249009E508A /* LegacyLocationPicker.swift */, + D04B4D651EEA993A00711AF6 /* LegacyLocationController.swift */, D0D03B211DECB1AD00220C46 /* TGDataItem.h */, D0D03B221DECB1AD00220C46 /* TGDataItem.m */, ); @@ -2207,6 +2246,16 @@ name = "Presentation Data"; sourceTree = ""; }; + D099D74B1EEFEE0100A3128C /* Game */ = { + isa = PBXGroup; + children = ( + D099D74C1EEFEE1500A3128C /* GameController.swift */, + D099D74E1EEFEE6A00A3128C /* GameControllerNode.swift */, + D099D7501EEFF91E00A3128C /* GameControllerTitleView.swift */, + ); + name = Game; + sourceTree = ""; + }; D099EA1D1DE744EE001AF5A8 /* Horizontal List */ = { isa = PBXGroup; children = ( @@ -3283,6 +3332,7 @@ D03E5E0E1E55F8B90029569A /* ChannelVisibilityController.swift */, D0561DE71E574C3200E6B9E9 /* ChannelAdminsController.swift */, D00BDA1E1EE5B69200C64C5E /* ChannelAdminController.swift */, + D0F680091EE750EE000E5906 /* ChannelBannedMemberController.swift */, D0B98E7E1E575D2C008084B1 /* ChannelBlacklistController.swift */, D0613FC71E5F8AB100202CDB /* ChannelInfoController.swift */, D0613FCC1E60482300202CDB /* ChannelMembersController.swift */, @@ -3290,6 +3340,9 @@ D033FEAA1E61BFC100644997 /* GroupAdminsController.swift */, D0528E621E65BECA00E2FEF5 /* UserInfoController.swift */, D099261E1E69791E00D95539 /* GroupsInCommonController.swift */, + D0F67FEF1EE6B8A8000E5906 /* ChannelMembersSearchController.swift */, + D0F67FF11EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift */, + D0F67FF31EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift */, ); name = "Peer Info"; sourceTree = ""; @@ -3425,6 +3478,8 @@ D01B27931E38F3920022A4C0 /* Item List */, D0EE97131D88BB1A006C18E1 /* Peer Info */, D0D2689B1D79D31500C422DA /* Peer Selection */, + D0754D251EEE10A100884F6E /* Bot Payments */, + D099D74B1EEFEE0100A3128C /* Game */, D0EC6B391EB8CF1E00EBF1C3 /* Call */, D01BAA161ECC8DED00295217 /* Call List */, D0F69E791D6B8C3B0046BCD6 /* Settings */, @@ -3524,12 +3579,16 @@ D0F69E251D6B8B030046BCD6 /* ChatMessageItem.swift */, D0F69E261D6B8B030046BCD6 /* ChatMessageItemView.swift */, D0F69E271D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift */, + D04B4D121EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift */, D0F69E281D6B8B030046BCD6 /* ChatMessageReplyInfoNode.swift */, D0F69E291D6B8B030046BCD6 /* ChatMessageStickerItemNode.swift */, D0575AF61EA0ED4F006F2541 /* ChatMessageInstantVideoItemNode.swift */, D0F69E2A1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift */, D0ACCB1B1EC5FF4B0079D8BF /* ChatMessageCallBubbleContentNode.swift */, D0F69E2B1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift */, + D0754D1F1EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift */, + D0754D211EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift */, + D0754D1D1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift */, D0F69E2C1D6B8B030046BCD6 /* ChatUnreadItem.swift */, D0F69E191D6B8AE60046BCD6 /* ChatHoleItem.swift */, D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */, @@ -3539,6 +3598,7 @@ D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */, D0C48F431E81D5110075317D /* ChatEmptyItem.swift */, D02298361E0C34E900707F91 /* ChatMessageBackground.swift */, + D0754D231EEE0F4100884F6E /* ChatMessageInteractiveMediaLabelNode.swift */, ); name = Items; sourceTree = ""; @@ -3726,6 +3786,7 @@ D0F3A8B71E83125C00B4C64C /* MediaResources.swift */, D0F3A8B91E831E6300B4C64C /* FetchVideoMediaResource.swift */, D06E4AC31E84806300627D1D /* FetchPhotoLibraryImageResource.swift */, + D04B4D101EEA04D400711AF6 /* MapResources.swift */, ); name = Resources; sourceTree = ""; @@ -3957,6 +4018,7 @@ D0EC6CC01EB9F58800EBF1C3 /* LegacyMediaPickers.swift in Sources */, D0AF7C4A1ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift in Sources */, D0EC6CC11EB9F58800EBF1C3 /* LegacyCamera.swift in Sources */, + D0754D1E1EEDDF6200884F6E /* ChatMessageAttachedContentNode.swift in Sources */, D0EC6FEC1EBA182B00EBF1C3 /* aec_core_sse2.cc in Sources */, D0EC6FC81EBA135100EBF1C3 /* cross_correlation.c in Sources */, D0EC6FB11EBA112600EBF1C3 /* ooura_fft_neon.cc in Sources */, @@ -4003,6 +4065,7 @@ D0EC6CE11EB9F58800EBF1C3 /* PresentationResourcesItemList.swift in Sources */, D0EC6CE21EB9F58800EBF1C3 /* PresentationResourcesChatList.swift in Sources */, D0EC6CE31EB9F58800EBF1C3 /* PresentationResourcesChat.swift in Sources */, + D0F67FF21EE6B915000E5906 /* ChannelMembersSearchControllerNode.swift in Sources */, D0EC6CE41EB9F58800EBF1C3 /* PresentationData.swift in Sources */, D0EC6CE51EB9F58800EBF1C3 /* PresentationStrings.swift in Sources */, D0EC6EB41EBA0FC100EBF1C3 /* AudioOutput.cpp in Sources */, @@ -4032,6 +4095,7 @@ D0EC6CF51EB9F58800EBF1C3 /* PeerMessageManagedMediaId.swift in Sources */, D0EC6CF61EB9F58800EBF1C3 /* ChatContextResultManagedMediaId.swift in Sources */, D0EC6EB01EBA0FBB00EBF1C3 /* NetworkSocket.cpp in Sources */, + D04B4D661EEA993A00711AF6 /* LegacyLocationController.swift in Sources */, D0EC6CF71EB9F58800EBF1C3 /* RecentGifManagedMediaId.swift in Sources */, D0EC6CF81EB9F58800EBF1C3 /* ManagedVideoNode.swift in Sources */, D0ACCB1A1EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift in Sources */, @@ -4098,6 +4162,7 @@ D0EC6D291EB9F58800EBF1C3 /* FetchVideoMediaResource.swift in Sources */, D0EC6FA51EBA111500EBF1C3 /* audio_util.cc in Sources */, D0EC6FFD1EBA1F2400EBF1C3 /* OngoingCallThreadLocalContext.mm in Sources */, + D0F67FF41EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift in Sources */, D0EC6D2A1EB9F58800EBF1C3 /* FetchPhotoLibraryImageResource.swift in Sources */, D0EC6FDB1EBA135100EBF1C3 /* refl_coef_to_lpc.c in Sources */, D0EC6D2B1EB9F58800EBF1C3 /* FileMediaResourceStatus.swift in Sources */, @@ -4155,6 +4220,7 @@ D0EC6D5F1EB9F58800EBF1C3 /* GridMessageItem.swift in Sources */, D0EC6D601EB9F58800EBF1C3 /* GridHoleItem.swift in Sources */, D0EC6D611EB9F58800EBF1C3 /* GridMessageSelectionNode.swift in Sources */, + D0754D201EEDEBA000884F6E /* ChatMessageGameBubbleContentNode.swift in Sources */, D0C0B5921EDC5A3B000F4D2C /* LinkHighlightingNode.swift in Sources */, D0EC6D621EB9F58800EBF1C3 /* ContactListNode.swift in Sources */, D0EC6D631EB9F58800EBF1C3 /* ContactListActionItem.swift in Sources */, @@ -4175,6 +4241,7 @@ D0EC6FAB1EBA112600EBF1C3 /* splitting_filter.cc in Sources */, D0EC6D6F1EB9F58800EBF1C3 /* AuthorizationSequenceCodeEntryController.swift in Sources */, D0EC6D701EB9F58800EBF1C3 /* AuthorizationSequenceCodeEntryControllerNode.swift in Sources */, + D099D7511EEFF91E00A3128C /* GameControllerTitleView.swift in Sources */, D0EC6FBB1EBA114200EBF1C3 /* digital_agc.c in Sources */, D0EC6D711EB9F58800EBF1C3 /* AuthorizationSequencePasswordEntryController.swift in Sources */, D0EC6D721EB9F58800EBF1C3 /* AuthorizationSequencePasswordEntryControllerNode.swift in Sources */, @@ -4187,6 +4254,7 @@ D0EC6D771EB9F58800EBF1C3 /* ChatListControllerNode.swift in Sources */, D0EC6D781EB9F58800EBF1C3 /* NetworkStatusTitleView.swift in Sources */, D0EC6FAC1EBA112600EBF1C3 /* three_band_filter_bank.cc in Sources */, + D0F6800A1EE750EE000E5906 /* ChannelBannedMemberController.swift in Sources */, D0EC6D791EB9F58800EBF1C3 /* ChatListTitleLockView.swift in Sources */, D0EC6D7A1EB9F58800EBF1C3 /* ChatListSearchContainerNode.swift in Sources */, D0EC6D7B1EB9F58800EBF1C3 /* ChatListRecentPeersListItem.swift in Sources */, @@ -4215,6 +4283,8 @@ D0EC6D921EB9F58900EBF1C3 /* ChatMessageDateAndStatusNode.swift in Sources */, D0EC6D931EB9F58900EBF1C3 /* ChatMessageFileBubbleContentNode.swift in Sources */, D0EC6D941EB9F58900EBF1C3 /* ChatMessageForwardInfoNode.swift in Sources */, + D0754D241EEE0F4100884F6E /* ChatMessageInteractiveMediaLabelNode.swift in Sources */, + D04B4D131EEA0A6500711AF6 /* ChatMessageMapBubbleContentNode.swift in Sources */, D0EC6FB81EBA114200EBF1C3 /* aecm_core_neon.cc in Sources */, D0EC6EB61EBA0FD000EBF1C3 /* AudioInputAudioUnit.cpp in Sources */, D0EC6D951EB9F58900EBF1C3 /* ChatMessageInteractiveFileNode.swift in Sources */, @@ -4249,6 +4319,7 @@ D0EC6FAA1EBA111500EBF1C3 /* wav_header.cc in Sources */, D0EC6DAD1EB9F58900EBF1C3 /* ChatInterfaceStateNavigationButtons.swift in Sources */, D0EC6DAE1EB9F58900EBF1C3 /* ChatInterfaceStateContextMenus.swift in Sources */, + D0F67FF01EE6B8A8000E5906 /* ChannelMembersSearchController.swift in Sources */, D0EC6DAF1EB9F58900EBF1C3 /* ChatInterfaceInputContexts.swift in Sources */, D0EC6DB01EB9F58900EBF1C3 /* ChatInterfaceInputContextPanels.swift in Sources */, D0EC6DB11EB9F58900EBF1C3 /* ChatInterfaceInputNodes.swift in Sources */, @@ -4283,6 +4354,7 @@ D0EC6DC81EB9F58900EBF1C3 /* MultiplexedVideoNode.swift in Sources */, D0EC6DC91EB9F58900EBF1C3 /* SoftwareVideoLayerFrameManager.swift in Sources */, D0EC6DCA1EB9F58900EBF1C3 /* SoftwareVideoThumbnailLayer.swift in Sources */, + D0754D221EEDF89900884F6E /* ChatMessageInvoiceBubbleContentNode.swift in Sources */, D0EC6FE11EBA135100EBF1C3 /* spl_init.c in Sources */, D0EC6DCB1EB9F58900EBF1C3 /* ChatMediaInputTrendingPane.swift in Sources */, D0EC6DCC1EB9F58900EBF1C3 /* ChatButtonKeyboardInputNode.swift in Sources */, @@ -4366,6 +4438,7 @@ D0EC6FEA1EBA17C300EBF1C3 /* fft4g.c in Sources */, D0EC6E0E1EB9F58900EBF1C3 /* PeerAvatarImageGalleryItem.swift in Sources */, D0EC6E0F1EB9F58900EBF1C3 /* MapInputController.swift in Sources */, + D04B4D111EEA04D400711AF6 /* MapResources.swift in Sources */, D0EC6E101EB9F58900EBF1C3 /* MapInputControllerNode.swift in Sources */, D0EC6E111EB9F58900EBF1C3 /* InstantPageNode.swift in Sources */, D0EC6E121EB9F58900EBF1C3 /* InstantPageLayout.swift in Sources */, @@ -4398,8 +4471,10 @@ D0EC6E281EB9F58900EBF1C3 /* ContactsController.swift in Sources */, D0EC6E291EB9F58900EBF1C3 /* ContactsControllerNode.swift in Sources */, D0EC6E2A1EB9F58900EBF1C3 /* ContactsSearchContainerNode.swift in Sources */, + D099D74F1EEFEE6A00A3128C /* GameControllerNode.swift in Sources */, D0EC6FD21EBA135100EBF1C3 /* get_hanning_window.c in Sources */, D0EC6E2B1EB9F58900EBF1C3 /* ComposeController.swift in Sources */, + D099D74D1EEFEE1500A3128C /* GameController.swift in Sources */, D0EC6E2C1EB9F58900EBF1C3 /* ComposeControllerNode.swift in Sources */, D0EC6E2D1EB9F58900EBF1C3 /* CounterContollerTitleView.swift in Sources */, D0EC6E2E1EB9F58900EBF1C3 /* ContactMultiselectionController.swift in Sources */, @@ -4508,6 +4583,7 @@ D0EC6E7F1EB9F58900EBF1C3 /* ChangePhoneNumberControllerNode.swift in Sources */, D0EC6E801EB9F58900EBF1C3 /* ChangePhoneNumberCodeController.swift in Sources */, D0EC6E811EB9F58900EBF1C3 /* NotificationContainerController.swift in Sources */, + D0754D271EEE10C800884F6E /* BotCheckoutController.swift in Sources */, D0EC6E821EB9F58900EBF1C3 /* NotificationContainerControllerNode.swift in Sources */, D0EC6E831EB9F58900EBF1C3 /* NotificationItemContainerNode.swift in Sources */, D0EC6E841EB9F58900EBF1C3 /* NotificationItem.swift in Sources */, diff --git a/TelegramUI/BotCheckoutController.swift b/TelegramUI/BotCheckoutController.swift new file mode 100644 index 0000000000..9a30829567 --- /dev/null +++ b/TelegramUI/BotCheckoutController.swift @@ -0,0 +1,3 @@ +import Foundation + + diff --git a/TelegramUI/CallListCallItem.swift b/TelegramUI/CallListCallItem.swift index aa273ee016..65e9b0de99 100644 --- a/TelegramUI/CallListCallItem.swift +++ b/TelegramUI/CallListCallItem.swift @@ -341,12 +341,12 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.topMessage.timestamp, relativeTo: timestamp) - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + let (dateLayout, dateApply) = makeDateLayout(NSAttributedString(string: dateText, font: dateFont, textColor: item.theme.list.itemSecondaryTextColor), nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + + let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - dateRightInset - dateLayout.size.width - 10.0), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) - let (dateLayout, dateApply) = makeDateLayout(NSAttributedString(string: dateText, font: dateFont, textColor: item.theme.list.itemSecondaryTextColor), nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 56.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) let outgoingIcon = PresentationResourcesCallList.outgoingIcon(item.theme) diff --git a/TelegramUI/CallListController.swift b/TelegramUI/CallListController.swift index 325fa27c4a..6cc40513b8 100644 --- a/TelegramUI/CallListController.swift +++ b/TelegramUI/CallListController.swift @@ -22,6 +22,9 @@ public final class CallListController: ViewController { private let segmentedTitleView: ItemListControllerSegmentedTitleView + private var isEmpty: Bool? + private var editingMode: Bool = false + private let createActionDisposable = MetaDisposable() public init(account: Account) { @@ -34,9 +37,7 @@ public final class CallListController: ViewController { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style - self.navigationItem.titleView = self.segmentedTitleView self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconCalls") @@ -80,9 +81,18 @@ public final class CallListController: ViewController { self.segmentedTitleView.segments = [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed] self.segmentedTitleView.color = self.presentationData.theme.rootController.navigationBar.accentTextColor + if let isEmpty = self.isEmpty, isEmpty { + self.title = self.presentationData.strings.Calls_TabTitle + } else { + if self.editingMode { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + } else { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + } + } + self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) @@ -110,6 +120,25 @@ public final class CallListController: ViewController { } }) } + }, emptyStateUpdated: { [weak self] empty in + if let strongSelf = self { + if empty != strongSelf.isEmpty { + strongSelf.isEmpty = empty + + if empty { + strongSelf.navigationItem.setLeftBarButton(nil, animated: true) + strongSelf.title = strongSelf.presentationData.strings.Calls_TabTitle + } else { + strongSelf.title = "" + strongSelf.navigationItem.titleView = strongSelf.segmentedTitleView + if strongSelf.editingMode { + strongSelf.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)) + } else { + strongSelf.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)) + } + } + } + } }) self._ready.set(self.controllerNode.ready) self.displayNodeDidLoad() @@ -151,6 +180,7 @@ public final class CallListController: ViewController { } @objc func editPressed() { + self.editingMode = true self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) self.controllerNode.updateState { state in @@ -159,6 +189,7 @@ public final class CallListController: ViewController { } @objc func donePressed() { + self.editingMode = false self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) self.controllerNode.updateState { state in diff --git a/TelegramUI/CallListControllerNode.swift b/TelegramUI/CallListControllerNode.swift index 8a9d2f4be9..9c74ab48d9 100644 --- a/TelegramUI/CallListControllerNode.swift +++ b/TelegramUI/CallListControllerNode.swift @@ -165,12 +165,14 @@ final class CallListControllerNode: ASDisplayNode { private let call: (PeerId) -> Void private let openInfo: (PeerId) -> Void + private let emptyStateUpdated: (Bool) -> Void - init(account: Account, presentationData: PresentationData, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId) -> Void) { + init(account: Account, presentationData: PresentationData, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId) -> Void, emptyStateUpdated: @escaping (Bool) -> Void) { self.account = account self.presentationData = presentationData self.call = call self.openInfo = openInfo + self.emptyStateUpdated = emptyStateUpdated self.currentState = CallListNodeState(theme: presentationData.theme, strings: presentationData.strings, editing: false, messageIdWithRevealedOptions: nil) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) @@ -340,7 +342,7 @@ final class CallListControllerNode: ASDisplayNode { } return EmptyDisposable - } |> runOn(Queue.mainQueue()) + } |> runOn(Queue.mainQueue()) } private func dequeueTransition() { @@ -351,6 +353,8 @@ final class CallListControllerNode: ASDisplayNode { if let strongSelf = self { strongSelf.callListView = transition.callListView + strongSelf.emptyStateUpdated(transition.callListView.filteredEntries.isEmpty) + if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf._ready.set(true) diff --git a/TelegramUI/ChannelAdminController.swift b/TelegramUI/ChannelAdminController.swift index 4c111ac81f..0eba7d5f96 100644 --- a/TelegramUI/ChannelAdminController.swift +++ b/TelegramUI/ChannelAdminController.swift @@ -441,22 +441,58 @@ public func channelAdminController(account: Account, peerId: PeerId, adminId: Pe rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) } else if canEdit { rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { - var updateFlags: TelegramChannelAdminRightsFlags? - updateState { current in - updateFlags = current.updatedFlags - if let _ = updateFlags { - return current.withUpdatedUpdating(true) - } else { - return current + if let _ = initialParticipant { + var updateFlags: TelegramChannelAdminRightsFlags? + updateState { current in + updateFlags = current.updatedFlags + if let _ = updateFlags { + return current.withUpdatedUpdating(true) + } else { + return current + } } - } - if let updateFlags = updateFlags { - updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: updateFlags)) |> deliverOnMainQueue).start(error: { _ in + + if let updateFlags = updateFlags { + updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: updateFlags)) |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + updated(TelegramChannelAdminRights(flags: updateFlags)) + dismissImpl?() + })) + } + } else if canEdit, let channel = channelView.peers[channelView.peerId] as? TelegramChannel { + var updateFlags: TelegramChannelAdminRightsFlags? + updateState { current in + updateFlags = current.updatedFlags + return current.withUpdatedUpdating(true) + } + + if updateFlags == nil { + let maskRightsFlags: TelegramChannelAdminRightsFlags + switch channel.info { + case .broadcast: + maskRightsFlags = .broadcastSpecific + case .group: + maskRightsFlags = .groupSpecific + } - }, completed: { - updated(TelegramChannelAdminRights(flags: updateFlags)) - dismissImpl?() - })) + if channel.flags.contains(.isCreator) { + updateFlags = maskRightsFlags.subtracting(.canAddAdmins) + } else if let adminRights = channel.adminRights { + updateFlags = maskRightsFlags.intersection(adminRights.flags).subtracting(.canAddAdmins) + } else { + updateFlags = [] + } + } + + if let updateFlags = updateFlags { + updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: updateFlags)) |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + updated(TelegramChannelAdminRights(flags: updateFlags)) + dismissImpl?() + })) + } } }) } diff --git a/TelegramUI/ChannelAdminsController.swift b/TelegramUI/ChannelAdminsController.swift index 9115c785f6..53106ba824 100644 --- a/TelegramUI/ChannelAdminsController.swift +++ b/TelegramUI/ChannelAdminsController.swift @@ -235,7 +235,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { arguments.removeAdmin(peerId) }) case let .addAdmin(theme, text, editing): - return ItemListPeerActionItem(theme: defaultPresentationTheme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: editing, action: { + return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: editing, action: { arguments.addAdmin() }) case let .adminsInfo(theme, text): @@ -328,33 +328,35 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, if let peer = view.peers[view.peerId] as? TelegramChannel { var isGroup = false - if case let .group(info) = peer.info, peer.flags.contains(.isCreator) { + if case let .group(info) = peer.info { isGroup = true - let selectedType: CurrentAdministrationType - if let current = state.selectedType { - selectedType = current - } else { - if info.flags.contains(.everyMemberCanInviteMembers) { - selectedType = .everyoneCanAddMembers + if peer.flags.contains(.isCreator) { + let selectedType: CurrentAdministrationType + if let current = state.selectedType { + selectedType = current } else { - selectedType = .adminsCanAddMembers + if info.flags.contains(.everyMemberCanInviteMembers) { + selectedType = .everyoneCanAddMembers + } else { + selectedType = .adminsCanAddMembers + } } + let selectedTypeValue: String + let infoText: String + switch selectedType { + case .everyoneCanAddMembers: + selectedTypeValue = presentationData.strings.ChannelMembers_WhoCanAddMembers_AllMembers + infoText = presentationData.strings.ChannelMembers_AllMembersMayInviteOnHelp + case .adminsCanAddMembers: + selectedTypeValue = presentationData.strings.ChannelMembers_WhoCanAddMembers_Admins + infoText = presentationData.strings.ChannelMembers_AllMembersMayInviteOffHelp + } + + entries.append(.administrationType(presentationData.theme, presentationData.strings.ChannelMembers_WhoCanAddMembers, selectedTypeValue)) + + entries.append(.administrationInfo(presentationData.theme, infoText)) } - let selectedTypeValue: String - let infoText: String - switch selectedType { - case .everyoneCanAddMembers: - selectedTypeValue = presentationData.strings.ChannelMembers_WhoCanAddMembers_AllMembers - infoText = presentationData.strings.ChannelMembers_AllMembersMayInviteOnHelp - case .adminsCanAddMembers: - selectedTypeValue = presentationData.strings.ChannelMembers_WhoCanAddMembers_Admins - infoText = presentationData.strings.ChannelMembers_AllMembersMayInviteOffHelp - } - - entries.append(.administrationType(presentationData.theme, presentationData.strings.ChannelMembers_WhoCanAddMembers, selectedTypeValue)) - - entries.append(.administrationInfo(presentationData.theme, infoText)) } if let participants = participants { @@ -413,8 +415,10 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, } } - entries.append(.addAdmin(presentationData.theme, presentationData.strings.Channel_Management_AddModerator, state.editing)) - entries.append(.adminsInfo(presentationData.theme, presentationData.strings.Channel_Management_AddModeratorHelp)) + 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)) + } } } @@ -541,117 +545,49 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon } })) }, addAdmin: { - var confirmationImpl: ((PeerId) -> Signal)? - let contactsController = ContactSelectionController(account: account, title: { $0.Channel_Management_AddModerator }, confirmation: { peerId in - if let confirmationImpl = confirmationImpl { - return confirmationImpl(peerId) - } else { - return .single(false) - } - }) - confirmationImpl = { [weak contactsController] peerId in - return account.postbox.loadedPeerWithId(peerId) - |> deliverOnMainQueue - |> mapToSignal { peer in - let result = ValuePromise() - if let contactsController = contactsController { - let alertController = standardTextAlertController(title: nil, text: "Add \(peer.displayTitle) as admin?", actions: [ - TextAlertAction(type: .genericAction, title: "Cancel", action: { - result.set(false) - }), - TextAlertAction(type: .defaultAction, title: "OK", action: { - result.set(true) - }) - ]) - contactsController.present(alertController, in: .window) - } - - return result.get() - } - } - /*let addAdmin = contactsController.result - |> deliverOnMainQueue - |> mapToSignal { memberId -> Signal in - if let memberId = memberId { - return account.postbox.peerView(id: memberId) - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { view -> Signal in - if let peer = view.peers[memberId] { - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - - updateState { state in - var found = false - for participant in state.temporaryAdmins { - if participant.peer.id == memberId { - found = true - break - } - } - var removedPeerIds = state.removedPeerIds - removedPeerIds.remove(memberId) - if !found { - var temporaryAdmins = state.temporaryAdmins - temporaryAdmins.append(RenderedChannelParticipant(participant: ChannelParticipant.moderator(id: peer.id, invitedBy: account.peerId, invitedAt: timestamp), peer: peer)) - return state.withUpdatedTemporaryAdmins(temporaryAdmins).withUpdatedRemovedPeerIds(removedPeerIds) - } else { - return state.withUpdatedRemovedPeerIds(removedPeerIds) + presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, openPeer: { peer in + presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: peer.id, initialParticipant: nil, updated: { updatedRights in + let applyAdmin: Signal = adminsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { admins -> Signal in + if let admins = admins { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + var updatedAdmins = admins + if updatedRights.isEmpty { + for i in 0 ..< updatedAdmins.count { + if updatedAdmins[i].peer.id == peer.id { + updatedAdmins.remove(at: i) + break } } - - let applyAdmin: Signal = adminsPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue - |> mapError { _ -> AddPeerAdminError in return .generic } - |> mapToSignal { admins -> Signal in - if let admins = admins { - var updatedAdmins = admins - var found = false - for i in 0 ..< updatedAdmins.count { - if updatedAdmins[i].peer.id == memberId { - found = true - break - } - } - if !found { - updatedAdmins.append(RenderedChannelParticipant(participant: ChannelParticipant.moderator(id: peer.id, invitedBy: account.peerId, invitedAt: timestamp), peer: peer)) - adminsPromise.set(.single(updatedAdmins)) - } - } - - return .complete() - } - - return addPeerAdmin(account: account, peerId: peerId, adminId: memberId) - |> deliverOnMainQueue - |> then(applyAdmin) - |> `catch` { _ -> Signal in - updateState { state in - var temporaryAdmins = state.temporaryAdmins - for i in 0 ..< temporaryAdmins.count { - if temporaryAdmins[i].peer.id == memberId { - temporaryAdmins.remove(at: i) - break - } - } - - return state.withUpdatedTemporaryAdmins(temporaryAdmins) - } - return .complete() - } } else { - return .complete() + var found = false + for i in 0 ..< updatedAdmins.count { + if updatedAdmins[i].peer.id == peer.id { + if case let .member(id, date, _, banInfo) = updatedAdmins[i].participant { + updatedAdmins[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: updatedAdmins[i].peer, peers: [:]) + } + found = true + break + } + } + if !found { + updatedAdmins.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: timestamp, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: nil), peer: peer, peers: [:])) + } } - } - } else { - return .complete() + adminsPromise.set(.single(updatedAdmins)) + } + + return .complete() } - } - presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - addAdminDisposable.set(addAdmin.start())*/ + addAdminDisposable.set(applyAdmin.start()) + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, openAdmin: { participant in - if case let .member(adminId, timestamp, _, _) = participant { + if case let .member(adminId, _, _, _) = participant { presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: participant.peerId, initialParticipant: participant, updated: { updatedRights in let applyAdmin: Signal = adminsPromise.get() |> filter { $0 != nil } @@ -672,7 +608,7 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon for i in 0 ..< updatedAdmins.count { if updatedAdmins[i].peer.id == adminId { if case let .member(id, date, _, banInfo) = updatedAdmins[i].participant { - updatedAdmins[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: updatedAdmins[i].peer) + updatedAdmins[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: updatedAdmins[i].peer, peers: [:]) } found = true break diff --git a/TelegramUI/ChannelBannedMemberController.swift b/TelegramUI/ChannelBannedMemberController.swift new file mode 100644 index 0000000000..630295a22f --- /dev/null +++ b/TelegramUI/ChannelBannedMemberController.swift @@ -0,0 +1,419 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class ChannelBannedMemberControllerArguments { + let account: Account + let toggleRight: (TelegramChannelBannedRightsFlags, TelegramChannelBannedRightsFlags) -> Void + let openTimeout: () -> Void + + init(account: Account, toggleRight: @escaping (TelegramChannelBannedRightsFlags, TelegramChannelBannedRightsFlags) -> Void, openTimeout: @escaping () -> Void) { + self.account = account + self.toggleRight = toggleRight + self.openTimeout = openTimeout + } +} + +private enum ChannelBannedMemberSection: Int32 { + case info + case rights + case timeout +} + +private enum ChannelBannedMemberEntryStableId: Hashable { + case info + case right(TelegramChannelBannedRightsFlags) + case timeout + + var hashValue: Int { + switch self { + case .info: + return 0 + case .timeout: + return 1 + case let .right(flags): + return flags.rawValue.hashValue + } + } + + static func ==(lhs: ChannelBannedMemberEntryStableId, rhs: ChannelBannedMemberEntryStableId) -> Bool { + switch lhs { + case .info: + if case .info = rhs { + return true + } else { + return false + } + case let right(flags): + if case .right(flags) = rhs { + return true + } else { + return false + } + case .timeout: + if case .timeout = rhs { + return true + } else { + return false + } + } + } +} + +private enum ChannelBannedMemberEntry: ItemListNodeEntry { + case info(PresentationTheme, PresentationStrings, Peer, TelegramUserPresence?) + case rightItem(PresentationTheme, Int, String, TelegramChannelBannedRightsFlags, TelegramChannelBannedRightsFlags, Bool, Bool) + case timeout(PresentationTheme, String, String) + + var section: ItemListSectionId { + switch self { + case .info: + return ChannelBannedMemberSection.info.rawValue + case .rightItem: + return ChannelBannedMemberSection.rights.rawValue + case .timeout: + return ChannelBannedMemberSection.timeout.rawValue + } + } + + var stableId: ChannelBannedMemberEntryStableId { + switch self { + case .info: + return .info + case let .rightItem(_, _, _, right, _, _, _): + return .right(right) + case .timeout: + return .timeout + } + } + + static func ==(lhs: ChannelBannedMemberEntry, rhs: ChannelBannedMemberEntry) -> Bool { + switch lhs { + case let .info(lhsTheme, lhsStrings, lhsPeer, lhsPresence): + if case let .info(rhsTheme, rhsStrings, rhsPeer, rhsPresence) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if !arePeersEqual(lhsPeer, rhsPeer) { + return false + } + if lhsPresence != rhsPresence { + return false + } + + return true + } else { + return false + } + case let .rightItem(lhsTheme, lhsIndex, lhsText, lhsRight, lhsFlags, lhsValue, lhsEnabled): + if case let .rightItem(rhsTheme, rhsIndex, rhsText, rhsRight, rhsFlags, rhsValue, rhsEnabled) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsIndex != rhsIndex { + return false + } + if lhsText != rhsText { + return false + } + if lhsRight != rhsRight { + return false + } + if lhsFlags != rhsFlags { + return false + } + if lhsValue != rhsValue { + return false + } + if lhsEnabled != rhsEnabled { + return false + } + return true + } else { + return false + } + case let .timeout(lhsTheme, lhsText, lhsValue): + if case let .timeout(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + } + } + + static func <(lhs: ChannelBannedMemberEntry, rhs: ChannelBannedMemberEntry) -> Bool { + switch lhs { + case .info: + switch rhs { + case .info: + return false + default: + return true + } + case let .rightItem(_, lhsIndex, _, _, _, _, _): + switch rhs { + case .info: + return false + case let .rightItem(_, rhsIndex, _, _, _, _, _): + return lhsIndex < rhsIndex + default: + return true + } + case .timeout: + return false + } + } + + func item(_ arguments: ChannelBannedMemberControllerArguments) -> ListViewItem { + switch self { + case let .info(theme, strings, peer, presence): + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true), editingNameUpdated: { _ in + }, avatarTapped: { + }) + case let .rightItem(theme, _, text, right, flags, value, enabled): + return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in + arguments.toggleRight(right, flags) + }) + case let .timeout(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + arguments.openTimeout() + }) + } + } +} + +private struct ChannelBannedMemberControllerState: Equatable { + let updatedFlags: TelegramChannelBannedRightsFlags? + let updatedTimeout: Int32? + let updating: Bool + + init(updatedFlags: TelegramChannelBannedRightsFlags? = nil, updatedTimeout: Int32? = nil, updating: Bool = false) { + self.updatedFlags = updatedFlags + self.updatedTimeout = updatedTimeout + self.updating = updating + } + + static func ==(lhs: ChannelBannedMemberControllerState, rhs: ChannelBannedMemberControllerState) -> Bool { + if lhs.updatedFlags != rhs.updatedFlags { + return false + } + if lhs.updatedTimeout != rhs.updatedTimeout { + return false + } + if lhs.updating != rhs.updating { + return false + } + return true + } + + func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChannelBannedRightsFlags?) -> ChannelBannedMemberControllerState { + return ChannelBannedMemberControllerState(updatedFlags: updatedFlags, updatedTimeout: self.updatedTimeout, updating: self.updating) + } + + func withUpdatedUpdatedTimeout(_ updatedTimeoyt: Int32?) -> ChannelBannedMemberControllerState { + return ChannelBannedMemberControllerState(updatedFlags: self.updatedFlags, updatedTimeout: updatedTimeout, updating: self.updating) + } + + func withUpdatedUpdating(_ updating: Bool) -> ChannelBannedMemberControllerState { + return ChannelBannedMemberControllerState(updatedFlags: self.updatedFlags, updatedTimeout: self.updatedTimeout, updating: updating) + } +} + +private func stringForRight(strings: PresentationStrings, right: TelegramChannelBannedRightsFlags) -> String { + if right.contains(.banReadMessages) { + return strings.Channel_BanUser_PermissionReadMessages + } else if right.contains(.banSendMessages) { + return strings.Channel_BanUser_PermissionSendMessages + } else if right.contains(.banSendMedia) { + return strings.Channel_BanUser_PermissionSendMedia + } else if right.contains(.banSendGifs) { + return strings.Channel_BanUser_PermissionSendStickersAndGifs + } else if right.contains(.banEmbedLinks) { + return strings.Channel_BanUser_PermissionEmbedLinks + } else { + return "" + } +} + +private func rightDependencies(_ right: TelegramChannelBannedRightsFlags) -> [TelegramChannelBannedRightsFlags] { + if right.contains(.banReadMessages) { + return [] + } else if right.contains(.banSendMessages) { + return [.banReadMessages] + } else if right.contains(.banSendMedia) { + return [.banReadMessages, .banSendMessages] + } else if right.contains(.banSendStickers) { + return [.banReadMessages, .banSendMessages] + } else if right.contains(.banEmbedLinks) { + return [.banReadMessages, .banSendMessages] + } else { + return [] + } +} + +private func channelBannedMemberControllerEntries(presentationData: PresentationData, state: ChannelBannedMemberControllerState, accountPeerId: PeerId, channelView: PeerView, memberView: PeerView, initialParticipant: ChannelParticipant?) -> [ChannelBannedMemberEntry] { + var entries: [ChannelBannedMemberEntry] = [] + + if let _ = channelView.peers[channelView.peerId] as? TelegramChannel, let member = memberView.peers[memberView.peerId] { + entries.append(.info(presentationData.theme, presentationData.strings, member, memberView.peerPresences[member.id] as? TelegramUserPresence)) + + let currentRightsFlags: TelegramChannelBannedRightsFlags + if let updatedFlags = state.updatedFlags { + currentRightsFlags = updatedFlags + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo) = initialParticipant, let banInfo = maybeBanInfo { + currentRightsFlags = banInfo.flags + } else { + currentRightsFlags = [.banSendMessages, .banSendGifs, .banSendGames, .banSendInline, .banSendStickers, .banSendMedia, .banEmbedLinks] + } + + let currentTimeout: Int32 + if let updatedTimeout = state.updatedTimeout { + currentTimeout = updatedTimeout + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo) = initialParticipant, let banInfo = maybeBanInfo { + currentTimeout = banInfo.untilDate + } else { + currentTimeout = Int32.max + } + + let rightsOrder: [TelegramChannelBannedRightsFlags] = [ + .banReadMessages, + .banSendMessages, + .banSendMedia, + .banSendGifs, + .banEmbedLinks + ] + + var index = 0 + for right in rightsOrder { + entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating)) + index += 1 + } + + + } + + return entries +} + +public func channelBannedMemberController(account: Account, peerId: PeerId, memberId: PeerId, initialParticipant: ChannelParticipant?, updated: @escaping (TelegramChannelBannedRights) -> Void) -> ViewController { + let statePromise = ValuePromise(ChannelBannedMemberControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: ChannelBannedMemberControllerState()) + let updateState: ((ChannelBannedMemberControllerState) -> ChannelBannedMemberControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let actionsDisposable = DisposableSet() + + let updateRightsDisposable = MetaDisposable() + actionsDisposable.add(updateRightsDisposable) + + var dismissImpl: (() -> Void)? + + let arguments = ChannelBannedMemberControllerArguments(account: account, toggleRight: { right, flags in + updateState { current in + var updated = flags + if flags.contains(right) { + updated.remove(right) + } else { + updated.insert(right) + } + return current.withUpdatedUpdatedFlags(updated) + } + }, openTimeout: { + + }) + + let combinedView = account.postbox.combinedView(keys: [.peer(peerId: peerId), .peer(peerId: memberId)]) + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), combinedView) + |> deliverOnMainQueue + |> map { presentationData, state, combinedView -> (ItemListControllerState, (ItemListNodeState, ChannelBannedMemberEntry.ItemGenerationArguments)) in + let channelView = combinedView.views[.peer(peerId: peerId)] as! PeerView + let memberView = combinedView.views[.peer(peerId: memberId)] as! PeerView + + let leftNavigationButton: ItemListNavigationButton + leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + dismissImpl?() + }) + + var rightNavigationButton: ItemListNavigationButton? + if state.updating { + rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + } else { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + /*if let _ = initialParticipant { + var updateFlags: TelegramChannelBannedMemberRightsFlags? + updateState { current in + updateFlags = current.updatedFlags + if let _ = updateFlags { + return current.withUpdatedUpdating(true) + } else { + return current + } + } + + if let updateFlags = updateFlags { + updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelBannedMemberRights(flags: updateFlags)) |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + updated(TelegramChannelBannedMemberRights(flags: updateFlags)) + dismissImpl?() + })) + } + } else if canEdit, let channel = channelView.peers[channelView.peerId] as? TelegramChannel { + var updateFlags: TelegramChannelBannedMemberRightsFlags? + updateState { current in + updateFlags = current.updatedFlags + return current.withUpdatedUpdating(true) + } + + if updateFlags == nil { + let maskRightsFlags: TelegramChannelBannedMemberRightsFlags + switch channel.info { + case .broadcast: + maskRightsFlags = .broadcastSpecific + case .group: + maskRightsFlags = .groupSpecific + } + + if channel.flags.contains(.isCreator) { + updateFlags = maskRightsFlags.subtracting(.canAddAdmins) + } else if let adminRights = channel.adminRights { + updateFlags = maskRightsFlags.intersection(adminRights.flags).subtracting(.canAddAdmins) + } else { + updateFlags = [] + } + } + + if let updateFlags = updateFlags { + updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelBannedMemberRights(flags: updateFlags)) |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + updated(TelegramChannelBannedMemberRights(flags: updateFlags)) + dismissImpl?() + })) + } + }*/ + }) + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Channel_BanUser_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + + let listState = ItemListNodeState(entries: channelBannedMemberControllerEntries(presentationData: presentationData, state: state, accountPeerId: account.peerId, channelView: channelView, memberView: memberView, initialParticipant: initialParticipant), style: .blocks, emptyStateItem: nil, animateChanges: true) + + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(account: account, state: signal) + dismissImpl = { [weak controller] in + controller?.dismiss() + } + return controller +} diff --git a/TelegramUI/ChannelBlacklistController.swift b/TelegramUI/ChannelBlacklistController.swift index a89ee408ca..f25873a625 100644 --- a/TelegramUI/ChannelBlacklistController.swift +++ b/TelegramUI/ChannelBlacklistController.swift @@ -8,24 +8,32 @@ private final class ChannelBlacklistControllerArguments { let account: Account let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void + let addPeer: () -> Void let removePeer: (PeerId) -> Void + let openPeer: (ChannelParticipant) -> Void - init(account: Account, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void) { + init(account: Account, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (ChannelParticipant) -> Void) { self.account = account + self.addPeer = addPeer self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removePeer = removePeer + self.openPeer = openPeer } } private enum ChannelBlacklistSection: Int32 { + case add case peers } private enum ChannelBlacklistEntryStableId: Hashable { + case add case peer(PeerId) var hashValue: Int { switch self { + case .add: + return 0 case let .peer(peerId): return peerId.hashValue } @@ -33,6 +41,12 @@ private enum ChannelBlacklistEntryStableId: Hashable { static func ==(lhs: ChannelBlacklistEntryStableId, rhs: ChannelBlacklistEntryStableId) -> Bool { switch lhs { + case .add: + if case .add = rhs { + return true + } else { + return false + } case let .peer(peerId): if case .peer(peerId) = rhs { return true @@ -44,10 +58,13 @@ private enum ChannelBlacklistEntryStableId: Hashable { } private enum ChannelBlacklistEntry: ItemListNodeEntry { - case peerItem(Int32, RenderedChannelParticipant, ItemListPeerItemEditing, Bool) + case add(PresentationTheme, String) + case peerItem(PresentationTheme, PresentationStrings, Int32, RenderedChannelParticipant, ItemListPeerItemEditing, Bool) var section: ItemListSectionId { switch self { + case .add: + return ChannelBlacklistSection.add.rawValue case .peerItem: return ChannelBlacklistSection.peers.rawValue } @@ -55,15 +72,29 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { var stableId: ChannelBlacklistEntryStableId { switch self { - case let .peerItem(_, participant, _, _): + case .add: + return .add + case let .peerItem(_, _, _, participant, _, _): return .peer(participant.peer.id) } } static func ==(lhs: ChannelBlacklistEntry, rhs: ChannelBlacklistEntry) -> Bool { switch lhs { - case let .peerItem(lhsIndex, lhsParticipant, lhsEditing, lhsEnabled): - if case let .peerItem(rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs { + case let .add(lhsTheme, lhsText): + if case let .add(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .peerItem(lhsTheme, lhsStrings, lhsIndex, lhsParticipant, lhsEditing, lhsEnabled): + if case let .peerItem(rhsTheme, rhsStrings, rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if lhsIndex != rhsIndex { return false } @@ -85,9 +116,18 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { static func <(lhs: ChannelBlacklistEntry, rhs: ChannelBlacklistEntry) -> Bool { switch lhs { - case let .peerItem(index, _, _, _): + case .add: switch rhs { - case let .peerItem(rhsIndex, _, _, _): + case .add: + return false + default: + return true + } + case let .peerItem(_, _, index, _, _, _): + switch rhs { + case .add: + return false + case let .peerItem(_, _, rhsIndex, _, _, _): return index < rhsIndex } } @@ -95,8 +135,14 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { func item(_ arguments: ChannelBlacklistControllerArguments) -> ListViewItem { switch self { - case let .peerItem(_, participant, editing, enabled): - return ItemListPeerItem(account: arguments.account, peer: participant.peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in + case let .add(theme, text): + return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: false, action: { + arguments.addPeer() + }) + case let .peerItem(theme, strings, _, participant, editing, enabled): + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: participant.peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: { + arguments.openPeer(participant.participant) + }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removePeer(peerId) @@ -149,12 +195,14 @@ private struct ChannelBlacklistControllerState: Equatable { } } -private func channelBlacklistControllerEntries(view: PeerView, state: ChannelBlacklistControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelBlacklistEntry] { +private func channelBlacklistControllerEntries(presentationData: PresentationData, view: PeerView, state: ChannelBlacklistControllerState, blacklist: ChannelBlacklist?) -> [ChannelBlacklistEntry] { var entries: [ChannelBlacklistEntry] = [] - if let participants = participants { + if let blacklist = blacklist { + entries.append(.add(presentationData.theme, presentationData.strings.Channel_Members_AddMembers)) + var index: Int32 = 0 - for participant in participants.sorted(by: { lhs, rhs in + for participant in blacklist.banned.sorted(by: { lhs, rhs in let lhsInvitedAt: Int32 switch lhs.participant { case .creator: @@ -175,7 +223,7 @@ private func channelBlacklistControllerEntries(view: PeerView, state: ChannelBla if case .creator = participant.participant { editable = false } - entries.append(.peerItem(index, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id)) + entries.append(.peerItem(presentationData.theme, presentationData.strings, index, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id)) index += 1 } } @@ -200,7 +248,7 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View let removePeerDisposable = MetaDisposable() actionsDisposable.add(removePeerDisposable) - let peersPromise = Promise<[RenderedChannelParticipant]?>(nil) + let blacklistPromise = Promise(nil) let arguments = ChannelBlacklistControllerArguments(account: account, setPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in @@ -210,31 +258,67 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View return state } } + }, addPeer: { + presentControllerImpl?(ChannelMembersSearchController(account: account, peerId: peerId, openPeer: { peer in + /*presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: peer.id, initialParticipant: nil, updated: { updatedRights in + let applyAdmin: Signal = adminsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { admins -> Signal in + if let admins = admins { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + var updatedAdmins = admins + if updatedRights.isEmpty { + for i in 0 ..< updatedAdmins.count { + if updatedAdmins[i].peer.id == peer.id { + updatedAdmins.remove(at: i) + break + } + } + } else { + var found = false + for i in 0 ..< updatedAdmins.count { + if updatedAdmins[i].peer.id == peer.id { + if case let .member(id, date, _, banInfo) = updatedAdmins[i].participant { + updatedAdmins[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: updatedAdmins[i].peer) + } + found = true + break + } + } + if !found { + updatedAdmins.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: timestamp, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: nil), peer: peer)) + } + } + adminsPromise.set(.single(updatedAdmins)) + } + + return .complete() + } + addAdminDisposable.set(applyAdmin.start()) + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))*/ + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, removePeer: { memberId in updateState { return $0.withUpdatedRemovingPeerId(memberId) } - let applyPeers: Signal = peersPromise.get() + let applyPeers: Signal = blacklistPromise.get() |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue - |> mapToSignal { peers -> Signal in - if let peers = peers { - var updatedPeers = peers - for i in 0 ..< updatedPeers.count { - if updatedPeers[i].peer.id == memberId { - updatedPeers.remove(at: i) - break - } - } - peersPromise.set(.single(updatedPeers)) + |> mapToSignal { blacklist -> Signal in + if let blacklist = blacklist { + let updatedBlacklist = blacklist.withRemovedPeerId(memberId) + blacklistPromise.set(.single(updatedBlacklist)) } return .complete() } - removePeerDisposable.set((removeChannelBlacklistedPeer(account: account, peerId: peerId, memberId: memberId) |> then(applyPeers) |> deliverOnMainQueue).start(error: { _ in + /*removePeerDisposable.set((removeChannelBlacklistedPeer(account: account, peerId: peerId, memberId: memberId) |> then(applyPeers) |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedRemovingPeerId(nil) } @@ -243,30 +327,34 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View return $0.withUpdatedRemovingPeerId(nil) } - })) + }))*/ + }, openPeer: { participant in + presentControllerImpl?(channelBannedMemberController(account: account, peerId: peerId, memberId: participant.peerId, initialParticipant: participant, updated: { rights in + + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) let peerView = account.viewTracker.peerView(peerId) - let peersSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelBlacklist(account: account, peerId: peerId) |> map { Optional($0) }) + let blacklistSignal: Signal = .single(nil) |> then(channelBlacklistParticipants(account: account, peerId: peerId) |> map { Optional($0) }) - peersPromise.set(peersSignal) + blacklistPromise.set(blacklistSignal) - var previousPeers: [RenderedChannelParticipant]? + var previousBlacklist: ChannelBlacklist? - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView, peersPromise.get()) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView, blacklistPromise.get()) |> deliverOnMainQueue - |> map { presentationData, state, view, peers -> (ItemListControllerState, (ItemListNodeState, ChannelBlacklistEntry.ItemGenerationArguments)) in + |> map { presentationData, state, view, blacklist -> (ItemListControllerState, (ItemListNodeState, ChannelBlacklistEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? - if let peers = peers, !peers.isEmpty { + if let blacklist = blacklist, !blacklist.isEmpty { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } @@ -275,25 +363,15 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View } var emptyStateItem: ItemListControllerEmptyStateItem? - if let peers = peers { - if peers.isEmpty { - if let peer = view.peers[view.peerId] as? TelegramChannel { - if case .group = peer.info { - emptyStateItem = ItemListTextEmptyStateItem(text: "Blacklisted users are removed from the group and can only come back if invited by an admin. Invite links don't work for them.") - } else { - emptyStateItem = ItemListTextEmptyStateItem(text: "Blacklisted users are removed from the channel and can only come back if invited by an admin. Invite links don't work for them.") - } - } - } - } else if peers == nil { + if blacklist == nil { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() } - let previous = previousPeers - previousPeers = peers + let previous = previousBlacklist + previousBlacklist = blacklist - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Blacklist"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: true) - let listState = ItemListNodeState(entries: channelBlacklistControllerEntries(view: view, state: state, participants: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Channel_BlackList_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(entries: channelBlacklistControllerEntries(presentationData: presentationData, view: view, state: state, blacklist: blacklist), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && blacklist != nil && (previous!.restricted.count + previous!.banned.count) >= (blacklist!.restricted.count + blacklist!.banned.count)) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index dba42b4539..ffdd240cc7 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -56,19 +56,19 @@ private enum ChannelInfoEntryTag { private enum ChannelInfoEntry: ItemListNodeEntry { case info(PresentationTheme, PresentationStrings, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: TelegramMediaImageRepresentation?) - case about(text: String) - case addressName(value: String) - case channelPhotoSetup - case channelTypeSetup(isPublic: Bool) - case channelDescriptionSetup(text: String) - case admins(count: Int32) - case members(count: Int32) - case banned(count: Int32) - case notifications(settings: PeerNotificationSettings) - case sharedMedia - case report - case leave - case deleteChannel + case about(theme: PresentationTheme, text: String, value: String) + case addressName(theme: PresentationTheme, text: String, value: String) + case channelPhotoSetup(theme: PresentationTheme, text: String) + case channelTypeSetup(theme: PresentationTheme, text: String, value: String) + case channelDescriptionSetup(theme: PresentationTheme, placeholder: String, value: String) + case admins(theme: PresentationTheme, text: String, value: String) + case members(theme: PresentationTheme, text: String, value: String) + case banned(theme: PresentationTheme, text: String, value: String) + case notifications(theme: PresentationTheme, text: String, value: String) + case sharedMedia(theme: PresentationTheme, text: String) + case report(theme: PresentationTheme, text: String) + case leave(theme: PresentationTheme, text: String) + case deleteChannel(theme: PresentationTheme, text: String) var section: ItemListSectionId { switch self { @@ -150,59 +150,81 @@ private enum ChannelInfoEntry: ItemListNodeEntry { } else { return false } - case let .about(text): - if case .about(text) = rhs { + case let .about(lhsTheme, lhsText, lhsValue): + if case let .about(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .addressName(value): - if case .addressName(value) = rhs { + case let .addressName(lhsTheme, lhsText, lhsValue): + if case let .addressName(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case .channelPhotoSetup: - if case .channelPhotoSetup = rhs { + case let .channelPhotoSetup(lhsTheme, lhsText): + if case let .channelPhotoSetup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .channelTypeSetup(isPublic): - if case .channelTypeSetup(isPublic) = rhs { + case let .channelTypeSetup(lhsTheme, lhsText, lhsValue): + if case let .channelTypeSetup(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .channelDescriptionSetup(text): - if case .channelDescriptionSetup(text) = rhs { + case let .channelDescriptionSetup(lhsTheme, lhsPlaceholder, lhsValue): + if case let .channelDescriptionSetup(rhsTheme, rhsPlaceholder, rhsValue) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder, lhsValue == rhsValue { return true } else { return false } - case let .admins(count): - if case .admins(count) = rhs { + case let .admins(lhsTheme, lhsText, lhsValue): + if case let .admins(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .members(count): - if case .members(count) = rhs { + case let .members(lhsTheme, lhsText, lhsValue): + if case let .members(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .banned(count): - if case .banned(count) = rhs { + case let .banned(lhsTheme, lhsText, lhsValue): + if case let .banned(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case .sharedMedia, .report, .leave, .deleteChannel: - return lhs.stableId == rhs.stableId - case let .notifications(lhsSettings): - if case let .notifications(rhsSettings) = rhs { - return lhsSettings.isEqual(to: rhsSettings) + case let .sharedMedia(lhsTheme, lhsText): + if case let .sharedMedia(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .report(lhsTheme, lhsText): + if case let .report(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .leave(lhsTheme, lhsText): + if case let .leave(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .deleteChannel(lhsTheme, lhsText): + if case let .deleteChannel(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .notifications(lhsTheme, lhsText, lhsValue): + if case let .notifications(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true } else { return false } @@ -221,62 +243,56 @@ private enum ChannelInfoEntry: ItemListNodeEntry { }, avatarTapped: { arguments.tapAvatarAction() }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar) - case let .about(text): - return ItemListTextWithLabelItem(theme: defaultPresentationTheme, label: "about", text: text, multiline: true, sectionId: self.section, action: nil) - case let .addressName(value): - return ItemListTextWithLabelItem(theme: defaultPresentationTheme, label: "share link", text: "https://t.me/\(value)", multiline: false, sectionId: self.section, action: { + case let .about(theme, text, value): + return ItemListTextWithLabelItem(theme: theme, label: text, text: value, multiline: true, sectionId: self.section, action: nil) + case let .addressName(theme, text, value): + return ItemListTextWithLabelItem(theme: theme, label: text, text: "https://t.me/\(value)", multiline: false, sectionId: self.section, action: { arguments.displayAddressNameContextMenu("https://t.me/\(value)") }, tag: ChannelInfoEntryTag.addressName) - case .channelPhotoSetup: - return ItemListActionItem(title: "Set Channel Photo", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + case let .channelPhotoSetup(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.changeProfilePhoto() }) - case let .channelTypeSetup(isPublic): - return ItemListDisclosureItem(title: "Channel Type", label: isPublic ? "Public" : "Private", sectionId: self.section, style: .plain, action: { + case let .channelTypeSetup(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.openChannelTypeSetup() }) - case let .channelDescriptionSetup(text): - return ItemListMultilineInputItem(theme: defaultPresentationTheme, text: text, placeholder: "Channel Description", sectionId: self.section, style: .plain, textUpdated: { updatedText in + case let .channelDescriptionSetup(theme, placeholder, value): + return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, sectionId: self.section, style: .plain, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }, action: { }) - case let .admins(count): - return ItemListDisclosureItem(title: "Admins", label: "\(count)", sectionId: self.section, style: .plain, action: { + case let .admins(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.openAdmins() }) - case let .members(count): - return ItemListDisclosureItem(title: "Members", label: "\(count)", sectionId: self.section, style: .plain, action: { + case let .members(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.openMembers() }) - case let .banned(count): - return ItemListDisclosureItem(title: "Blacklist", label: "\(count)", sectionId: self.section, style: .plain, action: { + case let .banned(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.openBanned() }) - case .sharedMedia: - return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .plain, action: { + case let .sharedMedia(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .plain, action: { arguments.openSharedMedia() }) - case let .notifications(settings): - let label: String - if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState { - label = "Disabled" - } else { - label = "Enabled" - } - return ItemListDisclosureItem(title: "Notifications", label: label, sectionId: self.section, style: .plain, action: { + case let .notifications(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.changeNotificationMuteSettings() }) - case .report: - return ItemListActionItem(title: "Report", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + case let .report(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.reportChannel() }) - case .leave: - return ItemListActionItem(title: "Leave Channel", kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { + case let .leave(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.leaveChannel() }) - case .deleteChannel: - return ItemListActionItem(title: "Delete Channel", kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { + case let .deleteChannel(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.deleteChannel() }) } @@ -357,53 +373,65 @@ private func channelInfoEntries(account: Account, presentationData: Presentation entries.append(.info(presentationData.theme, presentationData.strings, peer: peer, cachedData: view.cachedData, state: infoState, updatingAvatar: state.updatingAvatar)) if state.editingState != nil && canEditChannel { - entries.append(.channelPhotoSetup) + entries.append(.channelPhotoSetup(theme: presentationData.theme, text: presentationData.strings.Channel_UpdatePhotoItem)) } if let cachedChannelData = view.cachedData as? CachedChannelData { if let editingState = state.editingState, canEditChannel { - entries.append(.channelDescriptionSetup(text: editingState.editingDescriptionText)) + entries.append(.channelDescriptionSetup(theme: presentationData.theme, placeholder: presentationData.strings.Channel_Edit_AboutItem, value: editingState.editingDescriptionText)) } else { if let about = cachedChannelData.about, !about.isEmpty { - entries.append(.about(text: about)) + entries.append(.about(theme: presentationData.theme, text: presentationData.strings.Channel_AboutItem, value: about)) } } } if state.editingState != nil && peer.flags.contains(.isCreator) { - entries.append(.channelTypeSetup(isPublic: isPublic)) + let linkText: String + if let username = peer.username { + linkText = "@\(username)" + } else { + linkText = presentationData.strings.Channel_Setup_TypePrivate + } + entries.append(.channelTypeSetup(theme: presentationData.theme, text: presentationData.strings.Channel_Edit_LinkItem, value: linkText)) } else if let username = peer.username, !username.isEmpty { - entries.append(.addressName(value: username)) + entries.append(.addressName(theme: presentationData.theme, text: presentationData.strings.Channel_LinkItem, value: username)) } if let cachedChannelData = view.cachedData as? CachedChannelData { if state.editingState != nil && canEditMembers { - if let bannedCount = cachedChannelData.participantsSummary.bannedCount { - entries.append(.banned(count: bannedCount)) + if let kickedCount = cachedChannelData.participantsSummary.kickedCount { + entries.append(.banned(theme: presentationData.theme, text: presentationData.strings.Channel_Info_Banned, value: "\(kickedCount)")) } } else { if let adminCount = cachedChannelData.participantsSummary.adminCount { - entries.append(.admins(count: adminCount)) + entries.append(.admins(theme: presentationData.theme, text: presentationData.strings.Channel_Info_Management, value: "\(adminCount)")) } if let memberCount = cachedChannelData.participantsSummary.memberCount { - entries.append(.members(count: memberCount)) + entries.append(.members(theme: presentationData.theme, text: presentationData.strings.Channel_Info_Members, value: "\(memberCount)")) } } } - if let notificationSettings = view.notificationSettings { - entries.append(ChannelInfoEntry.notifications(settings: notificationSettings)) + if let notificationSettings = view.notificationSettings as? TelegramPeerNotificationSettings { + let notificationsText: String + if case .muted = notificationSettings.muteState { + notificationsText = presentationData.strings.UserInfo_NotificationsDisabled + } else { + notificationsText = presentationData.strings.UserInfo_NotificationsEnabled + } + entries.append(ChannelInfoEntry.notifications(theme: presentationData.theme, text: presentationData.strings.GroupInfo_Notifications, value: notificationsText)) } - entries.append(ChannelInfoEntry.sharedMedia) + entries.append(ChannelInfoEntry.sharedMedia(theme: presentationData.theme, text: presentationData.strings.GroupInfo_SharedMedia)) if peer.flags.contains(.isCreator) { if state.editingState != nil { - entries.append(ChannelInfoEntry.deleteChannel) + entries.append(ChannelInfoEntry.deleteChannel(theme: presentationData.theme, text: presentationData.strings.ChannelInfo_DeleteChannel)) } } else { - entries.append(ChannelInfoEntry.report) + entries.append(ChannelInfoEntry.report(theme: presentationData.theme, text: presentationData.strings.ReportPeer_Report)) if peer.participationStatus == .member { - entries.append(ChannelInfoEntry.leave) + entries.append(ChannelInfoEntry.leave(theme: presentationData.theme, text: presentationData.strings.Channel_LeaveChannel)) } } } @@ -552,6 +580,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr }, openChannelTypeSetup: { presentControllerImpl?(channelVisibilityController(account: account, peerId: peerId, mode: .generic), ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) }, changeNotificationMuteSettings: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController() let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() @@ -567,30 +596,30 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr } changeMuteSettingsDisposable.set(changePeerNotificationSettings(account: account, peerId: peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) } + var items: [ActionSheetItem] = [] + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsEnable, action: { + dismissAction() + notificationAction(0) + })) + let intervals: [Int32] = [ + 1 * 60 * 60, + 8 * 60 * 60, + 2 * 24 * 60 * 60 + ] + for value in intervals { + items.append(ActionSheetButtonItem(title: muteForIntervalString(strings: presentationData.strings, value: value), action: { + dismissAction() + notificationAction(value) + })) + } + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsDisable, action: { + dismissAction() + notificationAction(Int32.max) + })) + controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Enable", action: { - dismissAction() - notificationAction(0) - }), - ActionSheetButtonItem(title: "Mute for 1 hour", action: { - dismissAction() - notificationAction(1 * 60 * 60) - }), - ActionSheetButtonItem(title: "Mute for 8 hours", action: { - dismissAction() - notificationAction(8 * 60 * 60) - }), - ActionSheetButtonItem(title: "Mute for 2 days", action: { - dismissAction() - notificationAction(2 * 24 * 60 * 60) - }), - ActionSheetButtonItem(title: "Disable", action: { - dismissAction() - notificationAction(Int32.max) - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, openSharedMedia: { @@ -606,20 +635,21 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr }, reportChannel: { }, leaveChannel: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController() let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } controller.setItemGroups([ ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Leave Channel", action: { + ActionSheetButtonItem(title: presentationData.strings.Channel_LeaveChannel, action: { let _ = removePeerChat(postbox: account.postbox, peerId: peerId, reportChatSpam: false).start() dismissAction() popToRootControllerImpl?() }), ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) - ]) + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, deleteChannel: { @@ -643,7 +673,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr var leftNavigationButton: ItemListNavigationButton? var rightNavigationButton: ItemListNavigationButton? if let editingState = state.editingState { - leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { updateState { $0.withUpdatedEditingState(nil) } @@ -662,7 +692,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr if state.savingData { rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: doneEnabled, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: doneEnabled, action: { var updateValues: (title: String?, description: String?) = (nil, nil) updateState { state in updateValues = valuesRequiringUpdate(state: state, view: view) @@ -703,7 +733,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr }) } } else if canManageChannel { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { if let channel = peer as? TelegramChannel, case .broadcast = channel.info { var text = "" if let cachedData = view.cachedData as? CachedChannelData, let about = cachedData.about { @@ -716,7 +746,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Info"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.GroupInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) let listState = ItemListNodeState(entries: channelInfoEntries(account: account, presentationData: presentationData, view: view, state: state), style: .plain) return (controllerState, (listState, arguments)) @@ -737,6 +767,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr } displayAddressNameContextMenuImpl = { [weak controller] text in if let strongController = controller { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } var resultItemNode: ListViewItemNode? let _ = strongController.frameForItemNode({ itemNode in if let itemNode = itemNode as? ItemListTextWithLabelItemNode { @@ -750,7 +781,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr return false }) if let resultItemNode = resultItemNode { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text("Copy"), action: { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = text })]) strongController.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in diff --git a/TelegramUI/ChannelMembersController.swift b/TelegramUI/ChannelMembersController.swift index e8bf30270b..9d155df579 100644 --- a/TelegramUI/ChannelMembersController.swift +++ b/TelegramUI/ChannelMembersController.swift @@ -311,7 +311,7 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo } if !found { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - updatedPeers.append(RenderedChannelParticipant(participant: ChannelParticipant.member(id: peer.id, invitedAt: timestamp, adminInfo: nil, banInfo: nil), peer: peer)) + updatedPeers.append(RenderedChannelParticipant(participant: ChannelParticipant.member(id: peer.id, invitedAt: timestamp, adminInfo: nil, banInfo: nil), peer: peer, peers: [:])) peersPromise.set(.single(updatedPeers)) } } diff --git a/TelegramUI/ChannelMembersSearchContainerNode.swift b/TelegramUI/ChannelMembersSearchContainerNode.swift new file mode 100644 index 0000000000..24c6454074 --- /dev/null +++ b/TelegramUI/ChannelMembersSearchContainerNode.swift @@ -0,0 +1,256 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private enum ChannelMembersSearchSection { + case members + case contacts + case global + + var chatListHeaderType: ChatListSearchItemHeaderType { + switch self { + case .members: + return .members + case .contacts: + return .contacts + case .global: + return .globalPeers + } + } +} + +private final class ChannelMembersSearchEntry: Comparable, Identifiable { + let index: Int + let peer: Peer + let section: ChannelMembersSearchSection + + init(index: Int, peer: Peer, section: ChannelMembersSearchSection) { + self.index = index + self.peer = peer + self.section = section + } + + var stableId: PeerId { + return self.peer.id + } + + static func ==(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool { + return lhs.index == rhs.index && arePeersEqual(lhs.peer, rhs.peer) && lhs.section == rhs.section + } + + static func <(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { + let peer = self.peer + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: self.peer, chatPeer: self.peer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: self.section.chatListHeaderType, theme: theme, strings: strings), action: { _ in + peerSelected(peer) + }) + } +} +struct ChannelMembersSearchContainerTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private func channelMembersSearchContainerPreparedRecentTransition(from fromEntries: [ChannelMembersSearchEntry], to toEntries: [ChannelMembersSearchEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void) -> ChannelMembersSearchContainerTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, peerSelected: peerSelected), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, peerSelected: peerSelected), directionHint: nil) } + + return ChannelMembersSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates) +} + +final class ChannelMembersSearchContainerNode: SearchDisplayControllerContentNode { + private let account: Account + private let openPeer: (Peer) -> Void + + private let listNode: ListView + + private var enqueuedTransitions: [(ChannelMembersSearchContainerTransition, Bool)] = [] + private var hasValidLayout = false + + private let searchQuery = Promise() + private let searchDisposable = MetaDisposable() + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + + init(account: Account, peerId: PeerId, openPeer: @escaping (Peer) -> Void) { + self.account = account + self.openPeer = openPeer + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) + + self.listNode = ListView() + + super.init() + + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor + + self.addSubnode(self.listNode) + + let themeAndStringsPromise = self.themeAndStringsPromise + let foundItems = searchQuery.get() + |> mapToSignal { query -> Signal<[ChannelMembersSearchEntry]?, NoError> in + if let query = query, !query.isEmpty { + let foundMembers = channelMembers(account: account, peerId: peerId, filter: .search(query)) + let foundContacts = account.postbox.searchContacts(query: query.lowercased()) + let foundRemotePeers: Signal<[Peer], NoError> = .single([]) |> then(searchPeers(account: account, query: query) + |> delay(0.2, queue: Queue.concurrentDefaultQueue())) + + return combineLatest(foundMembers, foundContacts, foundRemotePeers, themeAndStringsPromise.get()) + |> map { foundMembers, foundContacts, foundRemotePeers, themeAndStrings -> [ChannelMembersSearchEntry]? in + var entries: [ChannelMembersSearchEntry] = [] + + var existingPeerIds = Set() + + var index = 0 + for participant in foundMembers { + if !existingPeerIds.contains(participant.peer.id) { + existingPeerIds.insert(participant.peer.id) + entries.append(ChannelMembersSearchEntry(index: index, peer: participant.peer, section: .members)) + index += 1 + } + } + + for peer in foundContacts { + if !existingPeerIds.contains(peer.id) { + existingPeerIds.insert(peer.id) + entries.append(ChannelMembersSearchEntry(index: index, peer: peer, section: .contacts)) + index += 1 + } + } + + for peer in foundRemotePeers { + if !existingPeerIds.contains(peer.id) && peer is TelegramUser { + existingPeerIds.insert(peer.id) + entries.append(ChannelMembersSearchEntry(index: index, peer: peer, section: .global)) + index += 1 + } + } + + return entries + } + } else { + return .single(nil) + } + } + + let previousSearchItems = Atomic<[ChannelMembersSearchEntry]?>(value: nil) + + self.searchDisposable.set((combineLatest(foundItems, self.themeAndStringsPromise.get()) + |> deliverOnMainQueue).start(next: { [weak self] entries, themeAndStrings in + if let strongSelf = self { + let previousEntries = previousSearchItems.swap(entries) + + let firstTime = previousEntries == nil + let transition = channelMembersSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], account: account, theme: themeAndStrings.0, strings: themeAndStrings.1, peerSelected: openPeer) + strongSelf.enqueueTransition(transition, firstTime: firstTime) + } + })) + + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) + } + } + }) + } + + deinit { + self.searchDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.backgroundColor = theme.chatList.backgroundColor + } + + override func searchTextUpdated(text: String) { + if text.isEmpty { + self.searchQuery.set(.single(nil)) + } else { + self.searchQuery.set(.single(text)) + } + } + + private func enqueueTransition(_ transition: ChannelMembersSearchContainerTransition, firstTime: Bool) { + enqueuedTransitions.append((transition, firstTime)) + + if self.hasValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let (transition, firstTime) = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.PreferSynchronousDrawing) + if firstTime { + } else { + //options.insert(.AnimateAlpha) + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + }) + } + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default + } + + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !hasValidLayout { + hasValidLayout = true + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } +} diff --git a/TelegramUI/ChannelMembersSearchController.swift b/TelegramUI/ChannelMembersSearchController.swift new file mode 100644 index 0000000000..01b9f4a270 --- /dev/null +++ b/TelegramUI/ChannelMembersSearchController.swift @@ -0,0 +1,100 @@ +import Foundation +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +final class ChannelMembersSearchController: TelegramController { + private let queue = Queue() + + private let account: Account + private let peerId: PeerId + private let openPeer: (Peer) -> Void + + private var presentationData: PresentationData + + private var didPlayPresentationAnimation = false + + private var controllerNode: ChannelMembersSearchControllerNode { + return self.displayNode as! ChannelMembersSearchControllerNode + } + + init(account: Account, peerId: PeerId, openPeer: @escaping (Peer) -> Void) { + self.account = account + self.peerId = peerId + self.openPeer = openPeer + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(account: account) + + self.title = self.presentationData.strings.Channel_Members_Title + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = ChannelMembersSearchControllerNode(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings, peerId: peerId) + self.controllerNode.navigationBar = self.navigationBar + self.controllerNode.requestActivateSearch = { [weak self] in + self?.activateSearch() + } + self.controllerNode.requestDeactivateSearch = { [weak self] in + self?.deactivateSearch(animated: true) + } + self.controllerNode.requestOpenPeerFromSearch = { [weak self] peer in + self?.dismiss() + self?.openPeer(peer) + } + + self.displayNodeDidLoad() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { + self.didPlayPresentationAnimation = true + if case .modalSheet = presentationArguments.presentationAnimation { + self.controllerNode.animateIn() + } + } + } + + override func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + private func activateSearch() { + if self.displayNavigationBar { + if let scrollToTop = self.scrollToTop { + scrollToTop() + } + self.controllerNode.activateSearch() + self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + } + } + + private func deactivateSearch(animated: Bool) { + if !self.displayNavigationBar { + self.controllerNode.deactivateSearch(animated: animated) + self.setDisplayNavigationBar(true, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate) + } + } + + @objc func cancelPressed() { + self.dismiss() + } +} diff --git a/TelegramUI/ChannelMembersSearchControllerNode.swift b/TelegramUI/ChannelMembersSearchControllerNode.swift new file mode 100644 index 0000000000..e37c4c9394 --- /dev/null +++ b/TelegramUI/ChannelMembersSearchControllerNode.swift @@ -0,0 +1,176 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit + +class ChannelMembersSearchControllerNode: ASDisplayNode { + private let account: Account + private let peerId: PeerId + + let listNode: ListView + var navigationBar: NavigationBar? + + private(set) var searchDisplayController: SearchDisplayController? + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + var requestActivateSearch: (() -> Void)? + var requestDeactivateSearch: (() -> Void)? + var requestOpenPeerFromSearch: ((Peer) -> Void)? + + var themeAndStrings: (PresentationTheme, PresentationStrings) + + private var disposable: Disposable? + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerId: PeerId) { + self.account = account + self.listNode = ListView() + self.peerId = peerId + + self.themeAndStrings = (theme, strings) + + super.init(viewBlock: { + return UITracingLayerView() + }, didLoad: nil) + + self.backgroundColor = theme.chatList.backgroundColor + + self.addSubnode(self.listNode) + + self.disposable = (channelMembers(account: account, peerId: peerId) + |> deliverOnMainQueue).start(next: { [weak self] participants in + if let strongSelf = self { + var items: [ListViewItem] = [] + items.append(ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { + if let strongSelf = self { + strongSelf.requestActivateSearch?() + } + })) + + for participant in participants { + items.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peer: participant.peer, chatPeer: nil, status: .none, selection: .none, index: nil, header: nil, action: { peer in + if let strongSelf = self { + strongSelf.requestOpenPeerFromSearch?(peer) + } + })) + } + + var insertItems: [ListViewInsertItem] = [] + for i in 0 ..< items.count { + insertItems.append(ListViewInsertItem(index: i, previousIndex: nil, item: items[i], directionHint: nil)) + } + + strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: insertItems, updateIndicesAndItems: [], options: [], updateOpaqueState: nil) + } + }) + } + + deinit { + self.disposable?.dispose() + } + + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.themeAndStrings = (theme, strings) + self.searchDisplayController?.updateThemeAndStrings(theme: theme, strings: strings) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.input]) + insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) + + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default + } + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + } + + func activateSearch() { + guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar else { + return + } + + var maybePlaceholderNode: SearchBarPlaceholderNode? + self.listNode.forEachItemNode { node in + if let node = node as? ChatListSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + if let _ = self.searchDisplayController { + return + } + + if let placeholderNode = maybePlaceholderNode { + self.searchDisplayController = SearchDisplayController(theme: self.themeAndStrings.0, strings: self.themeAndStrings.1, contentNode: ChannelMembersSearchContainerNode(account: self.account, peerId: self.peerId, openPeer: { [weak self] peer in + if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { + requestOpenPeerFromSearch(peer) + } + }), cancel: { [weak self] in + if let requestDeactivateSearch = self?.requestDeactivateSearch { + requestDeactivateSearch() + } + }) + + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { subnode in + self.insertSubnode(subnode, belowSubnode: navigationBar) + }, placeholder: placeholderNode) + } + } + + func deactivateSearch(animated: Bool) { + if let searchDisplayController = self.searchDisplayController { + var maybePlaceholderNode: SearchBarPlaceholderNode? + self.listNode.forEachItemNode { node in + if let node = node as? ChatListSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + searchDisplayController.deactivate(placeholder: maybePlaceholderNode, animated: animated) + self.searchDisplayController = nil + } + } + + func animateIn() { + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + + func animateOut(completion: (() -> Void)? = nil) { + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { _ in + completion?() + }) + } +} diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index b80e06b2b2..73fb2731e2 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -113,28 +113,33 @@ public class ChatController: TelegramController { } let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] id in - if let strongSelf = self, strongSelf.isNodeLoaded { + if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { var galleryMedia: Media? - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { - for media in message.media { - if let file = media as? TelegramMediaFile { - if !file.isAnimated { - galleryMedia = file - } - } else if let image = media as? TelegramMediaImage { - galleryMedia = image - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - if let file = content.file { - galleryMedia = file - } else if let image = content.image { - galleryMedia = image - } + for media in message.media { + if let file = media as? TelegramMediaFile { + if !file.isAnimated { + galleryMedia = file } + } else if let image = media as? TelegramMediaImage { + galleryMedia = image + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + if let file = content.file { + galleryMedia = file + } else if let image = content.image { + galleryMedia = image + } + } else if let mapMedia = media as? TelegramMediaMap { + galleryMedia = mapMedia } } if let galleryMedia = galleryMedia { - if let file = galleryMedia as? TelegramMediaFile, file.isSticker { + if let mapMedia = galleryMedia as? TelegramMediaMap { + strongSelf.chatDisplayNode.dismissInput() + strongSelf.present(legacyLocationController(message: message, mapMedia: mapMedia, account: strongSelf.account, openPeer: { peer in + self?.openPeer(peerId: peer.id, navigation: .info, fromMessageId: nil) + }), in: .window) + } else if let file = galleryMedia as? TelegramMediaFile, file.isSticker { for attribute in file.attributes { if case let .Sticker(_, reference) = attribute { if let reference = reference { @@ -305,61 +310,67 @@ public class ChatController: TelegramController { } }, requestMessageActionCallback: { [weak self] messageId, data, isGame in if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if !$0.contains(where: { - switch $0 { - case .requestInProgress: - return true - default: - return false - } - }) { - var updatedContexts = $0 - updatedContexts.append(.requestInProgress) - return updatedContexts.sorted() - } - return $0 - } - }) - - strongSelf.messageActionCallbackDisposable.set((requestMessageActionCallback(account: strongSelf.account, messageId: messageId, isGame: isGame, data: data) |> afterDisposed { - Queue.mainQueue().async { - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if let index = $0.index(where: { - switch $0 { - case .requestInProgress: - return true - default: - return false - } - }) { - var updatedContexts = $0 - updatedContexts.remove(at: index) - return updatedContexts - } - return $0 + if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if !$0.contains(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false } - }) + }) { + var updatedContexts = $0 + updatedContexts.append(.requestInProgress) + return updatedContexts.sorted() + } + return $0 } - } - }).start(next: { result in - if let strongSelf = self { - switch result { - case .none: - break - case let .alert(text): - let message: Signal = .single(text) - let noMessage: Signal = .single(nil) - let delayedNoMessage: Signal = noMessage |> delay(1.0, queue: Queue.mainQueue()) - strongSelf.botCallbackAlertMessage.set(message |> then(delayedNoMessage)) - case let .url(url): - strongSelf.openUrl(url) + }) + + strongSelf.messageActionCallbackDisposable.set(((requestMessageActionCallback(account: strongSelf.account, messageId: messageId, isGame: isGame, data: data) |> afterDisposed { + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.index(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts + } + return $0 + } + }) + } } - } - })) + }) |> deliverOnMainQueue).start(next: { result in + if let strongSelf = self { + switch result { + case .none: + break + case let .alert(text): + let message: Signal = .single(text) + let noMessage: Signal = .single(nil) + let delayedNoMessage: Signal = noMessage |> delay(1.0, queue: Queue.mainQueue()) + strongSelf.botCallbackAlertMessage.set(message |> then(delayedNoMessage)) + case let .url(url): + if isGame { + strongSelf.present(GameController(account: strongSelf.account, url: url, message: message), in: .window) + } else { + strongSelf.openUrl(url) + } + } + } + })) + } } }, openUrl: { [weak self] url in if let strongSelf = self { @@ -501,6 +512,30 @@ public class ChatController: TelegramController { }) ])]) strongSelf.present(actionSheet, in: .window) + case let .peerMention(peerId, mention): + let actionSheet = ActionSheetController() + var items: [ActionSheetItem] = [] + if !mention.isEmpty { + items.append(ActionSheetTextItem(title: mention)) + } + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil), fromMessageId: nil) + } + })) + if !mention.isEmpty { + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = mention + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items:items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, in: .window) case let .mention(mention): let actionSheet = ActionSheetController() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ @@ -1042,7 +1077,7 @@ public class ChatController: TelegramController { }, openFileGallery: { self?.presentMediaPicker(fileMode: true) }, openMap: { - + self?.presentMapPicker() }, openContacts: { if let strongSelf = self { let contactsController = ContactSelectionController(account: strongSelf.account, title: { $0.DialogList_SelectContact }) @@ -1897,6 +1932,23 @@ public class ChatController: TelegramController { }) } + private func presentMapPicker() { + self.present(legacyLocationPickerController(sendLocation: { [weak self] coordinate, venue in + if let strongSelf = self { + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + let message: EnqueueMessage = .message(text: "", attributes: [], media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: venue), replyToMessageId: replyMessageId) + let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [message]).start() + } + }), in: .window) + } + private func enqueueMediaMessages(signals: [Any]?) { self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(account: self.account, peerId: self.peerId, signals: signals!) |> deliverOnMainQueue).start(next: { [weak self] messages in if let strongSelf = self { @@ -1923,7 +1975,7 @@ public class ChatController: TelegramController { }) } }) - enqueueMessages(account: self.account, peerId: self.peerId, messages: [message.withUpdatedReplyToMessageId(replyMessageId)]).start() + let _ = enqueueMessages(account: self.account, peerId: self.peerId, messages: [message.withUpdatedReplyToMessageId(replyMessageId)]).start() } } diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index e2fa201eec..7e5af84c33 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -31,6 +31,7 @@ struct ChatInterfaceHighlightedState: Equatable { public enum ChatControllerInteractionLongTapAction { case url(String) case mention(String) + case peerMention(PeerId, String) case command(String) case hashtag(String) } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index c4e25efe3c..257f37f2c9 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -231,7 +231,7 @@ class ChatControllerNode: ASDisplayNode { transitionIsAnimated = true } - if let search = self.chatPresentationInterfaceState.search, let interfaceInteraction = self.interfaceInteraction { + if let _ = self.chatPresentationInterfaceState.search, let interfaceInteraction = self.interfaceInteraction { var activate = false if self.searchNavigationNode == nil { activate = true @@ -241,7 +241,7 @@ class ChatControllerNode: ASDisplayNode { if activate { self.searchNavigationNode?.activate() } - } else if let searchNavigationNode = self.searchNavigationNode { + } else if let _ = self.searchNavigationNode { self.searchNavigationNode = nil self.navigationBar.setContentNode(nil, animated: transitionIsAnimated) } diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index f4ead4e302..899fa17c68 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -44,8 +44,23 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun if maxIndex >= targetIndex { for i in targetIndex ..< maxIndex { if case .HoleEntry = view.entries[i] { - fadeIn = true - return .Loading(initialData: initialData) + var incomingCount: Int32 = 0 + inner: for entry in view.entries.reversed() { + switch entry { + case .HoleEntry: + break inner + case let .MessageEntry(message, _, _, _): + if message.flags.contains(.Incoming) { + incomingCount += 1 + } + } + } + if let combinedReadState = view.combinedReadState, combinedReadState.count == incomingCount { + + } else { + fadeIn = true + return .Loading(initialData: initialData) + } } } } diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index ec2eb5b916..874b469188 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -363,171 +363,175 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { editingOffset = 0.0 } - if true { - let peer: Peer? - - var messageText: String - if let message = message { - if let messageMain = messageMainPeer(message) { - peer = messageMain - } else { - peer = item.peer.chatMainPeer - } - - messageText = message.text - if message.text.isEmpty { - for media in message.media { - switch media { - case _ as TelegramMediaImage: - if message.text.isEmpty { - messageText = item.strings.Message_Photo - } - case let fileMedia as TelegramMediaFile: - if message.text.isEmpty { - if let fileName = fileMedia.fileName { - messageText = fileName - } else { - messageText = item.strings.Message_File - } - inner: for attribute in fileMedia.attributes { - switch attribute { - case .Animated: - messageText = item.strings.Message_Animation - break inner - case let .Audio(isVoice, _, title, performer, _): - if isVoice { - messageText = item.strings.Message_Audio - break inner - } else { - let descriptionString: String - if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { - descriptionString = title + " — " + performer - } else if let title = title, !title.isEmpty { - descriptionString = title - } else if let performer = performer, !performer.isEmpty { - descriptionString = performer - } else if let fileName = fileMedia.fileName { - descriptionString = fileName - } else { - descriptionString = item.strings.Message_Audio - } - messageText = descriptionString - break inner - } - case let .Sticker(displayText, _): - if displayText.isEmpty { - messageText = item.strings.Message_Sticker - break inner - } else { - messageText = displayText + " " + item.strings.Message_Sticker - break inner - } - case let .Video(_, _, flags): - if flags.contains(.instantRoundVideo) { - messageText = item.strings.Message_VideoMessage - } else { - messageText = item.strings.Message_Video - } - break inner - default: - break - } - } - } - case _ as TelegramMediaMap: - messageText = item.strings.Message_Location - case _ as TelegramMediaContact: - messageText = item.strings.Message_Contact - case let action as TelegramMediaAction: - switch action.action { - case .phoneCall: - if message.effectivelyIncoming { - messageText = item.strings.Notification_CallIncoming - } else { - messageText = item.strings.Notification_CallOutgoing - } - default: - if let text = serviceMessageString(theme: item.theme, strings: item.strings, message: message, accountPeerId: item.account.peerId) { - messageText = text.string - } - } - default: - break - } - } - } + let peer: Peer? + + var hideAuthor = false + var messageText: String + if let message = message { + if let messageMain = messageMainPeer(message) { + peer = messageMain } else { peer = item.peer.chatMainPeer - messageText = "" } - let attributedText: NSAttributedString - if let embeddedState = embeddedState as? ChatEmbeddedInterfaceState { - authorAttributedString = NSAttributedString(string: item.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor) - - attributedText = NSAttributedString(string: embeddedState.text, font: textFont, textColor: theme.messageTextColor) - } else if let message = message, let author = message.author as? TelegramUser, let peer = peer, !(peer is TelegramUser) { - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: theme.messageTextColor) - } else { - let peerText: String = author.id == account.peerId ? item.strings.DialogList_You : author.displayTitle - - authorAttributedString = NSAttributedString(string: peerText, font: textFont, textColor: theme.authorNameColor) - attributedText = NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) - } - } else { - attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: theme.messageTextColor) - } - - if let displayTitle = peer?.displayTitle { - titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat ? theme.secretTitleColor : theme.titleColor) - } - - textAttributedString = attributedText - - var t = Int(item.index.messageIndex.timestamp) - var timeinfo = tm() - localtime_r(&t, &timeinfo) - - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.index.messageIndex.timestamp, relativeTo: timestamp) - - dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: theme.dateTextColor) - - if let message = message, message.author?.id == account.peerId { - if !message.flags.isSending { - if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(MessageIndex(message)) { - statusImage = PresentationResourcesChatList.doubleCheckImage(item.theme) - } else { - statusImage = PresentationResourcesChatList.singleCheckImage(item.theme) + messageText = message.text + if message.text.isEmpty { + for media in message.media { + switch media { + case _ as TelegramMediaImage: + if message.text.isEmpty { + messageText = item.strings.Message_Photo + } + case let fileMedia as TelegramMediaFile: + if message.text.isEmpty { + if let fileName = fileMedia.fileName { + messageText = fileName + } else { + messageText = item.strings.Message_File + } + inner: for attribute in fileMedia.attributes { + switch attribute { + case .Animated: + messageText = item.strings.Message_Animation + break inner + case let .Audio(isVoice, _, title, performer, _): + if isVoice { + messageText = item.strings.Message_Audio + break inner + } else { + let descriptionString: String + if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { + descriptionString = title + " — " + performer + } else if let title = title, !title.isEmpty { + descriptionString = title + } else if let performer = performer, !performer.isEmpty { + descriptionString = performer + } else if let fileName = fileMedia.fileName { + descriptionString = fileName + } else { + descriptionString = item.strings.Message_Audio + } + messageText = descriptionString + break inner + } + case let .Sticker(displayText, _): + if displayText.isEmpty { + messageText = item.strings.Message_Sticker + break inner + } else { + messageText = displayText + " " + item.strings.Message_Sticker + break inner + } + case let .Video(_, _, flags): + if flags.contains(.instantRoundVideo) { + messageText = item.strings.Message_VideoMessage + } else { + messageText = item.strings.Message_Video + } + break inner + default: + break + } + } + } + case _ as TelegramMediaMap: + messageText = item.strings.Message_Location + case _ as TelegramMediaContact: + messageText = item.strings.Message_Contact + case let game as TelegramMediaGame: + messageText = "🎮 \(game.title)" + case let invoice as TelegramMediaInvoice: + messageText = invoice.title + case let action as TelegramMediaAction: + hideAuthor = true + switch action.action { + case .phoneCall: + if message.effectivelyIncoming { + messageText = item.strings.Notification_CallIncoming + } else { + messageText = item.strings.Notification_CallOutgoing + } + default: + if let text = serviceMessageString(theme: item.theme, strings: item.strings, message: message, accountPeerId: item.account.peerId) { + messageText = text.string + } + } + default: + break } } } + } else { + peer = item.peer.chatMainPeer + messageText = "" + } + + let attributedText: NSAttributedString + if let embeddedState = embeddedState as? ChatEmbeddedInterfaceState { + authorAttributedString = NSAttributedString(string: item.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor) - if let combinedReadState = combinedReadState { - let unreadCount = combinedReadState.count - if unreadCount != 0 { - let badgeTextColor: UIColor - if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings { - if case .unmuted = notificationSettings.muteState { - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme) - badgeTextColor = theme.unreadBadgeActiveTextColor - } else { - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.theme) - badgeTextColor = theme.unreadBadgeInactiveTextColor - } - } else { + attributedText = NSAttributedString(string: embeddedState.text, font: textFont, textColor: theme.messageTextColor) + } else if let message = message, let author = message.author as? TelegramUser, let peer = peer, !(peer is TelegramUser) { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: theme.messageTextColor) + } else { + let peerText: String = author.id == account.peerId ? item.strings.DialogList_You : author.displayTitle + + authorAttributedString = NSAttributedString(string: peerText, font: textFont, textColor: theme.authorNameColor) + attributedText = NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) + } + } else { + attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: theme.messageTextColor) + } + + if let displayTitle = peer?.displayTitle { + titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat ? theme.secretTitleColor : theme.titleColor) + } + + textAttributedString = attributedText + + var t = Int(item.index.messageIndex.timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo) + + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.index.messageIndex.timestamp, relativeTo: timestamp) + + dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: theme.dateTextColor) + + if let message = message, message.author?.id == account.peerId { + if !message.flags.isSending { + if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(MessageIndex(message)) { + statusImage = PresentationResourcesChatList.doubleCheckImage(item.theme) + } else { + statusImage = PresentationResourcesChatList.singleCheckImage(item.theme) + } + } + } + + if let combinedReadState = combinedReadState { + let unreadCount = combinedReadState.count + if unreadCount != 0 { + let badgeTextColor: UIColor + if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings { + if case .unmuted = notificationSettings.muteState { currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme) badgeTextColor = theme.unreadBadgeActiveTextColor + } else { + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.theme) + badgeTextColor = theme.unreadBadgeInactiveTextColor } - badgeAttributedString = NSAttributedString(string: "\(unreadCount)", font: badgeFont, textColor: badgeTextColor) + } else { + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme) + badgeTextColor = theme.unreadBadgeActiveTextColor } + badgeAttributedString = NSAttributedString(string: "\(unreadCount)", font: badgeFont, textColor: badgeTextColor) } - - if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings { - if case .muted = notificationSettings.muteState { - currentMutedIconImage = peerMutedIcon - } + } + + if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings { + if case .muted = notificationSettings.muteState { + currentMutedIconImage = peerMutedIcon } } @@ -551,7 +555,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { badgeSize = 0.0 } - let (authorLayout, authorApply) = authorLayout(authorAttributedString, nil, 1, .end, CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)) + let (authorLayout, authorApply) = authorLayout(hideAuthor ? nil : authorAttributedString, nil, 1, .end, CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)) let (textLayout, textApply) = textLayout(textAttributedString, nil, authorAttributedString == nil ? 2 : 1, .end, CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)) diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index 442830592b..d9bd33755d 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -38,13 +38,13 @@ enum ChatListRecentEntryStableId: Hashable { enum ChatListRecentEntry: Comparable, Identifiable { case topPeers([Peer], PresentationTheme, PresentationStrings) - case peer(index: Int, peer: Peer, PresentationTheme, PresentationStrings) + case peer(index: Int, peer: Peer, associatedPeer: Peer?, PresentationTheme, PresentationStrings) var stableId: ChatListRecentEntryStableId { switch self { case .topPeers: return .topPeers - case let .peer(_, peer, _, _): + case let .peer(_, peer, _, _, _): return .peerId(peer.id) } } @@ -71,8 +71,8 @@ enum ChatListRecentEntry: Comparable, Identifiable { } else { return false } - case let .peer(lhsIndex, lhsPeer, lhsTheme, lhsStrings): - if case let .peer(rhsIndex, rhsPeer, rhsTheme, rhsStrings) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings { + case let .peer(lhsIndex, lhsPeer, lhsAssociatedPeer, lhsTheme, lhsStrings): + if case let .peer(rhsIndex, rhsPeer, rhsAssociatedPeer, rhsTheme, rhsStrings) = rhs, lhsPeer.isEqual(rhsPeer) && arePeersEqual(lhsAssociatedPeer, rhsAssociatedPeer) && lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings { return true } else { return false @@ -84,11 +84,11 @@ enum ChatListRecentEntry: Comparable, Identifiable { switch lhs { case .topPeers: return true - case let .peer(lhsIndex, _, _, _): + case let .peer(lhsIndex, _, _, _, _): switch rhs { case .topPeers: return false - case let .peer(rhsIndex, _, _, _): + case let .peer(rhsIndex, _, _, _, _): return lhsIndex <= rhsIndex } } @@ -100,8 +100,17 @@ enum ChatListRecentEntry: Comparable, Identifiable { return ChatListRecentPeersListItem(theme: theme, strings: strings, account: account, peers: peers, peerSelected: { peer in peerSelected(peer) }) - case let .peer(_, peer, theme, strings): - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings), action: { _ in + case let .peer(_, peer, associatedPeer, theme, strings): + let primaryPeer: Peer + var chatPeer: Peer? + if let associatedPeer = associatedPeer { + primaryPeer = associatedPeer + chatPeer = peer + } else { + primaryPeer = peer + chatPeer = associatedPeer + } + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: primaryPeer, chatPeer: chatPeer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings), action: { _ in peerSelected(peer) }) } @@ -151,13 +160,13 @@ enum ChatListSearchEntryStableId: Hashable { enum ChatListSearchEntry: Comparable, Identifiable { - case localPeer(Peer, Int, PresentationTheme, PresentationStrings) + case localPeer(Peer, Peer?, Int, PresentationTheme, PresentationStrings) case globalPeer(Peer, Int, PresentationTheme, PresentationStrings) case message(Message, PresentationTheme, PresentationStrings) var stableId: ChatListSearchEntryStableId { switch self { - case let .localPeer(peer, _, _, _): + case let .localPeer(peer, _, _, _, _): return .localPeerId(peer.id) case let .globalPeer(peer, _, _, _): return .globalPeerId(peer.id) @@ -168,8 +177,8 @@ enum ChatListSearchEntry: Comparable, Identifiable { static func ==(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool { switch lhs { - case let .localPeer(lhsPeer, lhsIndex, lhsTheme, lhsStrings): - if case let .localPeer(rhsPeer, rhsIndex, rhsTheme, rhsStrings) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings { + case let .localPeer(lhsPeer, lhsAssociatedPeer, lhsIndex, lhsTheme, lhsStrings): + if case let .localPeer(rhsPeer, rhsAssociatedPeer, rhsIndex, rhsTheme, rhsStrings) = rhs, lhsPeer.isEqual(rhsPeer) && arePeersEqual(lhsAssociatedPeer, rhsAssociatedPeer) && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings { return true } else { return false @@ -203,8 +212,8 @@ enum ChatListSearchEntry: Comparable, Identifiable { static func <(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool { switch lhs { - case let .localPeer(_, lhsIndex, _, _): - if case let .localPeer(_, rhsIndex, _, _) = rhs { + case let .localPeer(_, _, lhsIndex, _, _): + if case let .localPeer(_, _, rhsIndex, _, _) = rhs { return lhsIndex <= rhsIndex } else { return true @@ -229,8 +238,18 @@ enum ChatListSearchEntry: Comparable, Identifiable { func item(account: Account, enableHeaders: Bool, interaction: ChatListNodeInteraction) -> ListViewItem { switch self { - case let .localPeer(peer, _, theme, strings): - return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings), action: { _ in + case let .localPeer(peer, associatedPeer, _, theme, strings): + let primaryPeer: Peer + var chatPeer: Peer? + if let associatedPeer = associatedPeer { + primaryPeer = associatedPeer + chatPeer = peer + } else { + primaryPeer = peer + chatPeer = associatedPeer + } + + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: primaryPeer, chatPeer: chatPeer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings), action: { _ in interaction.peerSelected(peer) }) case let .globalPeer(peer, _, theme, strings): @@ -280,7 +299,6 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private let account: Account private let openMessage: (Peer, MessageId) -> Void - //private let recentPeersNode: ChatListSearchRecentPeersNode private let recentListNode: ListView private let listNode: ListView @@ -334,9 +352,15 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { |> map { foundLocalPeers, foundRemotePeers, foundRemoteMessages, themeAndStrings -> [ChatListSearchEntry]? in var entries: [ChatListSearchEntry] = [] var index = 0 - for peer in foundLocalPeers { - entries.append(.localPeer(peer, index, themeAndStrings.0, themeAndStrings.1)) - index += 1 + for renderedPeer in foundLocalPeers { + if let peer = renderedPeer.peers[renderedPeer.peerId] { + var associatedPeer: Peer? + if let associatedPeerId = peer.associatedPeerId { + associatedPeer = renderedPeer.peers[associatedPeerId] + } + entries.append(.localPeer(peer, associatedPeer, index, themeAndStrings.0, themeAndStrings.1)) + index += 1 + } } index = 0 @@ -381,8 +405,16 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { |> mapToSignal { [weak self] peers, themeAndStrings -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in var entries: [ChatListRecentEntry] = [] entries.append(.topPeers([], themeAndStrings.0, themeAndStrings.1)) - for i in 0 ..< peers.count { - entries.append(.peer(index: i, peer: peers[i], themeAndStrings.0, themeAndStrings.1)) + var index = 0 + for renderedPeer in peers { + if let peer = renderedPeer.peers[renderedPeer.peerId] { + var associatedPeer: Peer? + if let associatedPeerId = peer.associatedPeerId { + associatedPeer = renderedPeer.peers[associatedPeerId] + } + entries.append(.peer(index: index, peer: peer, associatedPeer: associatedPeer, themeAndStrings.0, themeAndStrings.1)) + index += 1 + } } let previousEntries = previousRecentItems.swap(entries) diff --git a/TelegramUI/ChatListSearchItemHeader.swift b/TelegramUI/ChatListSearchItemHeader.swift index a07a75872c..c597c378cb 100644 --- a/TelegramUI/ChatListSearchItemHeader.swift +++ b/TelegramUI/ChatListSearchItemHeader.swift @@ -3,6 +3,8 @@ import Display enum ChatListSearchItemHeaderType: Int32 { case localPeers + case members + case contacts case globalPeers case recentPeers case messages @@ -48,6 +50,10 @@ final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode { switch type { case .localPeers: self.sectionHeaderNode.title = strings.DialogList_SearchSectionDialogs.uppercased() + case .members: + self.sectionHeaderNode.title = strings.Compose_NewChannel_Members.uppercased() + case .contacts: + self.sectionHeaderNode.title = strings.Contacts_TopSection.uppercased() case .globalPeers: self.sectionHeaderNode.title = strings.DialogList_SearchSectionGlobal.uppercased() case .messages: diff --git a/TelegramUI/ChatMediaInputGifPane.swift b/TelegramUI/ChatMediaInputGifPane.swift index ab8a4f2c19..61952faf9a 100644 --- a/TelegramUI/ChatMediaInputGifPane.swift +++ b/TelegramUI/ChatMediaInputGifPane.swift @@ -31,7 +31,7 @@ final class ChatMediaInputGifPane: ASDisplayNode, UIScrollViewDelegate { } self.disposable.set((gifs |> deliverOnMainQueue).start(next: { [weak self] gifs in if let strongSelf = self { - strongSelf.multiplexedNode.files = [gifs.first!] + strongSelf.multiplexedNode.files = gifs } })) diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index 06246038a9..9d66c583e6 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -8,18 +8,32 @@ import TelegramCore private let titleFont = Font.regular(13.0) private let titleBoldFont = Font.bold(13.0) +private func peerMentionAttributes(theme: PresentationThemeServiceMessage, peerId: PeerId) -> MarkdownAttributeSet { + return MarkdownAttributeSet(font: titleBoldFont, textColor: theme.serviceMessagePrimaryTextColor, additionalAttributes: [TextNode.TelegramPeerMentionAttribute: TelegramPeerMention(peerId: peerId, mention: "")]) +} + +private func peerMentionsAttributes(theme: PresentationThemeServiceMessage, peerIds: [(Int, PeerId?)]) -> [Int: MarkdownAttributeSet] { + var result: [Int: MarkdownAttributeSet] = [:] + for (index, peerId) in peerIds { + if let peerId = peerId { + result[index] = peerMentionAttributes(theme: theme, peerId: peerId) + } + } + return result +} + func serviceMessageString(theme: PresentationTheme, strings: PresentationStrings, message: Message, accountPeerId: PeerId) -> NSAttributedString? { var attributedString: NSAttributedString? let theme = theme.chat.serviceMessage let bodyAttributes = MarkdownAttributeSet(font: titleFont, textColor: theme.serviceMessagePrimaryTextColor, additionalAttributes: [:]) - let linkAttributes = MarkdownAttributeSet(font: titleBoldFont, textColor: theme.serviceMessagePrimaryTextColor, additionalAttributes: [:]) for media in message.media { if let action = media as? TelegramMediaAction { let authorName = message.author?.displayTitle ?? "" + var isChannel = false if message.id.peerId.namespace == Namespaces.Peer.CloudChannel, let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { isChannel = true @@ -33,16 +47,24 @@ func serviceMessageString(theme: PresentationTheme, strings: PresentationStrings attributedString = NSAttributedString(string: strings.Notification_CreatedGroup, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) } case let .addedMembers(peerIds): - if peerIds.first == message.author?.id { - attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedChat(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + if let peerId = peerIds.first, peerId == message.author?.id { + attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedChat(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, peerId)])) } else { - attributedString = addAttributesToStringWithRanges(strings.Notification_Invited(authorName, peerDisplayTitles(peerIds, message.peers)), body: bodyAttributes, argumentAttributes: [0: linkAttributes, 1: linkAttributes]) + var attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)] + if peerIds.count == 1 { + attributePeerIds.append((1, peerIds.first)) + } + attributedString = addAttributesToStringWithRanges(strings.Notification_Invited(authorName, peerDisplayTitles(peerIds, message.peers)), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: attributePeerIds)) } case let .removedMembers(peerIds): if peerIds.first == message.author?.id { - attributedString = addAttributesToStringWithRanges(strings.Notification_LeftChat(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + attributedString = addAttributesToStringWithRanges(strings.Notification_LeftChat(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) } else { - attributedString = addAttributesToStringWithRanges(strings.Notification_Kicked(authorName, peerDisplayTitles(peerIds, message.peers)), body: bodyAttributes, argumentAttributes: [0: linkAttributes, 1: linkAttributes]) + var attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)] + if peerIds.count == 1 { + attributePeerIds.append((1, peerIds.first)) + } + attributedString = addAttributesToStringWithRanges(strings.Notification_Kicked(authorName, peerDisplayTitles(peerIds, message.peers)), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: attributePeerIds)) } case let .photoUpdated(image): if authorName.isEmpty || isChannel { @@ -61,9 +83,9 @@ func serviceMessageString(theme: PresentationTheme, strings: PresentationStrings } } else { if image != nil { - attributedString = addAttributesToStringWithRanges(strings.Notification_ChangedGroupPhoto(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + attributedString = addAttributesToStringWithRanges(strings.Notification_ChangedGroupPhoto(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) } else { - attributedString = addAttributesToStringWithRanges(strings.Notification_RemovedGroupPhoto(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + attributedString = addAttributesToStringWithRanges(strings.Notification_RemovedGroupPhoto(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) } } case let .titleUpdated(title): @@ -74,7 +96,7 @@ func serviceMessageString(theme: PresentationTheme, strings: PresentationStrings attributedString = NSAttributedString(string: strings.Group_MessageTitleUpdated(title).0, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) } } else { - attributedString = addAttributesToStringWithRanges(strings.Notification_ChangedGroupName(authorName, title), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + attributedString = addAttributesToStringWithRanges(strings.Notification_ChangedGroupName(authorName, title), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) } case .pinnedMessageUpdated: enum PinnnedMediaType { @@ -158,31 +180,31 @@ func serviceMessageString(theme: PresentationTheme, strings: PresentationStrings } switch type { - case let .text(text): - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedTextMessage(authorName, text), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) - case .photo: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedPhotoMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) - case .video: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedVideoMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) - case .round: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedRoundMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) - case .audio: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedAudioMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) - case .file: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedDocumentMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) - case .gif: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedAnimationMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) - case .sticker: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedStickerMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) - case .location: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedLocationMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) - case .contact: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedContactMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) - case .deleted: - attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedDeletedMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + case let .text(text): + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedTextMessage(authorName, text.replacingOccurrences(of: "\n", with: " ")), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) + case .photo: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedPhotoMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) + case .video: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedVideoMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) + case .round: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedRoundMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) + case .audio: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedAudioMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) + case .file: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedDocumentMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) + case .gif: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedAnimationMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) + case .sticker: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedStickerMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) + case .location: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedLocationMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) + case .contact: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedContactMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) + case .deleted: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedDeletedMessage(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) } case .joinedByLink: - attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedGroupByLink(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedGroupByLink(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)])) case .channelMigratedFromGroup, .groupMigratedToChannel: attributedString = NSAttributedString(string: strings.Notification_ChannelMigratedFrom, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) case let .messageAutoremoveTimeoutUpdated(timeout): @@ -258,7 +280,9 @@ func serviceMessageString(theme: PresentationTheme, strings: PresentationStrings } ranges.sort(by: { $0.1.location < $1.1.location }) - attributedString = addAttributesToStringWithRanges(formatWithArgumentRanges(baseString, ranges, [authorName, gameTitle ?? ""]), body: bodyAttributes, argumentAttributes: [0: linkAttributes, 1: linkAttributes]) + var argumentAttributes = peerMentionsAttributes(theme: theme, peerIds: [(0, message.author?.id)]) + argumentAttributes[1] = MarkdownAttributeSet(font: titleBoldFont, textColor: theme.serviceMessagePrimaryTextColor, additionalAttributes: [:]) + attributedString = addAttributesToStringWithRanges(formatWithArgumentRanges(baseString, ranges, [authorName, gameTitle ?? ""]), body: bodyAttributes, argumentAttributes: argumentAttributes) case .phoneCall: break default: @@ -274,7 +298,8 @@ func serviceMessageString(theme: PresentationTheme, strings: PresentationStrings class ChatMessageActionItemNode: ChatMessageItemView { let labelNode: TextNode - let backgroundNode: ASImageNode + let filledBackgroundNode: LinkHighlightingNode + var linkHighlightingNode: LinkHighlightingNode? private let fetchDisposable = MetaDisposable() @@ -285,14 +310,11 @@ class ChatMessageActionItemNode: ChatMessageItemView { self.labelNode.isLayerBacked = true self.labelNode.displaysAsynchronously = true - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false + self.filledBackgroundNode = LinkHighlightingNode(color: .clear) super.init(layerBacked: false) - self.addSubnode(self.backgroundNode) + self.addSubnode(self.filledBackgroundNode) self.addSubnode(self.labelNode) } @@ -304,45 +326,80 @@ class ChatMessageActionItemNode: ChatMessageItemView { self.fetchDisposable.dispose() } - override func setupItem(_ item: ChatMessageItem) { - super.setupItem(item) + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { _ in + return .waitForSingleTap + } + recognizer.highlight = { [weak self] point in + if let strongSelf = self { + strongSelf.updateTouchesAtPoint(point) + } + } + self.view.addGestureRecognizer(recognizer) } override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - let labelLayout = TextNode.asyncLayout(self.labelNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants - let currentItem = self.appliedItem + let backgroundLayout = self.filledBackgroundNode.asyncLayout() return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in - var updatedBackgroundImage: UIImage? - - if item.theme !== currentItem?.theme { - updatedBackgroundImage = PresentationResourcesChat.chatServiceBubbleFillImage(item.theme) - } - let attributedString = serviceMessageString(theme: item.theme, strings: item.strings, message: item.message, accountPeerId: item.account.peerId) - let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (labelLayout, apply) = makeLabelLayout(attributedString, nil, 0, .end, CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), .center, nil, UIEdgeInsets()) - let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0) + var labelRects = labelLayout.linesRects() + if labelRects.count > 1 { + let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) + for i in 0 ..< sortedIndices.count { + let index = sortedIndices[i] + for j in -1 ... 1 { + if j != 0 && index + j >= 0 && index + j < sortedIndices.count { + if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 { + labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width) + } + } + } + } + } + for i in 0 ..< labelRects.count { + /*if i != 0 && i != labelRects.count - 1 { + if labelRects[i - 1].width > labelRects[i].width && labelRects[i + 1].width > labelRects[i].width { + if abs(labelRects[i - 1].width - labelRects[i].width) < abs(labelRects[i + 1].width - labelRects[i].width) { + labelRects[i].size.width = labelRects[i - 1].width + } else { + labelRects[i].size.width = labelRects[i + 1].width + } + } + }*/ + + labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0)) + labelRects[i].size.height = 20.0 + labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) + } + + let backgroundApply = backgroundLayout(item.theme.chat.serviceMessage.serviceMessageFillColor, labelRects, 10.0, 10.0, 0.0) + + let backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) var layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) if dateHeaderAtBottom { layoutInsets.top += layoutConstants.timestampHeaderHeight } - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 20.0), insets: layoutInsets), { [weak self] animation in + return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: labelLayout.size.height + 4.0), insets: layoutInsets), { [weak self] animation in if let strongSelf = self { strongSelf.appliedItem = item - if let updatedBackgroundImage = updatedBackgroundImage { - strongSelf.backgroundNode.image = updatedBackgroundImage - } - let _ = apply() + let _ = backgroundApply() - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize) - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.backgroundNode.frame.origin.x + 8.0, y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0) - 1.0), size: size.size) + let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - labelLayout.size.width) / 2.0), y: floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) + strongSelf.labelNode.frame = labelFrame + strongSelf.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) } }) } @@ -351,14 +408,196 @@ class ChatMessageActionItemNode: ChatMessageItemView { override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } override func animateAdded(_ currentTimestamp: Double, duration: Double) { super.animateAdded(currentTimestamp, duration: duration) - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .began: + break + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + var foundTapAction = false + let tapAction = self.tapActionAtPoint(location) + switch tapAction { + case .none, .ignore: + break + case let .url(url): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.openUrl(url) + } + case let .peerMention(peerId, _): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.openPeer(peerId, .chat(textInputState: nil), nil) + } + case let .textMention(name): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.openPeerMention(name) + } + case let .botCommand(command): + foundTapAction = true + if let item = self.item, let controllerInteraction = self.controllerInteraction { + controllerInteraction.sendBotCommand(item.message.id, command) + } + case let .hashtag(peerName, hashtag): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.openHashtag(peerName, hashtag) + } + case .instantPage: + foundTapAction = true + if let item = self.item, let controllerInteraction = self.controllerInteraction { + controllerInteraction.openInstantPage(item.message.id) + } + case .holdToPreviewSecretMedia: + foundTapAction = true + case let .call(peerId): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.callPeer(peerId) + } + } + if !foundTapAction { + self.controllerInteraction?.clickThroughMessage() + } + case .longTap, .doubleTap: + if let item = self.item, self.labelNode.frame.contains(location) { + var foundTapAction = false + let tapAction = self.tapActionAtPoint(location) + switch tapAction { + case .none, .ignore: + break + case let .url(url): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.longTap(.url(url)) + } + case let .peerMention(peerId, mention): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.longTap(.peerMention(peerId, mention)) + } + case let .textMention(name): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.longTap(.mention(name)) + } + case let .botCommand(command): + foundTapAction = true + if let _ = self.item, let controllerInteraction = self.controllerInteraction { + controllerInteraction.longTap(.command(command)) + } + case let .hashtag(_, hashtag): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.longTap(.hashtag(hashtag)) + } + case .instantPage: + break + case .holdToPreviewSecretMedia: + break + case .call: + break + } + + if !foundTapAction { + self.controllerInteraction?.openMessageContextMenu(item.message.id, self, self.filledBackgroundNode.frame) + } + } + case .hold: + break + } + } + case .cancelled: + break + default: + break + } + } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + if let item = self.item { + var rects: [CGRect]? + let textNodeFrame = self.labelNode.frame + if let point = point { + if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)) { + let possibleNames: [String] = [ + TextNode.UrlAttribute, + TextNode.TelegramPeerMentionAttribute, + TextNode.TelegramPeerTextMentionAttribute, + TextNode.TelegramBotCommandAttribute, + TextNode.TelegramHashtagAttribute + ] + for name in possibleNames { + if let _ = attributes[name] { + rects = self.labelNode.attributeRects(name: name, at: index) + break + } + } + } + } + + if let rects = rects { + var mappedRects = rects + for i in 0 ..< mappedRects.count { + mappedRects[i].origin.x = floor((textNodeFrame.size.width - mappedRects[i].width) / 2.0) + } + + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming ? item.theme.chat.bubble.incomingLinkHighlightColor : item.theme.chat.bubble.outgoingLinkHighlightColor) + linkHighlightingNode.inset = 2.5 + self.linkHighlightingNode = linkHighlightingNode + self.insertSubnode(linkHighlightingNode, belowSubnode: self.labelNode) + } + linkHighlightingNode.frame = self.labelNode.frame.offsetBy(dx: 0.0, dy: 1.5) + linkHighlightingNode.updateRects(mappedRects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + } + + private func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { + let textNodeFrame = self.labelNode.frame + if let (_, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)) { + if let url = attributes[TextNode.UrlAttribute] as? String { + return .url(url) + } else if let peerMention = attributes[TextNode.TelegramPeerMentionAttribute] as? TelegramPeerMention { + return .peerMention(peerMention.peerId, peerMention.mention) + } else if let peerName = attributes[TextNode.TelegramPeerTextMentionAttribute] as? String { + return .textMention(peerName) + } else if let botCommand = attributes[TextNode.TelegramBotCommandAttribute] as? String { + return .botCommand(botCommand) + } else if let hashtag = attributes[TextNode.TelegramHashtagAttribute] as? TelegramHashtag { + return .hashtag(hashtag.peerName, hashtag.hashtag) + } else { + return .none + } + } else { + return .none + } } } diff --git a/TelegramUI/ChatMessageAttachedContentNode.swift b/TelegramUI/ChatMessageAttachedContentNode.swift new file mode 100644 index 0000000000..b3bd57d4de --- /dev/null +++ b/TelegramUI/ChatMessageAttachedContentNode.swift @@ -0,0 +1,488 @@ +import Foundation +import Postbox +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import Postbox + +private let titleFont: UIFont = Font.semibold(15.0) +private let textFont: UIFont = Font.regular(15.0) +private let textBoldFont: UIFont = Font.semibold(15.0) +private let textFixedFont: UIFont = Font.regular(15.0) + +struct ChatMessageAttachedContentNodeMediaFlags: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + init() { + self.rawValue = 0 + } + + static let preferMediaInline = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 0) + static let preferMediaBeforeText = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 1) +} + +final class ChatMessageAttachedContentNode: ASDisplayNode { + private let lineNode: ASImageNode + private let textNode: TextNode + private let inlineImageNode: TransformImageNode + private var contentImageNode: ChatMessageInteractiveMediaNode? + private var contentFileNode: ChatMessageInteractiveFileNode? + + private let statusNode: ChatMessageDateAndStatusNode + + private var message: Message? + private var media: Media? + + var openMedia: (() -> Void)? + var activateAction: (() -> Void)? + + var visibility: ListViewItemNodeVisibility = .none { + didSet { + self.contentImageNode?.visibility = self.visibility + } + } + + override init() { + self.lineNode = ASImageNode() + self.lineNode.isLayerBacked = true + self.lineNode.displaysAsynchronously = false + self.lineNode.displayWithoutProcessing = true + + self.textNode = TextNode() + self.textNode.isLayerBacked = true + self.textNode.displaysAsynchronously = true + self.textNode.contentsScale = UIScreenScale + self.textNode.contentMode = .topLeft + + self.inlineImageNode = TransformImageNode() + self.inlineImageNode.isLayerBacked = true + self.inlineImageNode.displaysAsynchronously = false + + self.statusNode = ChatMessageDateAndStatusNode() + + super.init() + + self.addSubnode(self.lineNode) + self.addSubnode(self.textNode) + + self.addSubnode(self.statusNode) + } + + func asyncLayout() -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ account: Account, _ message: Message, _ messageRead: Bool, _ title: String?, _ subtitle: String?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + let textAsyncLayout = TextNode.asyncLayout(self.textNode) + let currentImage = self.media as? TelegramMediaImage + let imageLayout = self.inlineImageNode.asyncLayout() + let statusLayout = self.statusNode.asyncLayout() + let contentImageLayout = ChatMessageInteractiveMediaNode.asyncLayout(self.contentImageNode) + let contentFileLayout = ChatMessageInteractiveFileNode.asyncLayout(self.contentFileNode) + + return { theme, strings, account, message, messageRead, title, subtitle, text, entities, mediaAndFlags, displayLine, layoutConstants, position, constrainedSize in + let incoming = message.effectivelyIncoming + + var insets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 5.0, right: 8.0) + switch position.top { + case .None: + insets.top += 8.0 + default: + break + } + if displayLine { + insets.left += 11.0 + } + + var preferMediaBeforeText = false + if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { + preferMediaBeforeText = true + } + + var t = Int(message.timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo) + + var edited = false + var sentViaBot = false + var viewCount: Int? + for attribute in message.attributes { + if let _ = attribute as? EditedMessageAttribute { + edited = true + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let _ = attribute as? InlineBotMessageAttribute { + sentViaBot = true + } + } + + var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + + if let author = message.author as? TelegramUser { + if author.botInfo != nil { + sentViaBot = true + } + if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + dateText = "\(author.displayTitle), \(dateText)" + } + } + + var textString: NSAttributedString? + var inlineImageDimensions: CGSize? + var inlineImageSize: CGSize? + var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var textCutout: TextNodeCutout? + var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude + var refineContentImageLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode)))? + var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode)))? + + let string = NSMutableAttributedString() + var notEmpty = false + + let bubbleTheme = theme.chat.bubble + + if let title = title, !title.isEmpty { + string.append(NSAttributedString(string: title, font: titleFont, textColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor)) + notEmpty = true + } + + if let subtitle = subtitle, !subtitle.isEmpty { + if notEmpty { + string.append(NSAttributedString(string: "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) + } + string.append(NSAttributedString(string: subtitle, font: titleFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) + notEmpty = true + } + + if let text = text, !text.isEmpty { + if notEmpty { + string.append(NSAttributedString(string: "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) + } + if let entities = entities { + string.append(stringWithAppliedEntities(text, entities: entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: textFont, boldFont: textBoldFont, fixedFont: textFixedFont)) + } else { + string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) + } + notEmpty = true + } + + textString = string + + if let (media, flags) = mediaAndFlags { + if let file = media as? TelegramMediaFile { + if file.isVideo { + let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, file, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) + initialWidth = initialImageWidth + insets.left + insets.right + refineContentImageLayout = refineLayout + } else { + var automaticDownload = false + if file.isVoice { + automaticDownload = true + } + let (_, refineLayout) = contentFileLayout(account, theme, strings, message, file, automaticDownload,message.effectivelyIncoming, nil, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) + refineContentFileLayout = refineLayout + } + } else if let image = media as? TelegramMediaImage { + if !flags.contains(.preferMediaInline) { + let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) + initialWidth = initialImageWidth + insets.left + insets.right + refineContentImageLayout = refineLayout + } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { + inlineImageDimensions = dimensions + + if image != currentImage { + updateInlineImageSignal = chatWebpageSnippetPhoto(account: account, photo: image) + } + } + } else if let image = media as? TelegramMediaWebFile { + let (initialImageWidth, _, refineLayout) = contentImageLayout(account, theme, strings, message, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) + initialWidth = initialImageWidth + insets.left + insets.right + refineContentImageLayout = refineLayout + } + } + + if let _ = inlineImageDimensions { + inlineImageSize = CGSize(width: 54.0, height: 54.0) + + if let inlineImageSize = inlineImageSize { + textCutout = TextNodeCutout(position: .TopRight, size: CGSize(width: inlineImageSize.width + 10.0, height: inlineImageSize.height + 10.0)) + } + } + + return (initialWidth, { constrainedSize in + let statusType: ChatMessageDateAndStatusType + if message.effectivelyIncoming { + statusType = .BubbleIncoming + } else { + if message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if message.flags.isSending { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: messageRead)) + } + } + + let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom) + + var statusSizeAndApply: (CGSize, (Bool) -> Void)? + + if (refineContentImageLayout == nil && refineContentFileLayout == nil) || preferMediaBeforeText { + statusSizeAndApply = statusLayout(theme, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) + } + + let (textLayout, textApply) = textAsyncLayout(textString, nil, 12, .end, textConstrainedSize, .natural, textCutout, UIEdgeInsets()) + + var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) + + var statusFrame: CGRect? + + if let (statusSize, _) = statusSizeAndApply { + var frame = CGRect(origin: CGPoint(), size: statusSize) + + let trailingLineWidth = textLayout.trailingLineWidth + if textLayout.size.width - trailingLineWidth >= statusSize.width { + frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY - statusSize.height) + } else if trailingLineWidth + statusSize.width < textConstrainedSize.width { + frame.origin = CGPoint(x: textFrame.minX + trailingLineWidth, y: textFrame.maxY - statusSize.height) + } else { + frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY) + } + + if let inlineImageSize = inlineImageSize { + if frame.origin.y < inlineImageSize.height + 4.0 { + frame.origin.y = inlineImageSize.height + 4.0 + } + } + + frame = frame.offsetBy(dx: insets.left, dy: insets.top) + statusFrame = frame + } + + textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top) + + let lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(theme) + + var boundingSize = textFrame.size + if let statusFrame = statusFrame { + boundingSize = textFrame.union(statusFrame).size + } + var lineHeight = textFrame.size.height + if let inlineImageSize = inlineImageSize { + if boundingSize.height < inlineImageSize.height { + boundingSize.height = inlineImageSize.height + } + if lineHeight < inlineImageSize.height { + lineHeight = inlineImageSize.height + } + } + + var finalizeContentImageLayout: ((CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))? + if let refineContentImageLayout = refineContentImageLayout { + let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize) + finalizeContentImageLayout = finalizeImageLayout + + boundingSize.width = max(boundingSize.width, refinedWidth) + } + var finalizeContentFileLayout: ((CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))? + if let refineContentFileLayout = refineContentFileLayout { + let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize) + finalizeContentFileLayout = finalizeFileLayout + + boundingSize.width = max(boundingSize.width, refinedWidth) + } + + boundingSize.width += insets.left + insets.right + boundingSize.height += insets.top + insets.bottom + lineHeight += insets.top + insets.bottom + + var imageApply: (() -> Void)? + if let inlineImageSize = inlineImageSize, let inlineImageDimensions = inlineImageDimensions { + let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) + let arguments = TransformImageArguments(corners: imageCorners, imageSize: inlineImageDimensions.aspectFilled(inlineImageSize), boundingSize: inlineImageSize, intrinsicInsets: UIEdgeInsets()) + imageApply = imageLayout(arguments) + } + + return (boundingSize.width, { boundingWidth in + var adjustedBoundingSize = boundingSize + var adjustedLineHeight = lineHeight + + var imageFrame: CGRect? + if let inlineImageSize = inlineImageSize { + imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right, y: 0.0), size: inlineImageSize) + } + + var contentImageSizeAndApply: (CGSize, () -> ChatMessageInteractiveMediaNode)? + if let finalizeContentImageLayout = finalizeContentImageLayout { + let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right) + contentImageSizeAndApply = (size, apply) + + var imageHeigthAddition = size.height + if textFrame.size.height > CGFloat.ulpOfOne { + imageHeigthAddition += 2.0 + } + + adjustedBoundingSize.height += imageHeigthAddition + 5.0 + adjustedLineHeight += imageHeigthAddition + 4.0 + } + + var contentFileSizeAndApply: (CGSize, () -> ChatMessageInteractiveFileNode)? + if let finalizeContentFileLayout = finalizeContentFileLayout { + let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right) + contentFileSizeAndApply = (size, apply) + + var imageHeigthAddition = size.height + if textFrame.size.height > CGFloat.ulpOfOne { + imageHeigthAddition += 2.0 + } + + adjustedBoundingSize.height += imageHeigthAddition + 5.0 + adjustedLineHeight += imageHeigthAddition + 4.0 + } + + /*if let _ = webPageContent?.instantPage { + adjustedBoundingSize.height += 4.0 + }*/ + + var adjustedStatusFrame: CGRect? + if let statusFrame = statusFrame { + adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - insets.right, y: statusFrame.origin.y), size: statusFrame.size) + } + + return (adjustedBoundingSize, { [weak self] animation in + if let strongSelf = self { + strongSelf.message = message + strongSelf.media = mediaAndFlags?.0 + + var hasAnimation = true + if case .None = animation { + hasAnimation = false + } + + strongSelf.lineNode.image = lineImage + strongSelf.lineNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 0.0), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)) + strongSelf.lineNode.isHidden = !displayLine + + let _ = textApply() + + if let imageFrame = imageFrame { + if let updateImageSignal = updateInlineImageSignal { + strongSelf.inlineImageNode.setSignal(account: account, signal: updateImageSignal) + } + + strongSelf.inlineImageNode.frame = imageFrame + if strongSelf.inlineImageNode.supernode == nil { + strongSelf.addSubnode(strongSelf.inlineImageNode) + } + + if let imageApply = imageApply { + imageApply() + } + } else if strongSelf.inlineImageNode.supernode != nil { + strongSelf.inlineImageNode.removeFromSupernode() + } + + var contentMediaHeight: CGFloat? + + if let (contentImageSize, contentImageApply) = contentImageSizeAndApply { + contentMediaHeight = contentImageSize.height + + let contentImageNode = contentImageApply() + if strongSelf.contentImageNode !== contentImageNode { + strongSelf.contentImageNode = contentImageNode + strongSelf.addSubnode(contentImageNode) + contentImageNode.activateLocalContent = { [weak strongSelf] in + if let strongSelf = strongSelf { + strongSelf.openMedia?() + } + } + contentImageNode.visibility = strongSelf.visibility + } + let _ = contentImageApply() + if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { + contentImageNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize) + } else { + contentImageNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentImageSize) + } + } else if let contentImageNode = strongSelf.contentImageNode { + contentImageNode.visibility = .none + contentImageNode.removeFromSupernode() + strongSelf.contentImageNode = nil + } + + if let (contentFileSize, contentFileApply) = contentFileSizeAndApply { + contentMediaHeight = contentFileSize.height + + let contentFileNode = contentFileApply() + if strongSelf.contentFileNode !== contentFileNode { + strongSelf.contentFileNode = contentFileNode + strongSelf.addSubnode(contentFileNode) + contentFileNode.activateLocalContent = { [weak strongSelf] in + if let strongSelf = strongSelf { + strongSelf.openMedia?() + } + } + } + let _ = contentFileApply() + if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { + contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentFileSize) + } else { + contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentFileSize) + } + } else if let contentFileNode = strongSelf.contentFileNode { + contentFileNode.removeFromSupernode() + strongSelf.contentFileNode = nil + } + + var textVerticalOffset: CGFloat = 0.0 + if let contentMediaHeight = contentMediaHeight, let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { + textVerticalOffset = contentMediaHeight + 7.0 + } + + strongSelf.textNode.frame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) + + if let (_, statusApply) = statusSizeAndApply, let adjustedStatusFrame = adjustedStatusFrame { + strongSelf.statusNode.frame = adjustedStatusFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) + if strongSelf.statusNode.supernode == nil { + strongSelf.addSubnode(strongSelf.statusNode) + } + statusApply(hasAnimation) + } else if strongSelf.statusNode.supernode != nil { + strongSelf.statusNode.removeFromSupernode() + } + } + }) + }) + }) + } + } + + func updateHiddenMedia(_ media: [Media]?) { + if let currentMedia = self.media { + if let media = media { + var found = false + for m in media { + if currentMedia.isEqual(m) { + found = true + break + } + } + if let contentImageNode = self.contentImageNode { + contentImageNode.isHidden = found + } + } else if let contentImageNode = self.contentImageNode { + contentImageNode.isHidden = false + } + } + } + + func transitionNode(media: Media) -> ASDisplayNode? { + if let image = self.media as? TelegramMediaImage, image.isEqual(media) { + return self.contentImageNode + } else if let file = self.media as? TelegramMediaFile, file.isEqual(media) { + return self.contentImageNode + } + return nil + } +} diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 6a00b2521e..a892e96a87 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -6,6 +6,7 @@ import TelegramCore private func contentNodeClassesForItem(_ item: ChatMessageItem) -> [AnyClass] { var result: [AnyClass] = [] + var skipText = false for media in item.message.media { if let _ = media as? TelegramMediaImage { result.append(ChatMessageMediaBubbleContentNode.self) @@ -17,10 +18,20 @@ private func contentNodeClassesForItem(_ item: ChatMessageItem) -> [AnyClass] { } } else if let action = media as? TelegramMediaAction, case .phoneCall = action.action { result.append(ChatMessageCallBubbleContentNode.self) + } else if let _ = media as? TelegramMediaMap { + result.append(ChatMessageMapBubbleContentNode.self) + } else if let _ = media as? TelegramMediaGame { + skipText = true + result.append(ChatMessageGameBubbleContentNode.self) + break + } else if let _ = media as? TelegramMediaInvoice { + skipText = true + result.append(ChatMessageInvoiceBubbleContentNode.self) + break } } - if !item.message.text.isEmpty { + if !skipText && !item.message.text.isEmpty { result.append(ChatMessageTextBubbleContentNode.self) } @@ -182,7 +193,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let strongSelf = self { for contentNode in strongSelf.contentNodes { var translatedPoint: CGPoint? - if let point = point { + if let point = point, contentNode.frame.insetBy(dx: -4.0, dy: -4.0).contains(point) { translatedPoint = CGPoint(x: point.x - contentNode.frame.minX, y: point.y - contentNode.frame.minY) } contentNode.updateTouchesAtPoint(translatedPoint) @@ -266,6 +277,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var inlineBotNameString: String? var replyMessage: Message? var replyMarkup: ReplyMarkupMessageAttribute? + var authorNameColor: UIColor? for attribute in message.attributes { if let attribute = attribute as? InlineBotMessageAttribute, let bot = message.peers[attribute.peerId] as? TelegramUser { @@ -277,10 +289,36 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } - var displayHeader = true + var initialDisplayHeader = true if inlineBotNameString == nil && message.forwardInfo == nil && replyMessage == nil { if let first = contentPropertiesAndPrepareLayouts.first, first.0.hidesSimpleAuthorHeader { - displayHeader = false + initialDisplayHeader = false + } + } + + if initialDisplayHeader && displayAuthorInfo { + if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + authorNameString = peer.displayTitle + authorNameColor = chatMessagePeerIdColors[Int(peer.id.id % 6)] + } else if let author = message.author { + authorNameString = author.displayTitle + authorNameColor = chatMessagePeerIdColors[Int(author.id.id % 6)] + } + } + + var displayHeader = false + if initialDisplayHeader { + if authorNameString != nil { + displayHeader = true + } + if inlineBotNameString != nil { + displayHeader = true + } + if message.forwardInfo != nil { + displayHeader = true + } + if replyMessage != nil { + displayHeader = true } } @@ -327,7 +365,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var nameNodeOriginY: CGFloat = 0.0 var nameNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil }) - var authorNameColor: UIColor? var replyInfoOriginY: CGFloat = 0.0 var replyInfoSizeApply: (CGSize, () -> ChatMessageReplyInfoNode?) = (CGSize(), { nil }) @@ -336,16 +373,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var forwardInfoSizeApply: (CGSize, () -> ChatMessageForwardInfoNode?) = (CGSize(), { nil }) if displayHeader { - if displayAuthorInfo { - if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - authorNameString = peer.displayTitle - authorNameColor = chatMessagePeerIdColors[Int(peer.id.id % 6)] - } else if let author = message.author { - authorNameString = author.displayTitle - authorNameColor = chatMessagePeerIdColors[Int(author.id.id % 6)] - } - } - if authorNameString != nil || inlineBotNameString != nil { if headerSize.height < CGFloat.ulpOfOne { headerSize.height += 4.0 @@ -915,7 +942,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { case let .peerMention(peerId, mention): foundTapAction = true if let controllerInteraction = self.controllerInteraction { - controllerInteraction.longTap(.mention(mention)) + controllerInteraction.longTap(.peerMention(peerId, mention)) } break loop case let .textMention(name): diff --git a/TelegramUI/ChatMessageFileBubbleContentNode.swift b/TelegramUI/ChatMessageFileBubbleContentNode.swift index 33ba537963..61bbe195b0 100644 --- a/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -64,7 +64,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { automaticDownload = true } - let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item, selectedFile!, automaticDownload, item.message.effectivelyIncoming, statusType, CGSize(width: constrainedSize.width, height: constrainedSize.height)) + let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item.theme, item.strings, item.message, selectedFile!, automaticDownload, item.message.effectivelyIncoming, statusType, CGSize(width: constrainedSize.width, height: constrainedSize.height)) return (initialWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { constrainedSize in let (refinedWidth, finishLayout) = refineLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageGameBubbleContentNode.swift b/TelegramUI/ChatMessageGameBubbleContentNode.swift new file mode 100644 index 0000000000..1e94a6577a --- /dev/null +++ b/TelegramUI/ChatMessageGameBubbleContentNode.swift @@ -0,0 +1,130 @@ +import Foundation +import Postbox +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore + +final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { + private var item: ChatMessageItem? + private var game: TelegramMediaGame? + + private let contentNode: ChatMessageAttachedContentNode + + override var properties: ChatMessageBubbleContentProperties { + return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0) + } + + override var visibility: ListViewItemNodeVisibility { + didSet { + self.contentNode.visibility = self.visibility + } + } + + required init() { + self.contentNode = ChatMessageAttachedContentNode() + + super.init() + + self.addSubnode(self.contentNode) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + let contentNodeLayout = self.contentNode.asyncLayout() + + return { item, layoutConstants, position, constrainedSize in + var game: TelegramMediaGame? + var messageEntities: [MessageTextEntity]? + + for media in item.message.media { + if let media = media as? TelegramMediaGame { + game = media + break + } + } + + for attribute in item.message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + messageEntities = attribute.entities + break + } + } + + var title: String? + let subtitle: String? = nil + var text: String? + var mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? + + + if let game = game { + title = game.title + text = game.description + + if let file = game.file { + mediaAndFlags = (file, [.preferMediaBeforeText]) + } else if let image = game.image { + mediaAndFlags = (image, [.preferMediaBeforeText]) + } + } + + let (initialWidth, continueLayout) = contentNodeLayout(item.theme, item.strings, item.account, item.message, item.read, title, subtitle, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, true, layoutConstants, position, constrainedSize) + + return (initialWidth, { constrainedSize in + let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize) + + return (refinedWidth, { boundingWidth in + let (size, apply) = finalizeLayout(boundingWidth) + + return (size, { [weak self] animation in + if let strongSelf = self { + strongSelf.game = game + + apply(animation) + + strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size) + } + }) + }) + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + + override func animateInsertionIntoBubble(_ duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { + if self.bounds.contains(point) { + /*if let webPage = self.webPage, case let .Loaded(content) = webPage.content { + if content.instantPage != nil { + return .instantPage + } + }*/ + } + return .none + } + + override func updateHiddenMedia(_ media: [Media]?) { + self.contentNode.updateHiddenMedia(media) + } + + override func transitionNode(media: Media) -> ASDisplayNode? { + return self.contentNode.transitionNode(media: media) + } +} diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index 5a0f08d247..a0966544d4 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -33,7 +33,8 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { var activateLocalContent: () -> Void = { } private var account: Account? - private var item: ChatMessageItem? + private var message: Message? + private var themeAndStrings: (PresentationTheme, PresentationStrings)? private var file: TelegramMediaFile? init() { @@ -72,7 +73,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if let resourceStatus = self.resourceStatus { switch resourceStatus { case let .fetchStatus(fetchStatus): - if let account = self.account, let message = self.item?.message, message.flags.isSending { + if let account = self.account, let message = self.message, message.flags.isSending { let _ = account.postbox.modify({ modifier -> Void in modifier.deleteMessages([message.id]) }).start() @@ -104,22 +105,21 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - func asyncLayout() -> (_ account: Account, _ item: ChatMessageItem, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { + func asyncLayout() -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { let currentFile = self.file let titleAsyncLayout = TextNode.asyncLayout(self.titleNode) let descriptionAsyncLayout = TextNode.asyncLayout(self.descriptionNode) let statusLayout = self.dateAndStatusNode.asyncLayout() - let currentItem = self.item + let currentMessage = self.message + let currentTheme = self.themeAndStrings?.0 - return { account, item, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in - let message = item.message - + return { account, theme, strings, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in var updatedTheme: PresentationTheme? - if item.theme !== currentItem?.theme { - updatedTheme = item.theme + if theme !== currentTheme { + updatedTheme = theme } return (CGFloat.greatestFiniteMagnitude, { constrainedSize in @@ -134,8 +134,6 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { mediaUpdated = true } - let currentMessage = currentItem?.message - var statusUpdated = mediaUpdated if currentMessage?.id != message.id || currentMessage?.flags != message.flags { statusUpdated = true @@ -163,9 +161,9 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if let attribute = attribute as? ConsumableContentMessageAttribute { if !attribute.consumed { if incoming { - consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentIncomingIcon(item.theme) + consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentIncomingIcon(theme) } else { - consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(item.theme) + consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(theme) } } break @@ -192,16 +190,16 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) - if let author = item.message.author as? TelegramUser { + if let author = message.author as? TelegramUser { if author.botInfo != nil { sentViaBot = true } - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { dateText = "\(author.displayTitle), \(dateText)" } } - let (size, apply) = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, constrainedSize) + let (size, apply) = statusLayout(theme, edited && !sentViaBot, viewCount, dateText, statusType, constrainedSize) statusSize = size statusApply = apply } @@ -214,7 +212,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { var isVoice = false var audioDuration: Int32 = 0 - let bubbleTheme = item.theme.chat.bubble + let bubbleTheme = theme.chat.bubble for attribute in file.attributes { if case let .Audio(voice, duration, title, performer, waveform) = attribute { @@ -301,7 +299,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { minLayoutWidth = max(titleLayout.size.width, descriptionLayout.size.width) + 44.0 + 8.0 } - let fileIconImage = incoming ? PresentationResourcesChat.chatBubbleRadialIndicatorFileIconIncoming(item.theme) : PresentationResourcesChat.chatBubbleRadialIndicatorFileIconOutgoing(item.theme) + let fileIconImage = incoming ? PresentationResourcesChat.chatBubbleRadialIndicatorFileIconIncoming(theme) : PresentationResourcesChat.chatBubbleRadialIndicatorFileIconOutgoing(theme) return (minLayoutWidth, { boundingWidth in let progressDiameter: CGFloat = isVoice ? 37.0 : 44.0 @@ -328,7 +326,8 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { return (fittedLayoutSize, { [weak self] in if let strongSelf = self { strongSelf.account = account - strongSelf.item = item + strongSelf.themeAndStrings = (theme, strings) + strongSelf.message = message strongSelf.file = file let _ = titleApply() @@ -435,12 +434,12 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ item: ChatMessageItem, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) { let currentAsyncLayout = node?.asyncLayout() - return { account, item, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in + return { account, theme, strings, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in var fileNode: ChatMessageInteractiveFileNode - var fileLayout: (_ account: Account, _ item: ChatMessageItem, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAnsStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) + var fileLayout: (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { fileNode = node @@ -450,7 +449,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { fileLayout = fileNode.asyncLayout() } - let (initialWidth, continueLayout) = fileLayout(account, item, file, automaticDownload, incoming, dateAndStatusType, constrainedSize) + let (initialWidth, continueLayout) = fileLayout(account, theme, strings, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize) return (initialWidth, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageInteractiveMediaLabelNode.swift b/TelegramUI/ChatMessageInteractiveMediaLabelNode.swift new file mode 100644 index 0000000000..0f227dab93 --- /dev/null +++ b/TelegramUI/ChatMessageInteractiveMediaLabelNode.swift @@ -0,0 +1,24 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class ChatMessageInteractiveMediaLabelNode: ASDisplayNode { + private let backgroundNode: ASImageNode + private let textNode: TextNode + + override init() { + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.displaysAsynchronously = false + + self.textNode = TextNode() + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textNode) + } + + +} diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index b34bd96c39..6456414718 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -15,12 +15,13 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { private var videoNode: ManagedVideoNode? private var progressNode: RadialProgressNode? private var timeoutNode: RadialTimeoutNode? + private var labelNode: ChatMessageInteractiveMediaLabelNode? private var tapRecognizer: UITapGestureRecognizer? private var account: Account? private var messageIdAndFlags: (MessageId, MessageFlags)? private var media: Media? - private var item: ChatMessageItem? + private var themeAndStrings: (PresentationTheme, PresentationStrings)? private let statusDisposable = MetaDisposable() private let fetchControls = Atomic(value: nil) @@ -61,6 +62,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } override func didLoad() { + super.didLoad() + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:))) self.imageNode.view.addGestureRecognizer(tapRecognizer) self.tapRecognizer = tapRecognizer @@ -102,7 +105,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - func asyncLayout() -> (_ account: Account, _ item: ChatMessageItem, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { + func asyncLayout() -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { let currentMessageIdAndFlags = self.messageIdAndFlags let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() @@ -110,19 +113,17 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { let currentVideoNode = self.videoNode let hasCurrentVideoNode = currentVideoNode != nil - let currentItem = self.item + let currentTheme = self.themeAndStrings?.0 - return { account, item, media, corners, automaticDownload, constrainedSize, layoutConstants in + return { [weak self] account, theme, strings, message, media, corners, automaticDownload, constrainedSize, layoutConstants in var nativeSize: CGSize var updatedTheme: PresentationTheme? - if item.theme !== currentItem?.theme { - updatedTheme = item.theme + if theme !== currentTheme { + updatedTheme = theme } - let message = item.message - let isSecretMedia = message.containsSecretMedia var secretBeginTimeAndTimeout: (Double, Double)? if isSecretMedia { @@ -143,9 +144,11 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } else if let file = media as? TelegramMediaFile, let dimensions = file.dimensions { nativeSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(constrainedSize) if file.isAnimated { - nativeSize = nativeSize.fitted(CGSize(width: 480.0, height: 480.0)) + nativeSize = nativeSize.aspectFilled(CGSize(width: 480.0, height: 480.0)) } isInlinePlayableVideo = file.isVideo && file.isAnimated + } else if let image = media as? TelegramMediaWebFile, let dimensions = image.dimensions { + nativeSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(constrainedSize) } else { nativeSize = CGSize(width: 54.0, height: 54.0) } @@ -166,7 +169,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { var secretProgressIcon: UIImage? if isSecretMedia { - secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(item.theme) + secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) } return (maxWidth, updatedCorners, { constrainedSize in @@ -210,13 +213,23 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { updateImageSignal = chatMessagePhoto(account: account, photo: image) } - updatedFetchControls = FetchControls(fetch: { [weak self] in + updatedFetchControls = FetchControls(fetch: { if let strongSelf = self { strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start()) } }, cancel: { chatMessagePhotoCancelInteractiveFetch(account: account, photo: image) }) + } else if let image = media as? TelegramMediaWebFile { + updateImageSignal = chatWebFileImage(account: account, file: image) + + updatedFetchControls = FetchControls(fetch: { + if let strongSelf = self { + strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: account, image: image).start()) + } + }, cancel: { + chatMessageWebFileCancelInteractiveFetch(account: account, image: image) + }) } else if let file = media as? TelegramMediaFile { if isSecretMedia { updateImageSignal = chatSecretMessageVideo(account: account, video: file) @@ -239,7 +252,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - updatedFetchControls = FetchControls(fetch: { [weak self] in + updatedFetchControls = FetchControls(fetch: { if let strongSelf = self { strongSelf.fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: file).start()) } @@ -281,12 +294,12 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { let imageApply = imageLayout(arguments) - return (boundingSize, { [weak self] in + return (boundingSize, { if let strongSelf = self { strongSelf.account = account strongSelf.messageIdAndFlags = (message.id, message.flags) strongSelf.media = media - strongSelf.item = item + strongSelf.themeAndStrings = (theme, strings) strongSelf.imageNode.frame = imageFrame strongSelf.progressNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) strongSelf.timeoutNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) @@ -323,7 +336,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if let secretBeginTimeAndTimeout = secretBeginTimeAndTimeout { if strongSelf.timeoutNode == nil { - let timeoutNode = RadialTimeoutNode(backgroundColor: item.theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: item.theme.chat.bubble.mediaOverlayControlForegroundColor) + let timeoutNode = RadialTimeoutNode(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor) timeoutNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) timeoutNode.position = strongSelf.imageNode.position strongSelf.timeoutNode = timeoutNode @@ -363,13 +376,13 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if progressRequired { if strongSelf.progressNode == nil { - let progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: item.theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: item.theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) + let progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) progressNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) progressNode.position = strongSelf.imageNode.position strongSelf.progressNode = progressNode strongSelf.addSubnode(progressNode) } else if let _ = updatedTheme { - strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: item.theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: item.theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) + strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) } } else { if let progressNode = strongSelf.progressNode { @@ -409,6 +422,8 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if automaticDownload { if let image = media as? TelegramMediaImage { strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start()) + } else if let image = media as? TelegramMediaWebFile { + strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: account, image: image).start()) } } } @@ -421,12 +436,12 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ message: ChatMessageItem, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() - return { account, item, media, corners, automaticDownload, constrainedSize, layoutConstants in + return { account, theme, strings, message, media, corners, automaticDownload, constrainedSize, layoutConstants in var imageNode: ChatMessageInteractiveMediaNode - var imageLayout: (_ account: Account, _ item: ChatMessageItem, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) + var imageLayout: (_ account: Account, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { imageNode = node @@ -436,7 +451,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { imageLayout = imageNode.asyncLayout() } - let (initialWidth, corners, continueLayout) = imageLayout(account, item, media, corners, automaticDownload, constrainedSize, layoutConstants) + let (initialWidth, corners, continueLayout) = imageLayout(account, theme, strings, message, media, corners, automaticDownload, constrainedSize, layoutConstants) return (initialWidth, corners, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift new file mode 100644 index 0000000000..19d4340fca --- /dev/null +++ b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift @@ -0,0 +1,118 @@ +import Foundation +import Postbox +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore + +final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { + private var item: ChatMessageItem? + private var invoice: TelegramMediaInvoice? + + private let contentNode: ChatMessageAttachedContentNode + + override var properties: ChatMessageBubbleContentProperties { + return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0) + } + + override var visibility: ListViewItemNodeVisibility { + didSet { + self.contentNode.visibility = self.visibility + } + } + + required init() { + self.contentNode = ChatMessageAttachedContentNode() + + super.init() + + self.addSubnode(self.contentNode) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + let contentNodeLayout = self.contentNode.asyncLayout() + + return { item, layoutConstants, position, constrainedSize in + var invoice: TelegramMediaInvoice? + for media in item.message.media { + if let media = media as? TelegramMediaInvoice { + invoice = media + break + } + } + + var title: String? + let subtitle: String? = nil + var text: String? + var mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? + + if let invoice = invoice { + title = invoice.title + text = invoice.description + + if let image = invoice.photo { + mediaAndFlags = (image, [.preferMediaBeforeText]) + } + } + + let (initialWidth, continueLayout) = contentNodeLayout(item.theme, item.strings, item.account, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, false, layoutConstants, position, constrainedSize) + + return (initialWidth, { constrainedSize in + let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize) + + return (refinedWidth, { boundingWidth in + let (size, apply) = finalizeLayout(boundingWidth) + + return (size, { [weak self] animation in + if let strongSelf = self { + strongSelf.invoice = invoice + + apply(animation) + + strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size) + } + }) + }) + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + + override func animateInsertionIntoBubble(_ duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { + if self.bounds.contains(point) { + /*if let webPage = self.webPage, case let .Loaded(content) = webPage.content { + if content.instantPage != nil { + return .instantPage + } + }*/ + } + return .none + } + + override func updateHiddenMedia(_ media: [Media]?) { + self.contentNode.updateHiddenMedia(media) + } + + override func transitionNode(media: Media) -> ASDisplayNode? { + return self.contentNode.transitionNode(media: media) + } +} diff --git a/TelegramUI/ChatMessageMapBubbleContentNode.swift b/TelegramUI/ChatMessageMapBubbleContentNode.swift new file mode 100644 index 0000000000..a37a81873a --- /dev/null +++ b/TelegramUI/ChatMessageMapBubbleContentNode.swift @@ -0,0 +1,295 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private let titleFont = Font.medium(14.0) +private let textFont = Font.regular(14.0) + +class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { + override var properties: ChatMessageBubbleContentProperties { + return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 5.0) + } + + private let imageNode: TransformImageNode + private let dateAndStatusNode: ChatMessageDateAndStatusNode + private let titleNode: TextNode + private let textNode: TextNode + + private var item: ChatMessageItem? + private var media: TelegramMediaMap? + + required init() { + self.imageNode = TransformImageNode() + self.dateAndStatusNode = ChatMessageDateAndStatusNode() + self.titleNode = TextNode() + self.textNode = TextNode() + + super.init() + + self.addSubnode(self.imageNode) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func didLoad() { + super.didLoad() + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:))) + self.view.addGestureRecognizer(tapRecognizer) + } + + override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + let makeImageLayout = self.imageNode.asyncLayout() + let statusLayout = self.dateAndStatusNode.asyncLayout() + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTextLayout = TextNode.asyncLayout(self.textNode) + + let previousMedia = self.media + + return { item, layoutConstants, position, constrainedSize in + var selectedMedia: TelegramMediaMap? + for media in item.message.media { + if let telegramImage = media as? TelegramMediaMap { + selectedMedia = telegramImage + } + } + + let imageCorners: ImageCorners + + var titleString: NSAttributedString? + var textString: NSAttributedString? + + let imageSize: CGSize + if let venue = selectedMedia?.venue { + imageCorners = ImageCorners(radius: 14.0) + imageSize = CGSize(width: 75.0, height: 75.0) + titleString = NSAttributedString(string: venue.title, font: titleFont, textColor: item.message.effectivelyIncoming ? item.theme.chat.bubble.incomingPrimaryTextColor : item.theme.chat.bubble.outgoingPrimaryTextColor) + if let address = venue.address, !address.isEmpty { + textString = NSAttributedString(string: address, font: textFont, textColor: item.message.effectivelyIncoming ? item.theme.chat.bubble.incomingAccentColor : item.theme.chat.bubble.outgoingAccentColor) + } + } else { + imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) + imageSize = CGSize(width: 160.0, height: 100.0) + } + + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + if let selectedMedia = selectedMedia, previousMedia == nil || !previousMedia!.isEqual(selectedMedia) { + updateImageSignal = chatMapSnapshotImage(account: item.account, resource: MapSnapshotMediaResource(latitude: selectedMedia.latitude, longitude: selectedMedia.longitude, width: 160, height: 100)) + } + + let maximumWidth: CGFloat + if let _ = titleString { + maximumWidth = CGFloat.greatestFiniteMagnitude + } else { + maximumWidth = imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right + } + + return (maximumWidth, { constrainedSize in + let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .end, CGSize(width: max(1.0, constrainedSize.width - imageSize.width - layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right - layoutConstants.text.bubbleInsets.right), height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(textString, nil, 2, .end, CGSize(width: max(1.0, constrainedSize.width - imageSize.width - layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right - layoutConstants.text.bubbleInsets.right), height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + + var t = Int(item.message.timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo) + + var edited = false + var sentViaBot = false + var viewCount: Int? + for attribute in item.message.attributes { + if let _ = attribute as? EditedMessageAttribute { + edited = true + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let _ = attribute as? InlineBotMessageAttribute { + sentViaBot = true + } + } + + var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + + if let author = item.message.author as? TelegramUser { + if author.botInfo != nil { + sentViaBot = true + } + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + dateText = "\(author.displayTitle), \(dateText)" + } + } + + let statusType: ChatMessageDateAndStatusType? + if case .None = position.bottom { + if let _ = titleString { + if item.message.effectivelyIncoming { + statusType = .BubbleIncoming + } else { + if item.message.flags.contains(.Failed) { + statusType = .BubbleOutgoing(.Failed) + } else if item.message.flags.isSending { + statusType = .BubbleOutgoing(.Sending) + } else { + statusType = .BubbleOutgoing(.Sent(read: item.read)) + } + } + } else { + if item.message.effectivelyIncoming { + statusType = .ImageIncoming + } else { + if item.message.flags.contains(.Failed) { + statusType = .ImageOutgoing(.Failed) + } else if item.message.flags.isSending { + statusType = .ImageOutgoing(.Sending) + } else { + statusType = .ImageOutgoing(.Sent(read: item.read)) + } + } + } + } else { + statusType = nil + } + + var statusSize = CGSize() + var statusApply: ((Bool) -> Void)? + + if let statusType = statusType { + let (size, apply) = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude)) + statusSize = size + statusApply = apply + } + + let contentWidth: CGFloat + if let _ = titleString { + contentWidth = imageSize.width + max(statusSize.width, max(titleLayout.size.width, textLayout.size.width)) + layoutConstants.text.bubbleInsets.right + 8.0 + + } else { + contentWidth = imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right + } + + return (contentWidth, { boundingWidth in + let arguments = TransformImageArguments(corners: imageCorners, imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()) + + let imageLayoutSize = CGSize(width: imageSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, height: imageSize.height + layoutConstants.image.bubbleInsets.top + layoutConstants.image.bubbleInsets.bottom) + + let layoutSize: CGSize + let statusFrame: CGRect + + let baseImageFrame = CGRect(origin: CGPoint(x: -arguments.insets.left, y: -arguments.insets.top), size: arguments.drawingSize) + + let imageFrame: CGRect + + if let _ = titleString { + layoutSize = CGSize(width: contentWidth, height: imageLayoutSize.height + 10.0) + statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - layoutConstants.text.bubbleInsets.right, y: layoutSize.height - statusSize.height - 5.0 - 4.0), size: statusSize) + imageFrame = baseImageFrame.offsetBy(dx: 5.0, dy: 5.0) + } else { + layoutSize = CGSize(width: max(imageLayoutSize.width, statusSize.width + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right + layoutConstants.image.statusInsets.left + layoutConstants.image.statusInsets.right), height: imageLayoutSize.height) + statusFrame = CGRect(origin: CGPoint(x: layoutSize.width - layoutConstants.image.bubbleInsets.right - layoutConstants.image.statusInsets.right - statusSize.width, y: layoutSize.height - layoutConstants.image.bubbleInsets.bottom - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize) + imageFrame = baseImageFrame.offsetBy(dx: layoutConstants.image.bubbleInsets.left, dy: layoutConstants.image.bubbleInsets.top) + } + + let imageApply = makeImageLayout(arguments) + + return (layoutSize, { [weak self] animation in + if let strongSelf = self { + strongSelf.item = item + strongSelf.media = selectedMedia + + strongSelf.imageNode.frame = imageFrame + + let _ = titleApply() + let _ = textApply() + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX + 7.0, y: imageFrame.minY + 1.0), size: titleLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX + 7.0, y: imageFrame.minY + 19.0), size: textLayout.size) + + if let statusApply = statusApply { + if strongSelf.dateAndStatusNode.supernode == nil { + strongSelf.imageNode.addSubnode(strongSelf.dateAndStatusNode) + } + var hasAnimation = true + if case .None = animation { + hasAnimation = false + } + statusApply(hasAnimation) + strongSelf.dateAndStatusNode.frame = statusFrame + } else if strongSelf.dateAndStatusNode.supernode != nil { + strongSelf.dateAndStatusNode.removeFromSupernode() + } + + if let _ = titleString { + if strongSelf.titleNode.supernode == nil { + strongSelf.addSubnode(strongSelf.titleNode) + } + if strongSelf.textNode.supernode == nil { + strongSelf.addSubnode(strongSelf.textNode) + } + } else { + if strongSelf.titleNode.supernode != nil { + strongSelf.titleNode.removeFromSupernode() + } + if strongSelf.textNode.supernode != nil { + strongSelf.textNode.removeFromSupernode() + } + } + + if let updateImageSignal = updateImageSignal { + strongSelf.imageNode.setSignal(account: item.account, signal: updateImageSignal) + } + + imageApply() + } + }) + }) + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + 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.2, removeOnCompletion: false) + } + + override func transitionNode(media: Media) -> ASDisplayNode? { + if let currentMedia = self.media, currentMedia.isEqual(media) { + return self.imageNode + } + return nil + } + + override func updateHiddenMedia(_ media: [Media]?) { + var mediaHidden = false + if let currentMedia = self.media, let media = media { + for item in media { + if item.isEqual(currentMedia) { + mediaHidden = true + break + } + } + } + + self.imageNode.isHidden = mediaHidden + } + + override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { + return .none + } + + @objc func imageTap(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let item = self.item { + self.controllerInteraction?.openMessage(item.message.id) + } + } + } +} diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index a1caf427f9..6e91fc43e9 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -59,7 +59,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { let initialImageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) - let (initialWidth, _, refineLayout) = interactiveImageLayout(item.account, item, selectedMedia!, initialImageCorners, item.account.settings.automaticDownloadSettingsForPeerId(item.peerId).downloadPhotos, CGSize(width: constrainedSize.width, height: constrainedSize.height), layoutConstants) + let (initialWidth, _, refineLayout) = interactiveImageLayout(item.account, item.theme, item.strings, item.message, selectedMedia!, initialImageCorners, item.account.settings.automaticDownloadSettingsForPeerId(item.peerId).downloadPhotos, CGSize(width: constrainedSize.width, height: constrainedSize.height), layoutConstants) return (initialWidth + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, { constrainedSize in let (refinedWidth, finishLayout) = refineLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index 779ea0d195..b916fd4c5f 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -5,46 +5,28 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramCore -private let titleFont: UIFont = UIFont.boldSystemFont(ofSize: 15.0) -private let textFont: UIFont = UIFont.systemFont(ofSize: 15.0) - final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { - private let lineNode: ASImageNode - private let textNode: TextNode - private let inlineImageNode: TransformImageNode - private var contentImageNode: ChatMessageInteractiveMediaNode? - private var contentFileNode: ChatMessageInteractiveFileNode? - - private let statusNode: ChatMessageDateAndStatusNode - private var item: ChatMessageItem? private var webPage: TelegramMediaWebpage? - private var image: TelegramMediaImage? + + private let contentNode: ChatMessageAttachedContentNode + + override var properties: ChatMessageBubbleContentProperties { + return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0) + } + + override var visibility: ListViewItemNodeVisibility { + didSet { + self.contentNode.visibility = self.visibility + } + } required init() { - self.lineNode = ASImageNode() - self.lineNode.isLayerBacked = true - self.lineNode.displaysAsynchronously = false - self.lineNode.displayWithoutProcessing = true - - self.textNode = TextNode() - self.textNode.isLayerBacked = true - self.textNode.displaysAsynchronously = true - self.textNode.contentsScale = UIScreenScale - self.textNode.contentMode = .topLeft - - self.inlineImageNode = TransformImageNode() - self.inlineImageNode.isLayerBacked = true - self.inlineImageNode.displaysAsynchronously = false - - self.statusNode = ChatMessageDateAndStatusNode() + self.contentNode = ChatMessageAttachedContentNode() super.init() - self.addSubnode(self.lineNode) - self.addSubnode(self.textNode) - - self.addSubnode(self.statusNode) + self.addSubnode(self.contentNode) } required init?(coder aDecoder: NSCoder) { @@ -52,18 +34,9 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { - let textAsyncLayout = TextNode.asyncLayout(self.textNode) - let currentImage = self.image - let imageLayout = self.inlineImageNode.asyncLayout() - let statusLayout = self.statusNode.asyncLayout() - let contentImageLayout = ChatMessageInteractiveMediaNode.asyncLayout(self.contentImageNode) - let contentFileLayout = ChatMessageInteractiveFileNode.asyncLayout(self.contentFileNode) + let contentNodeLayout = self.contentNode.asyncLayout() - return { item, layoutConstants, _, constrainedSize in - let insets = UIEdgeInsets(top: 0.0, left: 9.0 + 8.0, bottom: 5.0, right: 8.0) - - let incoming = item.message.effectivelyIncoming - + return { item, layoutConstants, position, constrainedSize in var webPage: TelegramMediaWebpage? var webPageContent: TelegramMediaWebpageLoadedContent? for media in item.message.media { @@ -76,328 +49,54 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } - var t = Int(item.message.timestamp) - var timeinfo = tm() - localtime_r(&t, &timeinfo) - - var edited = false - var sentViaBot = false - var viewCount: Int? - for attribute in item.message.attributes { - if let _ = attribute as? EditedMessageAttribute { - edited = true - } else if let attribute = attribute as? ViewCountMessageAttribute { - viewCount = attribute.count - } else if let _ = attribute as? InlineBotMessageAttribute { - sentViaBot = true - } - } - - var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) - - if let author = item.message.author as? TelegramUser { - if author.botInfo != nil { - sentViaBot = true - } - if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - dateText = "\(author.displayTitle), \(dateText)" - } - } - - var textString: NSAttributedString? - var inlineImageDimensions: CGSize? - var inlineImageSize: CGSize? - var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - var textCutout: TextNodeCutout? - var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude - var refineContentImageLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode)))? - var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode)))? + var title: String? + var subtitle: String? + var text: String? + var mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? if let webpage = webPageContent { - let string = NSMutableAttributedString() - var notEmpty = false - - let bubbleTheme = item.theme.chat.bubble - if let websiteName = webpage.websiteName, !websiteName.isEmpty { - string.append(NSAttributedString(string: websiteName, font: titleFont, textColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor)) - notEmpty = true + title = websiteName } if let title = webpage.title, !title.isEmpty { - if notEmpty { - string.append(NSAttributedString(string: "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) - } - string.append(NSAttributedString(string: title, font: titleFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) - notEmpty = true + subtitle = title } - if let text = webpage.text, !text.isEmpty { - if notEmpty { - string.append(NSAttributedString(string: "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) - } - string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) - notEmpty = true + if let textValue = webpage.text, !textValue.isEmpty { + text = textValue } - textString = string - if let file = webpage.file { - if file.isVideo { - let (initialImageWidth, _, refineLayout) = contentImageLayout(item.account, item, file, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) - initialWidth = initialImageWidth + insets.left + insets.right - refineContentImageLayout = refineLayout - } else { - var automaticDownload = false - if file.isVoice { - automaticDownload = true - } - let (_, refineLayout) = contentFileLayout(item.account, item, file, automaticDownload, item.message.effectivelyIncoming, nil, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) - refineContentFileLayout = refineLayout - } + mediaAndFlags = (file, []) } else if let image = webpage.image { if let type = webpage.type, ["photo"].contains(type) { - let (initialImageWidth, _, refineLayout) = contentImageLayout(item.account, item, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) - initialWidth = initialImageWidth + insets.left + insets.right - refineContentImageLayout = refineLayout - } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { - inlineImageDimensions = dimensions - - if image != currentImage { - updateInlineImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: image) + var flags = ChatMessageAttachedContentNodeMediaFlags() + if webpage.instantPage != nil { + flags.insert(.preferMediaBeforeText) } + mediaAndFlags = (image, flags) + } else if let _ = largestImageRepresentation(image.representations)?.dimensions { + mediaAndFlags = (image, [.preferMediaInline]) } } } - if let _ = inlineImageDimensions { - inlineImageSize = CGSize(width: 54.0, height: 54.0) - - if let inlineImageSize = inlineImageSize { - textCutout = TextNodeCutout(position: .TopRight, size: CGSize(width: inlineImageSize.width + 10.0, height: inlineImageSize.height + 10.0)) - } - } + let (initialWidth, continueLayout) = contentNodeLayout(item.theme, item.strings, item.account, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, true, layoutConstants, position, constrainedSize) return (initialWidth, { constrainedSize in - let statusType: ChatMessageDateAndStatusType - if item.message.effectivelyIncoming { - statusType = .BubbleIncoming - } else { - if item.message.flags.contains(.Failed) { - statusType = .BubbleOutgoing(.Failed) - } else if item.message.flags.isSending { - statusType = .BubbleOutgoing(.Sending) - } else { - statusType = .BubbleOutgoing(.Sent(read: item.read)) - } - } + let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize) - let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom) - - var statusSizeAndApply: (CGSize, (Bool) -> Void)? - - if refineContentImageLayout == nil && refineContentFileLayout == nil { - statusSizeAndApply = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) - } - - let (textLayout, textApply) = textAsyncLayout(textString, nil, 12, .end, textConstrainedSize, .natural, textCutout, UIEdgeInsets()) - - var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) - - var statusFrame: CGRect? - - if let (statusSize, _) = statusSizeAndApply { - var frame = CGRect(origin: CGPoint(), size: statusSize) + return (refinedWidth, { boundingWidth in + let (size, apply) = finalizeLayout(boundingWidth) - let trailingLineWidth = textLayout.trailingLineWidth - if textLayout.size.width - trailingLineWidth >= statusSize.width { - frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY - statusSize.height) - } else if trailingLineWidth + statusSize.width < textConstrainedSize.width { - frame.origin = CGPoint(x: textFrame.minX + trailingLineWidth, y: textFrame.maxY - statusSize.height) - } else { - frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY) - } - - if let inlineImageSize = inlineImageSize { - if frame.origin.y < inlineImageSize.height + 4.0 { - frame.origin.y = inlineImageSize.height + 4.0 - } - } - - frame = frame.offsetBy(dx: insets.left, dy: insets.top) - statusFrame = frame - } - - textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top) - - let lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(item.theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(item.theme) - - var boundingSize = textFrame.size - if let statusFrame = statusFrame { - boundingSize = textFrame.union(statusFrame).size - } - var lineHeight = textFrame.size.height - if let inlineImageSize = inlineImageSize { - if boundingSize.height < inlineImageSize.height { - boundingSize.height = inlineImageSize.height - } - if lineHeight < inlineImageSize.height { - lineHeight = inlineImageSize.height - } - } - - var finalizeContentImageLayout: ((CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))? - if let refineContentImageLayout = refineContentImageLayout { - let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize) - finalizeContentImageLayout = finalizeImageLayout - - boundingSize.width = max(boundingSize.width, refinedWidth) - } - var finalizeContentFileLayout: ((CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))? - if let refineContentFileLayout = refineContentFileLayout { - let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize) - finalizeContentFileLayout = finalizeFileLayout - - boundingSize.width = max(boundingSize.width, refinedWidth) - } - - boundingSize.width += insets.left + insets.right - boundingSize.height += insets.top + insets.bottom - lineHeight += insets.top + insets.bottom - - var imageApply: (() -> Void)? - if let inlineImageSize = inlineImageSize, let inlineImageDimensions = inlineImageDimensions { - let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) - let arguments = TransformImageArguments(corners: imageCorners, imageSize: inlineImageDimensions.aspectFilled(inlineImageSize), boundingSize: inlineImageSize, intrinsicInsets: UIEdgeInsets()) - imageApply = imageLayout(arguments) - } - - return (boundingSize.width, { boundingWidth in - var adjustedBoundingSize = boundingSize - var adjustedLineHeight = lineHeight - - var imageFrame: CGRect? - if let inlineImageSize = inlineImageSize { - imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right, y: 0.0), size: inlineImageSize) - } - - var contentImageSizeAndApply: (CGSize, () -> ChatMessageInteractiveMediaNode)? - if let finalizeContentImageLayout = finalizeContentImageLayout { - let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right) - contentImageSizeAndApply = (size, apply) - - var imageHeigthAddition = size.height - if textFrame.size.height > CGFloat.ulpOfOne { - imageHeigthAddition += 2.0 - } - - adjustedBoundingSize.height += imageHeigthAddition + 5.0 - adjustedLineHeight += imageHeigthAddition + 4.0 - } - - var contentFileSizeAndApply: (CGSize, () -> ChatMessageInteractiveFileNode)? - if let finalizeContentFileLayout = finalizeContentFileLayout { - let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right) - contentFileSizeAndApply = (size, apply) - - var imageHeigthAddition = size.height - if textFrame.size.height > CGFloat.ulpOfOne { - imageHeigthAddition += 2.0 - } - - adjustedBoundingSize.height += imageHeigthAddition + 5.0 - adjustedLineHeight += imageHeigthAddition + 4.0 - } - - if let _ = webPageContent?.instantPage { - adjustedBoundingSize.height += 4.0 - } - - var adjustedStatusFrame: CGRect? - if let statusFrame = statusFrame { - adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - insets.right, y: statusFrame.origin.y), size: statusFrame.size) - } - - return (adjustedBoundingSize, { [weak self] animation in + return (size, { [weak self] animation in if let strongSelf = self { - strongSelf.item = item - - var hasAnimation = true - if case .None = animation { - hasAnimation = false - } - - strongSelf.lineNode.image = lineImage - strongSelf.lineNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 0.0), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)) - - let _ = textApply() - strongSelf.textNode.frame = textFrame - - if let (_, statusApply) = statusSizeAndApply, let adjustedStatusFrame = adjustedStatusFrame { - strongSelf.statusNode.frame = adjustedStatusFrame - if strongSelf.statusNode.supernode == nil { - strongSelf.addSubnode(strongSelf.statusNode) - } - statusApply(hasAnimation) - } else if strongSelf.statusNode.supernode != nil { - strongSelf.statusNode.removeFromSupernode() - } - strongSelf.webPage = webPage - strongSelf.image = webPageContent?.image - if let imageFrame = imageFrame { - if let updateImageSignal = updateInlineImageSignal { - strongSelf.inlineImageNode.setSignal(account: item.account, signal: updateImageSignal) - } - - strongSelf.inlineImageNode.frame = imageFrame - if strongSelf.inlineImageNode.supernode == nil { - strongSelf.addSubnode(strongSelf.inlineImageNode) - } - - if let imageApply = imageApply { - imageApply() - } - } else if strongSelf.inlineImageNode.supernode != nil { - strongSelf.inlineImageNode.removeFromSupernode() - } + apply(animation) - if let (contentImageSize, contentImageApply) = contentImageSizeAndApply { - let contentImageNode = contentImageApply() - if strongSelf.contentImageNode !== contentImageNode { - strongSelf.contentImageNode = contentImageNode - strongSelf.addSubnode(contentImageNode) - contentImageNode.activateLocalContent = { [weak strongSelf] in - if let strongSelf = strongSelf, let item = strongSelf.item { - strongSelf.controllerInteraction?.openMessage(item.message.id) - } - } - } - let _ = contentImageApply() - contentImageNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentImageSize) - } else if let contentImageNode = strongSelf.contentImageNode { - contentImageNode.removeFromSupernode() - strongSelf.contentImageNode = nil - } - - if let (contentFileSize, contentFileApply) = contentFileSizeAndApply { - let contentFileNode = contentFileApply() - if strongSelf.contentFileNode !== contentFileNode { - strongSelf.contentFileNode = contentFileNode - strongSelf.addSubnode(contentFileNode) - contentFileNode.activateLocalContent = { [weak strongSelf] in - if let strongSelf = strongSelf, let item = strongSelf.item { - strongSelf.controllerInteraction?.openMessage(item.message.id) - } - } - } - let _ = contentFileApply() - contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentFileSize) - } else if let contentFileNode = strongSelf.contentFileNode { - contentFileNode.removeFromSupernode() - strongSelf.contentFileNode = nil - } + strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size) } }) }) @@ -406,31 +105,19 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - self.lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.inlineImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } override func animateAdded(_ currentTimestamp: Double, duration: Double) { - self.lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.inlineImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.lineNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.inlineImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) } override func animateInsertionIntoBubble(_ duration: Double) { - self.lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.inlineImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { @@ -445,44 +132,10 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } override func updateHiddenMedia(_ media: [Media]?) { - var currentMedia: Media? - if let webPage = self.webPage { - if case let .Loaded(content) = webPage.content { - if let image = content.image { - currentMedia = image - } else if let file = content.file { - currentMedia = file - } - } - } - if let currentMedia = currentMedia { - if let media = media { - var found = false - for m in media { - if currentMedia.isEqual(m) { - found = true - break - } - } - if let contentImageNode = self.contentImageNode { - contentImageNode.isHidden = found - } - } else if let contentImageNode = self.contentImageNode { - contentImageNode.isHidden = false - } - } + self.contentNode.updateHiddenMedia(media) } override func transitionNode(media: Media) -> ASDisplayNode? { - if let webPage = self.webPage { - if case let .Loaded(content) = webPage.content { - if let image = content.image, image.isEqual(media) { - return self.contentImageNode - } else if let file = content.file, file.isEqual(media) { - return self.contentImageNode - } - } - } - return nil + return self.contentNode.transitionNode(media: media) } } diff --git a/TelegramUI/ChatReportPeerTitlePanelNode.swift b/TelegramUI/ChatReportPeerTitlePanelNode.swift index 3d02eaacf4..01f5e7e706 100644 --- a/TelegramUI/ChatReportPeerTitlePanelNode.swift +++ b/TelegramUI/ChatReportPeerTitlePanelNode.swift @@ -4,27 +4,13 @@ import AsyncDisplayKit import Postbox import TelegramCore -private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(rgb: 0x9099A2).cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - context.move(to: CGPoint(x: 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) - context.strokePath() - context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) - context.strokePath() -}) - - private enum ChatReportPeerTitleButton { case reportSpam - var title: String { + func title(strings: PresentationStrings) -> String { switch self { case .reportSpam: - return "Report spam" + return strings.ReportPeer_ReasonSpam } } } @@ -39,20 +25,18 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { private let closeButton: HighlightableButtonNode private var buttons: [(ChatReportPeerTitleButton, UIButton)] = [] + private var theme: PresentationTheme? + override init() { self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) self.separatorNode.isLayerBacked = true self.closeButton = HighlightableButtonNode() - self.closeButton.setImage(closeButtonImage, for: []) self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.closeButton.displaysAsynchronously = false super.init() - self.backgroundColor = UIColor(rgb: 0xF5F6F8) - self.addSubnode(self.separatorNode) self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) @@ -60,6 +44,14 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { } override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + if interfaceState.theme !== self.theme { + self.theme = interfaceState.theme + + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(interfaceState.theme), for: []) + self.backgroundColor = interfaceState.theme.rootController.navigationBar.backgroundColor + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor + } + let panelHeight: CGFloat = 40.0 let rightInset: CGFloat = 18.0 @@ -93,10 +85,10 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.buttons.removeAll() for button in updatedButtons { let view = UIButton() - view.setTitle(button.title, for: []) + view.setTitle(button.title(strings: interfaceState.strings), for: []) view.titleLabel?.font = Font.regular(16.0) - view.setTitleColor(UIColor(rgb: 0x007ee5), for: []) - view.setTitleColor(UIColor(rgb: 0x007ee5).withAlphaComponent(0.7), for: [.highlighted]) + view.setTitleColor(interfaceState.theme.rootController.navigationBar.accentTextColor, for: []) + view.setTitleColor(interfaceState.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.7), for: [.highlighted]) view.addTarget(self, action: #selector(self.buttonPressed(_:)), for: [.touchUpInside]) self.view.addSubview(view) self.buttons.append((button, view)) diff --git a/TelegramUI/ChatRequestInProgressTitlePanelNode.swift b/TelegramUI/ChatRequestInProgressTitlePanelNode.swift index 328e7c0e5e..c477ab4007 100644 --- a/TelegramUI/ChatRequestInProgressTitlePanelNode.swift +++ b/TelegramUI/ChatRequestInProgressTitlePanelNode.swift @@ -6,13 +6,14 @@ final class ChatRequestInProgressTitlePanelNode: ChatTitleAccessoryPanelNode { private let separatorNode: ASDisplayNode private let titleNode: ASTextNode + private var theme: PresentationTheme? + private var strings: PresentationStrings? + override init() { self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) self.separatorNode.isLayerBacked = true self.titleNode = ASTextNode() - self.titleNode.attributedText = NSAttributedString(string: "Loading...", font: Font.regular(14.0), textColor: UIColor.black) self.titleNode.maximumNumberOfLines = 1 super.init() @@ -24,6 +25,19 @@ final class ChatRequestInProgressTitlePanelNode: ChatTitleAccessoryPanelNode { } override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + if interfaceState.strings !== self.strings { + self.strings = interfaceState.strings + + self.titleNode.attributedText = NSAttributedString(string: interfaceState.strings.Channel_NotificationLoading, font: Font.regular(14.0), textColor: UIColor.black) + } + + if interfaceState.theme !== self.theme { + self.theme = interfaceState.theme + + self.backgroundColor = interfaceState.theme.rootController.navigationBar.backgroundColor + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor + } + let panelHeight: CGFloat = 40.0 let titleSize = self.titleNode.measure(CGSize(width: width, height: 100.0)) diff --git a/TelegramUI/ComposeControllerNode.swift b/TelegramUI/ComposeControllerNode.swift index 121ec55ef8..e9f18b3ded 100644 --- a/TelegramUI/ComposeControllerNode.swift +++ b/TelegramUI/ComposeControllerNode.swift @@ -124,9 +124,7 @@ final class ComposeControllerNode: ASDisplayNode { requestOpenPeerFromSearch(peerId) } }), cancel: { [weak self] in - if let requestDeactivateSearch = self?.requestDeactivateSearch { - requestDeactivateSearch() - } + self?.requestDeactivateSearch?() }) self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index a76d647d44..aada66e8b0 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -208,7 +208,7 @@ private extension PeerIndexNameRepresentation { switch other { case let .title(title, _): return lhsTitle.compare(title) - case let .personName(_, last, _): + case let .personName(_, last, _, _): let lastResult = lhsTitle.compare(last) if lastResult == .orderedSame { return .orderedAscending @@ -216,7 +216,7 @@ private extension PeerIndexNameRepresentation { return lastResult } } - case let .personName(lhsFirst, lhsLast, _): + case let .personName(lhsFirst, lhsLast, _, _): switch other { case let .title(title, _): let lastResult = lhsFirst.compare(title) @@ -225,7 +225,7 @@ private extension PeerIndexNameRepresentation { } else { return lastResult } - case let .personName(first, last, _): + case let .personName(first, last, _, _): let lastResult = lhsLast.compare(last) if lastResult == .orderedSame { return lhsFirst.compare(first) @@ -284,7 +284,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences if let c = title.utf16.first { indexHeader = c } - case let .personName(first, last, _): + case let .personName(first, last, _, _): if let c = last.utf16.first { indexHeader = c } else if let c = first.utf16.first { @@ -317,7 +317,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences if title.isEmpty { removeIndices.append(i) } - case let .personName(first, last, _): + case let .personName(first, last, _, _): if first.isEmpty || last.isEmpty { removeIndices.append(i) } diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index 97e546fb16..e579891af9 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -297,19 +297,25 @@ class ContactsPeerItemNode: ListViewItemNode { var userPresence: TelegramUserPresence? if let peer = item.peer { + let textColor: UIColor + if let _ = item.chatPeer as? TelegramSecretChat { + textColor = item.theme.chatList.secretTitleColor + } else { + textColor = item.theme.list.itemPrimaryTextColor + } if let user = peer as? TelegramUser { if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { let string = NSMutableAttributedString() - string.append(NSAttributedString(string: firstName, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)) - string.append(NSAttributedString(string: " ", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)) - string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor)) + string.append(NSAttributedString(string: firstName, font: titleFont, textColor: textColor)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: textColor)) + string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: textColor)) titleAttributedString = string } else if let firstName = user.firstName, !firstName.isEmpty { - titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) + titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: textColor) } else if let lastName = user.lastName, !lastName.isEmpty { - titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) + titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: textColor) } else { - titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleBoldFont, textColor: item.theme.list.itemSecondaryTextColor) + titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleBoldFont, textColor: textColor) } } else if let group = peer as? TelegramGroup { titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) diff --git a/TelegramUI/GameController.swift b/TelegramUI/GameController.swift new file mode 100644 index 0000000000..c145679071 --- /dev/null +++ b/TelegramUI/GameController.swift @@ -0,0 +1,93 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import Postbox + +final class GameController: ViewController { + private var controllerNode: GameControllerNode { + return self.displayNode as! GameControllerNode + } + + private let account: Account + private let url: String + private let message: Message + + private var presentationData: PresentationData + + private var didPlayPresentationAnimation = false + + init(account: Account, url: String, message: Message) { + self.account = account + self.url = url + self.message = message + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.closePressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.sharePressed)) + + for media in message.media { + if let game = media as? TelegramMediaGame { + let titleView = GameControllerTitleView(theme: self.presentationData.theme) + + var botPeer: Peer? + inner: for attribute in message.attributes { + if let attribute = attribute as? InlineBotMessageAttribute { + botPeer = message.peers[attribute.peerId] + break inner + } + } + if botPeer == nil { + botPeer = message.author + } + + titleView.set(title: game.title, subtitle: "@\(botPeer?.addressName ?? "")") + self.navigationItem.titleView = titleView + } + } + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func closePressed() { + self.dismiss() + } + + @objc func sharePressed() { + + } + + override func loadDisplayNode() { + self.displayNode = GameControllerNode(presentationData: self.presentationData, url: self.url) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.didPlayPresentationAnimation { + self.didPlayPresentationAnimation = true + self.controllerNode.animateIn() + } + } + + override func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } +} diff --git a/TelegramUI/GameControllerNode.swift b/TelegramUI/GameControllerNode.swift new file mode 100644 index 0000000000..0a2d7e9857 --- /dev/null +++ b/TelegramUI/GameControllerNode.swift @@ -0,0 +1,38 @@ +import Foundation +import Display +import AsyncDisplayKit +import WebKit + +final class GameControllerNode: ViewControllerTracingNode { + private let webView: WKWebView + + var presentationData: PresentationData + + init(presentationData: PresentationData, url: String) { + self.presentationData = presentationData + + self.webView = WKWebView() + + super.init() + + self.view.addSubview(self.webView) + + if let parsedUrl = URL(string: url) { + self.webView.load(URLRequest(url: parsedUrl)) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.webView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight))) + } + + func animateIn() { + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + + func animateOut(completion: (() -> Void)? = nil) { + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { [weak self] _ in + completion?() + }) + } +} diff --git a/TelegramUI/GameControllerTitleView.swift b/TelegramUI/GameControllerTitleView.swift new file mode 100644 index 0000000000..7fcf3f3fbe --- /dev/null +++ b/TelegramUI/GameControllerTitleView.swift @@ -0,0 +1,70 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit +import TelegramLegacyComponents + +final class GameControllerTitleView: UIView { + private var theme: PresentationTheme + + private let titleNode: ASTextNode + private let infoNode: ASTextNode + + init(theme: PresentationTheme) { + self.theme = theme + + self.titleNode = ASTextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationMode = .byTruncatingTail + self.titleNode.isOpaque = false + + self.infoNode = ASTextNode() + self.infoNode.displaysAsynchronously = false + self.infoNode.maximumNumberOfLines = 1 + self.infoNode.truncationMode = .byTruncatingTail + self.infoNode.isOpaque = false + + super.init(frame: CGRect()) + + self.addSubnode(self.titleNode) + self.addSubnode(self.infoNode) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func set(title: String, subtitle: String) { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + self.infoNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + } + + override func layoutSubviews() { + super.layoutSubviews() + + let size = self.bounds.size + + if size.height > 40.0 { + let titleSize = self.titleNode.measure(size) + let infoSize = self.infoNode.measure(size) + let titleInfoSpacing: CGFloat = 0.0 + + let combinedHeight = titleSize.height + infoSize.height + titleInfoSpacing + + self.titleNode.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) + self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - infoSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: infoSize) + } else { + let titleSize = self.titleNode.measure(CGSize(width: floor(size.width / 2.0), height: size.height)) + let infoSize = self.infoNode.measure(CGSize(width: floor(size.width / 2.0), height: size.height)) + + let titleInfoSpacing: CGFloat = 8.0 + let combinedWidth = titleSize.width + infoSize.width + titleInfoSpacing + + self.titleNode.frame = CGRect(origin: CGPoint(x: floor((size.width - combinedWidth) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) + self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - combinedWidth) / 2.0 + titleSize.width + titleInfoSpacing), y: floor((size.height - infoSize.height) / 2.0)), size: infoSize) + } + } +} diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index ce9e3a6288..ac3b38de7c 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -5,8 +5,6 @@ import Postbox import TelegramCore import TelegramLegacyComponents -private let addMemberPlusIcon = UIImage(bundleImageName: "Peer Info/PeerItemPlusIcon")?.precomposed() - private final class GroupInfoArguments { let account: Account let peerId: PeerId @@ -1148,6 +1146,8 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl |> mapToSignal { peer -> Signal in let result = ValuePromise() + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: "Remove \(peer.displayTitle)?", color: .destructive, action: { [weak actionSheet] in @@ -1156,7 +1156,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl result.set(true) }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -1220,7 +1220,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl if state.savingData { rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: doneEnabled, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: doneEnabled, action: { var updateValues: (title: String?, description: String?) = (nil, nil) updateState { state in updateValues = valuesRequiringUpdate(state: state, view: view) @@ -1261,7 +1261,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl }) } } else { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { if let peer = peer as? TelegramGroup { updateState { state in return state.withUpdatedEditingState(GroupInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(peer.indexName), editingDescriptionText: "")) @@ -1278,7 +1278,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl }) } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Info"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back")) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.GroupInfo_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) let listState = ItemListNodeState(entries: groupInfoEntries(account: account, presentationData: presentationData, view: view, state: state), style: .blocks) return (controllerState, (listState, arguments)) diff --git a/TelegramUI/InstantPageLayout.swift b/TelegramUI/InstantPageLayout.swift index b32a99291c..aa6763da1f 100644 --- a/TelegramUI/InstantPageLayout.swift +++ b/TelegramUI/InstantPageLayout.swift @@ -128,7 +128,7 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h } else { return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) } - case let .webEmbed(url, html, dimensions, caption, stretchToWidth, allowScrolling): + case let .webEmbed(url, html, dimensions, caption, stretchToWidth, allowScrolling, coverId): var embedBoundingWidth = boundingWidth - horizontalInset * 2.0 if stretchToWidth { embedBoundingWidth = boundingWidth diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index fafb475172..20cdb70c97 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -13,7 +13,7 @@ enum ItemListAvatarAndNameInfoItemName: Equatable { init(_ name: PeerIndexNameRepresentation) { switch name { - case let .personName(first, last, _): + case let .personName(first, last, _, _): self = .personName(firstName: first, lastName: last) case let .title(title, _): self = .title(title: title) diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index ccb7674249..f4382ed417 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -323,9 +323,9 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.theme.list.itemSecondaryTextColor) } - let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - labelLayout.size.width - editingOffset - rightInset - labelInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 12.0 - labelLayout.size.width - editingOffset - rightInset - labelInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - (labelLayout.size.width > 0.0 ? (labelLayout.size.width) + 15.0 : 0.0) - editingOffset - rightInset - labelInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let insets = itemListNeighborsGroupedInsets(neighbors) diff --git a/TelegramUI/ItemListSwitchItem.swift b/TelegramUI/ItemListSwitchItem.swift index 5347c9766d..2f787028a5 100644 --- a/TelegramUI/ItemListSwitchItem.swift +++ b/TelegramUI/ItemListSwitchItem.swift @@ -128,7 +128,7 @@ class ItemListSwitchItemNode: ListViewItemNode { insets = itemListNeighborsGroupedInsets(neighbors) } - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 80, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) if !item.enabled { if currentDisabledOverlayNode == nil { diff --git a/TelegramUI/LegacyControllerNode.swift b/TelegramUI/LegacyControllerNode.swift index d322de6b09..9d93f6cf67 100644 --- a/TelegramUI/LegacyControllerNode.swift +++ b/TelegramUI/LegacyControllerNode.swift @@ -17,6 +17,8 @@ final class LegacyControllerNode: ASDisplayNode { super.init(viewBlock: { return UITracingLayerView() }, didLoad: nil) + + self.clipsToBounds = true } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -33,7 +35,7 @@ final class LegacyControllerNode: ASDisplayNode { } func animateModalOut(completion: @escaping () -> Void) { - self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: 0.0, y: self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, additive: true, completion: { _ in + self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, additive: true, completion: { _ in completion() }) } diff --git a/TelegramUI/LegacyLocationController.swift b/TelegramUI/LegacyLocationController.swift new file mode 100644 index 0000000000..ecfaff3751 --- /dev/null +++ b/TelegramUI/LegacyLocationController.swift @@ -0,0 +1,58 @@ +import Foundation +import Display +import TelegramLegacyComponents +import TelegramCore +import Postbox + +func legacyLocationController(message: Message, mapMedia: TelegramMediaMap, account: Account, openPeer: @escaping (Peer) -> Void) -> ViewController { + var legacyPeer: AnyObject? + if let user = message.author as? TelegramUser { + let legacyUser = TGUser() + legacyUser.uid = user.id.id + legacyUser.firstName = user.firstName + legacyUser.lastName = user.lastName + legacyPeer = legacyUser + } else if let channel = message.author as? TelegramChannel { + let legacyConversation = TGConversation() + legacyConversation.conversationId = Int64(channel.id.id) + legacyConversation.chatTitle = channel.title + legacyPeer = legacyConversation + } + let legacyLocation = TGLocationMediaAttachment() + legacyLocation.latitude = mapMedia.latitude + legacyLocation.longitude = mapMedia.longitude + if let venue = mapMedia.venue { + legacyLocation.venue = TGVenueAttachment(title: venue.title, address: venue.address, provider: venue.provider, venueId: venue.id) + } + + let controller = TGLocationViewController(locationAttachment: legacyLocation, peer: legacyPeer)! + let navigationController = TGNavigationController(controllers: [controller])! + let legacyController = LegacyController(legacyController: navigationController, presentation: .modal(animateIn: true)) + controller.customDismiss = { [weak legacyController] in + legacyController?.dismiss() + } + controller.customActions = { [weak legacyController] in + if let legacyController = legacyController { + var shareAction: (([PeerId]) -> Void)? + let shareController = ShareController(account: account, shareAction: { peerIds in + shareAction?(peerIds) + }, defaultAction: nil) + legacyController.present(shareController, in: .window) + shareAction = { [weak shareController] peerIds in + shareController?.dismiss() + + for peerId in peerIds { + let _ = enqueueMessages(account: account, peerId: peerId, messages: [.forward(source: message.id)]).start() + } + } + } + } + controller.calloutPressed = { [weak legacyController] in + legacyController?.dismiss() + + if let author = message.author { + openPeer(author) + } + } + return legacyController +} diff --git a/TelegramUI/LegacyLocationPicker.swift b/TelegramUI/LegacyLocationPicker.swift index fbf287572c..336f417026 100644 --- a/TelegramUI/LegacyLocationPicker.swift +++ b/TelegramUI/LegacyLocationPicker.swift @@ -1,2 +1,20 @@ import Foundation +import Display +import TelegramLegacyComponents +import TelegramCore +func legacyLocationPickerController(sendLocation: @escaping (CLLocationCoordinate2D, MapVenue?) -> Void) -> ViewController { + let controller = TGLocationPickerController(intent: TGLocationPickerControllerDefaultIntent)! + let navigationController = TGNavigationController(controllers: [controller])! + let legacyController = LegacyController(legacyController: navigationController, presentation: .modal(animateIn: true)) + controller.customDismiss = { [weak legacyController] in + legacyController?.dismiss() + } + controller.locationPicked = { [weak legacyController] coordinate, venue in + sendLocation(coordinate, venue.flatMap { venue in + return MapVenue(title: venue.title, address: venue.address, provider: venue.provider, id: venue.venueId) + }) + legacyController?.dismiss() + } + return legacyController +} diff --git a/TelegramUI/LinkHighlightingNode.swift b/TelegramUI/LinkHighlightingNode.swift index 7b96108fc9..4944063865 100644 --- a/TelegramUI/LinkHighlightingNode.swift +++ b/TelegramUI/LinkHighlightingNode.swift @@ -31,9 +31,9 @@ private func drawConnectingCorner(context: CGContext, color: UIColor, at point: context.setFillColor(color.cgColor) switch type { case .topLeft: - context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius))) context.setFillColor(UIColor.clear.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) case .topRight: context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius))) context.setFillColor(UIColor.clear.cgColor) @@ -49,13 +49,11 @@ private func drawConnectingCorner(context: CGContext, color: UIColor, at point: } } -private func generateRectsImage(color: UIColor, rects: [CGRect]) -> (CGPoint, UIImage?) { +private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, UIImage?) { if rects.isEmpty { return (CGPoint(), nil) } - let inset: CGFloat = 2.0 - var topLeft = rects[0].origin var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY) for i in 1 ..< rects.count { @@ -76,8 +74,6 @@ private func generateRectsImage(color: UIColor, rects: [CGRect]) -> (CGPoint, UI context.setBlendMode(.copy) - let radius: CGFloat = 4.0 - for i in 0 ..< rects.count { let rect = rects[i].insetBy(dx: -inset, dy: -inset) context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y)) @@ -98,42 +94,58 @@ private func generateRectsImage(color: UIColor, rects: [CGRect]) -> (CGPoint, UI if let previous = previous { if previous.contains(rect.topLeft) { - if abs(rect.topLeft.x - previous.minX) >= radius { + if abs(rect.topLeft.x - previous.minX) >= innerRadius { + var radius = innerRadius + if let next = next { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius) } } else { - drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: radius) + drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius) } if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) { - if abs(rect.topRight.x - previous.maxX) >= radius { + if abs(rect.topRight.x - previous.maxX) >= innerRadius { + var radius = innerRadius + if let next = next { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius) } } else { - drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: radius) + drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius) } } else { - drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: radius) - drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: radius) + drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius) + drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius) } if let next = next { if next.contains(rect.bottomLeft) { - if abs(rect.bottomRight.x - next.maxX) >= radius { + if abs(rect.bottomRight.x - next.maxX) >= innerRadius { + var radius = innerRadius + if let previous = previous { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius) } } else { - drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: radius) + drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius) } if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) { - if abs(rect.bottomRight.x - next.maxX) >= radius { + if abs(rect.bottomRight.x - next.maxX) >= innerRadius { + var radius = innerRadius + if let previous = previous { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius) } } else { - drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: radius) + drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius) } } else { - drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: radius) - drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: radius) + drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius) + drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius) } } })) @@ -144,8 +156,16 @@ final class LinkHighlightingNode: ASDisplayNode { private var rects: [CGRect] = [] private let imageNode: ASImageNode + var innerRadius: CGFloat = 4.0 + var outerRadius: CGFloat = 4.0 + var inset: CGFloat = 2.0 + + private var _color: UIColor var color: UIColor { - didSet { + get { + return _color + } set(value) { + self._color = value if !self.rects.isEmpty { self.updateImage() } @@ -153,13 +173,13 @@ final class LinkHighlightingNode: ASDisplayNode { } init(color: UIColor) { + self._color = color + self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true self.imageNode.displaysAsynchronously = false self.imageNode.displayWithoutProcessing = true - self.color = color - super.init() self.addSubnode(self.imageNode) @@ -177,11 +197,41 @@ final class LinkHighlightingNode: ASDisplayNode { if rects.isEmpty { self.imageNode.image = nil } - let (offset, image) = generateRectsImage(color: self.color, rects: self.rects) + let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius) if let image = image { self.imageNode.image = image self.imageNode.frame = CGRect(origin: offset, size: image.size) } } + + func asyncLayout() -> (UIColor, [CGRect], CGFloat, CGFloat, CGFloat) -> () -> Void { + let currentRects = self.rects + let currentColor = self._color + let currentInnerRadius = self.innerRadius + let currentOuterRadius = self.outerRadius + let currentInset = self.inset + + return { [weak self] color, rects, innerRadius, outerRadius, inset in + var updatedImage: (CGPoint, UIImage?)? + if currentRects != rects || !currentColor.isEqual(color) || currentInnerRadius != innerRadius || currentOuterRadius != outerRadius || currentInset != inset { + updatedImage = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius) + } + + return { + if let strongSelf = self { + strongSelf._color = color + strongSelf.rects = rects + strongSelf.innerRadius = innerRadius + strongSelf.outerRadius = outerRadius + strongSelf.inset = inset + + if let (offset, maybeImage) = updatedImage, let image = maybeImage { + strongSelf.imageNode.image = image + strongSelf.imageNode.frame = CGRect(origin: offset, size: image.size) + } + } + } + } + } } diff --git a/TelegramUI/MapResources.swift b/TelegramUI/MapResources.swift new file mode 100644 index 0000000000..0ebd14f662 --- /dev/null +++ b/TelegramUI/MapResources.swift @@ -0,0 +1,116 @@ +import Foundation +import Postbox +import TelegramCore +import MapKit +import SwiftSignalKit + +public struct MapSnapshotMediaResourceId: MediaResourceId { + public let latitude: Double + public let longitude: Double + public let width: Int32 + public let height: Int32 + + public var uniqueId: String { + return "map-\(latitude)-\(longitude)-\(width)x\(height)" + } + + public var hashValue: Int { + return self.uniqueId.hashValue + } + + public func isEqual(to: MediaResourceId) -> Bool { + if let to = to as? MapSnapshotMediaResourceId { + return self.latitude == to.latitude && self.longitude == to.longitude && self.width == to.width && self.height == to.height + } else { + return false + } + } +} + +public class MapSnapshotMediaResource: TelegramMediaResource { + public let latitude: Double + public let longitude: Double + public let width: Int32 + public let height: Int32 + + public init(latitude: Double, longitude: Double, width: Int32, height: Int32) { + self.latitude = latitude + self.longitude = longitude + self.width = width + self.height = height + } + + public required init(decoder: Decoder) { + self.latitude = decoder.decodeDoubleForKey("lt", orElse: 0.0) + self.longitude = decoder.decodeDoubleForKey("ln", orElse: 0.0) + self.width = decoder.decodeInt32ForKey("w", orElse: 0) + self.height = decoder.decodeInt32ForKey("h", orElse: 0) + } + + public func encode(_ encoder: Encoder) { + encoder.encodeDouble(self.latitude, forKey: "lt") + encoder.encodeDouble(self.longitude, forKey: "ln") + encoder.encodeInt32(self.width, forKey: "w") + encoder.encodeInt32(self.height, forKey: "h") + } + + public var id: MediaResourceId { + return MapSnapshotMediaResourceId(latitude: self.latitude, longitude: self.longitude, width: self.width, height: self.height) + } + + public func isEqual(to: TelegramMediaResource) -> Bool { + if let to = to as? MapSnapshotMediaResource { + return self.latitude == to.latitude && self.longitude == to.longitude && self.width == to.width && self.height == to.height + } else { + return false + } + } +} + +let TGGoogleMapsOffset: Int = 268435456 +let TGGoogleMapsRadius = Double(TGGoogleMapsOffset) / Double.pi + +private func yToLatitude(_ y: Int) -> Double { + return ((Double.pi / 2.0) - 2 * atan(exp((Double(y - TGGoogleMapsOffset)) / TGGoogleMapsRadius))) * 180.0 / Double.pi; +} + +private func latitudeToY(_ latitude: Double) -> Int { + return Int(round(Double(TGGoogleMapsOffset) - TGGoogleMapsRadius * log((1.0 + sin(latitude * Double.pi / 180.0)) / (1.0 - sin(latitude * Double.pi / 180.0))) / 2.0)) +} + +private func adjustGMapLatitude(_ latitude: Double, offset: Int, zoom: Int) -> Double { + let t: Int = (offset << (21 - zoom)) + return yToLatitude(latitudeToY(latitude) + t) +} + +func fetchMapSnapshotResource(resource: MapSnapshotMediaResource) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + Queue.concurrentDefaultQueue().async { + let options = MKMapSnapshotOptions() + let latitude = adjustGMapLatitude(resource.latitude, offset: -10, zoom: 15) + options.region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(latitude, resource.longitude), MKCoordinateSpanMake(0.003, 0.003)) + options.mapType = .standard + options.showsPointsOfInterest = false + options.showsBuildings = true + options.size = CGSize(width: CGFloat(resource.width + 1), height: CGFloat(resource.height + 24)) + options.scale = 2.0 + let snapshotter = MKMapSnapshotter(options: options) + snapshotter.start(with: DispatchQueue.global(), completionHandler: { result, error in + if let image = result?.image { + if let data = UIImageJPEGRepresentation(image, 0.6) { + subscriber.putNext(MediaResourceDataFetchResult.dataPart(data: data, range: 0 ..< data.count, complete: true)) + subscriber.putCompletion() + } + } + }) + disposable.set(ActionDisposable { + snapshotter.cancel() + }) + } + + return disposable + } +} + diff --git a/TelegramUI/MediaPlayer.swift b/TelegramUI/MediaPlayer.swift index 96e9cc87e7..4205ec2e1b 100644 --- a/TelegramUI/MediaPlayer.swift +++ b/TelegramUI/MediaPlayer.swift @@ -147,6 +147,8 @@ private final class MediaPlayerContext { } fileprivate func seek(timestamp: Double) { + assert(self.queue.isCurrent()) + let action: MediaPlayerPlaybackAction switch self.state { case .empty, .paused: diff --git a/TelegramUI/NumericFormat.swift b/TelegramUI/NumericFormat.swift index cd6f1a3cdb..9098d0eb75 100644 --- a/TelegramUI/NumericFormat.swift +++ b/TelegramUI/NumericFormat.swift @@ -22,15 +22,36 @@ public func compactNumericCountString(_ count: Int) -> String { func timeIntervalString(strings: PresentationStrings, value: Int32) -> String { if value < 60 { - return strings.MessageTimer_Seconds(value) + return strings.MessageTimer_Seconds(max(1, value)) } else if value < 60 * 60 { - return strings.MessageTimer_Minutes(value / 60) + return strings.MessageTimer_Minutes(max(1, value / 60)) } else if value < 60 * 60 * 24 { - return strings.MessageTimer_Hours(value / (60 * 60)) + return strings.MessageTimer_Hours(max(1, value / (60 * 60))) } else if value < 60 * 60 * 24 * 7 { - return strings.MessageTimer_Days(value / (60 * 60 * 24)) + return strings.MessageTimer_Days(max(1, value / (60 * 60 * 24))) } else { - return strings.MessageTimer_Weeks(value / (60 * 60 * 24 * 7)) + return strings.MessageTimer_Weeks(max(1, value / (60 * 60 * 24 * 7))) } } +func muteForIntervalString(strings: PresentationStrings, value: Int32) -> String { + if value < 60 * 60 { + return strings.MuteFor_Minutes(max(1, value / 60)) + } else if value < 60 * 60 * 24 { + return strings.MuteFor_Hours(max(1, value / (60 * 60))) + } else if value < 60 * 60 * 24 * 7 { + return strings.MuteFor_Days(max(1, value / (60 * 60 * 24))) + } else { + return strings.MuteFor_Weeks(max(1, value / (60 * 60 * 24 * 7))) + } +} + +func unmuteIntervalString(strings: PresentationStrings, value: Int32) -> String { + if value < 60 * 60 { + return strings.MuteExpires_Minutes(max(1, value / 60)) + } else if value < 60 * 60 * 24 { + return strings.MuteExpires_Hours(max(1, value / (60 * 60))) + } else { + return strings.MuteExpires_Days(max(1, value / (60 * 60 * 24))) + } +} diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 37db3c0f7c..314f8a49f6 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -1013,6 +1013,14 @@ func chatMessagePhotoCancelInteractiveFetch(account: Account, photo: TelegramMed } } +func chatMessageWebFileInteractiveFetched(account: Account, image: TelegramMediaWebFile) -> Signal { + return account.postbox.mediaBox.fetchedResource(image.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) +} + +func chatMessageWebFileCancelInteractiveFetch(account: Account, image: TelegramMediaWebFile) { + return account.postbox.mediaBox.cancelInteractiveResourceFetch(image.resource) +} + func chatWebpageSnippetPhotoData(account: Account, photo: TelegramMediaImage) -> Signal { if let closestRepresentation = photo.representationForDisplayAtSize(CGSize(width: 120.0, height: 120.0)) { let resourceData = account.postbox.mediaBox.resourceData(closestRepresentation.resource) |> map { next in @@ -1372,12 +1380,6 @@ func chatAvatarGalleryPhoto(account: Account, representations: [TelegramMediaIma var fullSizeImage: CGImage? if let fullSizeData = fullSizeData { if fullSizeComplete { - /*let options = NSMutableDictionary() - options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) - options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) - if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { - fullSizeImage = image - }*/ let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { @@ -1483,3 +1485,149 @@ func settingsBuiltinWallpaperImage(account: Account) -> Signal<(TransformImageAr } } +func chatMapSnapshotData(account: Account, resource: MapSnapshotMediaResource) -> Signal { + return Signal { subscriber in + let fetchedDisposable = account.postbox.mediaBox.fetchedResource(resource, tag: nil).start() + let dataDisposable = account.postbox.mediaBox.resourceData(resource).start(next: { next in + subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + dataDisposable.dispose() + } + } +} + +private let locationPinImage = UIImage(named: "ModernMessageLocationPin")?.precomposed() + +func chatMapSnapshotImage(account: Account, resource: MapSnapshotMediaResource) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMapSnapshotData(account: account, resource: resource) + + return signal |> map { fullSizeData in + return { arguments in + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + var fullSizeImage: CGImage? + if let fullSizeData = fullSizeData { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + + if let fullSizeImage = fullSizeImage { + let drawingRect = arguments.drawingRect + var fittedSize = CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height)).aspectFilled(drawingRect.size) + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + c.fill(arguments.drawingRect) + } + + c.setBlendMode(.copy) + + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + + c.setBlendMode(.normal) + + if let locationPinImage = locationPinImage { + c.draw(locationPinImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - locationPinImage.size.width) / 2.0), y: floor((arguments.drawingSize.height - locationPinImage.size.height) / 2.0) - 5.0), size: locationPinImage.size)) + } + } + } else { + context.withFlippedContext { c in + c.setBlendMode(.copy) + c.setFillColor(UIColor.white.cgColor) + c.fill(arguments.drawingRect) + + c.setBlendMode(.normal) + + if let locationPinImage = locationPinImage { + c.draw(locationPinImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - locationPinImage.size.width) / 2.0), y: floor((arguments.drawingSize.height - locationPinImage.size.height) / 2.0) - 5.0), size: locationPinImage.size)) + } + } + } + } + + addCorners(context, arguments: arguments) + + return context + } + } +} + +func chatWebFileImage(account: Account, file: TelegramMediaWebFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return account.postbox.mediaBox.resourceData(file.resource) + |> map { fullSizeData in + return { arguments in + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + var fullSizeImage: CGImage? + if fullSizeData.complete { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let imageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: fullSizeData.path) as CFURL, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + + if let fullSizeImage = fullSizeImage { + let drawingRect = arguments.drawingRect + var fittedSize = CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height)).aspectFilled(drawingRect.size) + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + c.fill(arguments.drawingRect) + } + + c.setBlendMode(.copy) + + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + + c.setBlendMode(.normal) + + if let locationPinImage = locationPinImage { + c.draw(locationPinImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - locationPinImage.size.width) / 2.0), y: floor((arguments.drawingSize.height - locationPinImage.size.height) / 2.0) - 5.0), size: locationPinImage.size)) + } + } + } else { + context.withFlippedContext { c in + c.setBlendMode(.copy) + c.setFillColor(UIColor.white.cgColor) + c.fill(arguments.drawingRect) + + c.setBlendMode(.normal) + + if let locationPinImage = locationPinImage { + c.draw(locationPinImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((arguments.drawingSize.width - locationPinImage.size.width) / 2.0), y: floor((arguments.drawingSize.height - locationPinImage.size.height) / 2.0) - 5.0), size: locationPinImage.size)) + } + } + } + } + + addCorners(context, arguments: arguments) + + return context + } + } +} diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index 9454fba022..8f344ef932 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -15,6 +15,7 @@ enum PresentationResourceKey: Int32 { case navigationComposeIcon case navigationCallIcon + case navigationShareIcon case navigationPlayerCloseButton case navigationPlayerPlayIcon diff --git a/TelegramUI/PresentationResourcesRootController.swift b/TelegramUI/PresentationResourcesRootController.swift index b5d87be2c0..c9fe6786d6 100644 --- a/TelegramUI/PresentationResourcesRootController.swift +++ b/TelegramUI/PresentationResourcesRootController.swift @@ -9,6 +9,10 @@ private func generateComposeButtonImage(theme: PresentationTheme) -> UIImage? { }) } +private func generateShareButtonImage(theme: PresentationTheme) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/NavigationShare"), color: theme.rootController.navigationBar.accentTextColor) +} + struct PresentationResourcesRootController { static func navigationIndefiniteActivityImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.rootNavigationIndefiniteActivity.rawValue, { theme in @@ -60,6 +64,10 @@ struct PresentationResourcesRootController { return theme.image(PresentationResourceKey.navigationComposeIcon.rawValue, generateComposeButtonImage) } + static func navigationShareIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationShareIcon.rawValue, generateShareButtonImage) + } + static func navigationDropdownArrowImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationDropdownArrowImage.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/TitleViewModeSelectionArrow"), color: theme.rootController.navigationBar.accentTextColor) diff --git a/TelegramUI/PresentationStrings.swift b/TelegramUI/PresentationStrings.swift index 38a9002d5f..8e99851f84 100644 --- a/TelegramUI/PresentationStrings.swift +++ b/TelegramUI/PresentationStrings.swift @@ -1,8 +1,26 @@ import Foundation +private let fallbackDict: [String: String] = { + if let mainPath = Bundle.main.path(forResource: "en", ofType: "lproj"), let bundle = Bundle(path: mainPath) { + if let path = bundle.path(forResource: "Localizable", ofType: "strings") { + if let dict = NSDictionary(contentsOf: URL(fileURLWithPath: path)) as? [String: String] { + return dict + } else { + return [:] + } + } else { + return [:] + } + } else { + return [:] + } +}() + private func getValue(_ dict: [String: String], _ key: String) -> String { if let value = dict[key] { return value + } else if let value = fallbackDict[key] { + return value } else { return key } @@ -14,9 +32,9 @@ private extension PluralizationForm { case .zero: return "_many" case .one: - return "_one" + return "_1" case .two: - return "_two" + return "_2" case .few: return "_1_3" case .many: @@ -29,6 +47,8 @@ private extension PluralizationForm { private func getValueWithForm(_ dict: [String: String], _ key: String, _ form: PluralizationForm) -> String { if let value = dict[key + form.canonicalSuffix] { return value + } else if let value = fallbackDict[key + form.canonicalSuffix] { + return value } return key } diff --git a/TelegramUI/ShareControllerNode.swift b/TelegramUI/ShareControllerNode.swift index f7717733c8..aaa76f0032 100644 --- a/TelegramUI/ShareControllerNode.swift +++ b/TelegramUI/ShareControllerNode.swift @@ -141,6 +141,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate if strongSelf.selectedPeers.isEmpty { if let defaultAction = strongSelf.defaultAction { strongSelf.installActionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) + } else { + strongSelf.installActionButtonNode.setTitle("Send", with: Font.medium(20.0), with: .gray, for: .normal) } strongSelf.installActionButtonNode.badge = nil } else { @@ -302,8 +304,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate transition.updateFrame(node: self.installActionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) transition.updateFrame(node: self.installActionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) - self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: contentFrame.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), lineSpacing: 0.0)), transition: transition), stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - transition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight)))) + let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight)) + + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), lineSpacing: 0.0)), transition: transition), stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + transition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) if animateIn { var durationOffset = 0.0 @@ -456,6 +460,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate if let defaultAction = defaultAction { self.installActionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) + } else { + self.installActionButtonNode.setTitle("Send", with: Font.medium(20.0), with: .gray, for: .normal) } } diff --git a/TelegramUI/TelegramAccountAuxiliaryMethods.swift b/TelegramUI/TelegramAccountAuxiliaryMethods.swift index 730328c5ca..505ba76e62 100644 --- a/TelegramUI/TelegramAccountAuxiliaryMethods.swift +++ b/TelegramUI/TelegramAccountAuxiliaryMethods.swift @@ -17,6 +17,8 @@ public let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerC return fetchLocalFileVideoMediaResource(resource: resource) } else if let photoLibraryResource = resource as? PhotoLibraryMediaResource { return fetchPhotoLibraryResource(localIdentifier: photoLibraryResource.localIdentifier) + } else if let mapSnapshotResource = resource as? MapSnapshotMediaResource { + return fetchMapSnapshotResource(resource: mapSnapshotResource) } return nil }) diff --git a/TelegramUI/TelegramInitializeLegacyComponents.swift b/TelegramUI/TelegramInitializeLegacyComponents.swift index eaf1ecf5c9..8346d8f4e4 100644 --- a/TelegramUI/TelegramInitializeLegacyComponents.swift +++ b/TelegramUI/TelegramInitializeLegacyComponents.swift @@ -45,7 +45,7 @@ private final class AccessCheckerImpl: NSObject, TGAccessCheckerProtocol { } } -public func initializeLegacyComponents(application: UIApplication, currentSizeClassGetter: @escaping () -> UIUserInterfaceSizeClass, currentHorizontalClassGetter: @escaping () -> UIUserInterfaceSizeClass, documentsPath: String, currentApplicationBounds: @escaping () -> CGRect) { +public func initializeLegacyComponents(application: UIApplication, currentSizeClassGetter: @escaping () -> UIUserInterfaceSizeClass, currentHorizontalClassGetter: @escaping () -> UIUserInterfaceSizeClass, documentsPath: String, currentApplicationBounds: @escaping () -> CGRect, canOpenUrl: @escaping (URL) -> Bool, openUrl: @escaping (URL) -> Void) { freedomInit() //freedomUIKitInit(); TGHacks.setApplication(application) @@ -57,4 +57,16 @@ public func initializeLegacyComponents(application: UIApplication, currentSizeCl return SSignal.single(UIUserInterfaceSizeClass.compact.rawValue as NSNumber) } TGLegacyComponentsSetDocumentsPath(documentsPath) + + TGLegacyComponentsSetCanOpenURL({ url in + if let url = url { + return canOpenUrl(url) + } + return false + }) + TGLegacyComponentsSetOpenURL({ url in + if let url = url { + return openUrl(url) + } + }) } diff --git a/TelegramUI/TextNode.swift b/TelegramUI/TextNode.swift index 5793c9c0a9..39a146fbef 100644 --- a/TelegramUI/TextNode.swift +++ b/TelegramUI/TextNode.swift @@ -115,6 +115,14 @@ final class TextNodeLayout: NSObject { return nil } + func linesRects() -> [CGRect] { + var rects: [CGRect] = [] + for line in self.lines { + rects.append(line.frame) + } + return rects + } + func attributeRects(name: String, at index: Int) -> [CGRect]? { if let attributedString = self.attributedString { var range = NSRange()