diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index d4ef9335b2..b09aefcaf6 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -244,6 +244,11 @@ D0E8175720122DAD00B82BBB /* ChatRecentActionsSearchNavigationContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8175620122DAD00B82BBB /* ChatRecentActionsSearchNavigationContentNode.swift */; }; D0E8175920122FE100B82BBB /* ChatRecentActionsFilterController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8175820122FE100B82BBB /* ChatRecentActionsFilterController.swift */; }; D0E8175B201254FA00B82BBB /* ChatRecentActionsEmptyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8175A201254FA00B82BBB /* ChatRecentActionsEmptyNode.swift */; }; + D0E8B8A72044339500605593 /* PresentationCallToneData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8B8A62044339500605593 /* PresentationCallToneData.swift */; }; + D0E8B8B9204477B600605593 /* SecretChatKeyVisualization.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8B8B8204477B600605593 /* SecretChatKeyVisualization.swift */; }; + D0E8B8BB2044780600605593 /* ItemListSecretChatKeyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8B8BA2044780600605593 /* ItemListSecretChatKeyItem.swift */; }; + D0E8B8BD204479A500605593 /* SecretChatKeyController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8B8BC204479A500605593 /* SecretChatKeyController.swift */; }; + D0E8B8BF20447A4600605593 /* SecretChatKeyControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E8B8BE20447A4600605593 /* SecretChatKeyControllerNode.swift */; }; D0E9B9E81EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9B9E71EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift */; }; D0E9B9EA1F00853C00F079A4 /* PhoneCountries.txt in Resources */ = {isa = PBXBuildFile; fileRef = D0E9B9E91F00853C00F079A4 /* PhoneCountries.txt */; }; D0E9B9F41F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E9B9F31F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift */; }; @@ -403,7 +408,6 @@ D0EC6CCE1EB9F58800EBF1C3 /* TelegramApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */; }; D0EC6CCF1EB9F58800EBF1C3 /* GeoLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B66B71DD672D00049C3D2 /* GeoLocation.swift */; }; D0EC6CD11EB9F58800EBF1C3 /* UrlHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D023836F1DDF0462004018B6 /* UrlHandling.swift */; }; - D0EC6CD21EB9F58800EBF1C3 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */; }; D0EC6CD31EB9F58800EBF1C3 /* GenerateTextEntities.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F917B41E0DA396003687E6 /* GenerateTextEntities.swift */; }; D0EC6CD41EB9F58800EBF1C3 /* StringWithAppliedEntities.swift in Sources */ = {isa = PBXBuildFile; fileRef = D017494D1E1059570057C89A /* StringWithAppliedEntities.swift */; }; D0EC6CD51EB9F58800EBF1C3 /* StoredMessageFromSearchPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01749541E1082770057C89A /* StoredMessageFromSearchPeer.swift */; }; @@ -854,6 +858,9 @@ D0F8C399201774AF00236FC5 /* FeedGroupingControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F8C398201774AF00236FC5 /* FeedGroupingControllerNode.swift */; }; D0F9720F1FFE4BD5002595C8 /* notification.caf in Resources */ = {isa = PBXBuildFile; fileRef = D0C50E431E93FCD200F62E39 /* notification.caf */; }; D0F972101FFE4BD5002595C8 /* MessageSent.caf in Resources */ = {isa = PBXBuildFile; fileRef = D073CE621DCBBE5D007511FD /* MessageSent.caf */; }; + D0FA08BE20481EA300DD23FC /* Locale.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA08BD20481EA300DD23FC /* Locale.swift */; }; + D0FA08C020483F9600DD23FC /* ExtractVideoData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA08BF20483F9600DD23FC /* ExtractVideoData.swift */; }; + D0FA08C8204982DC00DD23FC /* ChatTextInputActionButtonsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA08C7204982DC00DD23FC /* ChatTextInputActionButtonsNode.swift */; }; D0FB87B21F7C4C19004DE005 /* FetchMediaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FB87B11F7C4C19004DE005 /* FetchMediaUtils.swift */; }; D0FC194D201F82A000FEDBB2 /* OpenResolvedUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC194C201F82A000FEDBB2 /* OpenResolvedUrl.swift */; }; D0FC408E1D5B8E7500261D9D /* TelegramUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC408D1D5B8E7500261D9D /* TelegramUITests.swift */; }; @@ -1197,7 +1204,6 @@ D05677501F4CA0C2001B723E /* InstantPagePeerReferenceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePeerReferenceItem.swift; sourceTree = ""; }; D05677521F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePeerReferenceNode.swift; sourceTree = ""; }; D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveformNode.swift; sourceTree = ""; }; - D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = ""; }; D056CD6F1FF147B000880D28 /* IconButtonNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconButtonNode.swift; sourceTree = ""; }; D056CD711FF1569800880D28 /* MusicPlaybackSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicPlaybackSettings.swift; sourceTree = ""; }; D056CD731FF2996B00880D28 /* ExternalMusicAlbumArtResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalMusicAlbumArtResources.swift; sourceTree = ""; }; @@ -1516,6 +1522,11 @@ D0E8175620122DAD00B82BBB /* ChatRecentActionsSearchNavigationContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsSearchNavigationContentNode.swift; sourceTree = ""; }; D0E8175820122FE100B82BBB /* ChatRecentActionsFilterController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsFilterController.swift; sourceTree = ""; }; D0E8175A201254FA00B82BBB /* ChatRecentActionsEmptyNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsEmptyNode.swift; sourceTree = ""; }; + D0E8B8A62044339500605593 /* PresentationCallToneData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationCallToneData.swift; sourceTree = ""; }; + D0E8B8B8204477B600605593 /* SecretChatKeyVisualization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretChatKeyVisualization.swift; sourceTree = ""; }; + D0E8B8BA2044780600605593 /* ItemListSecretChatKeyItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListSecretChatKeyItem.swift; sourceTree = ""; }; + D0E8B8BC204479A500605593 /* SecretChatKeyController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretChatKeyController.swift; sourceTree = ""; }; + D0E8B8BE20447A4600605593 /* SecretChatKeyControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretChatKeyControllerNode.swift; sourceTree = ""; }; D0E9B9E71EFEFB9500F079A4 /* BotPaymentDisclosureItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotPaymentDisclosureItemNode.swift; sourceTree = ""; }; D0E9B9E91F00853C00F079A4 /* PhoneCountries.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = PhoneCountries.txt; path = TelegramUI/Resources/PhoneCountries.txt; sourceTree = ""; }; D0E9B9F31F018A6700F079A4 /* BotCheckoutPaymentMethodSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotCheckoutPaymentMethodSheet.swift; sourceTree = ""; }; @@ -1788,6 +1799,9 @@ D0F8C396201774A200236FC5 /* FeedGroupingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedGroupingController.swift; sourceTree = ""; }; D0F8C398201774AF00236FC5 /* FeedGroupingControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedGroupingControllerNode.swift; sourceTree = ""; }; D0F917B41E0DA396003687E6 /* GenerateTextEntities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenerateTextEntities.swift; sourceTree = ""; }; + D0FA08BD20481EA300DD23FC /* Locale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locale.swift; sourceTree = ""; }; + D0FA08BF20483F9600DD23FC /* ExtractVideoData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtractVideoData.swift; sourceTree = ""; }; + D0FA08C7204982DC00DD23FC /* ChatTextInputActionButtonsNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextInputActionButtonsNode.swift; sourceTree = ""; }; D0FA0ABE1E76E17F005BB9B7 /* TwoStepVerificationPasswordEntryController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationPasswordEntryController.swift; sourceTree = ""; }; D0FA0AC01E7725AA005BB9B7 /* TwoStepVerificationResetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationResetController.swift; sourceTree = ""; }; D0FA0AC41E77431A005BB9B7 /* InstalledStickerPacksController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstalledStickerPacksController.swift; sourceTree = ""; }; @@ -2668,6 +2682,7 @@ D0EC6B3E1EB8F3E500EBF1C3 /* PresentationCallManager.swift */, D0B4AF8A1EC1133600D51FF6 /* CallKitIntergation.swift */, D0F0AADF1EC1E12C005EE2A5 /* PresentationCall.swift */, + D0E8B8A62044339500605593 /* PresentationCallToneData.swift */, ); name = Calls; sourceTree = ""; @@ -3197,6 +3212,9 @@ D0F67FF31EE6C10F000E5906 /* ChannelMembersSearchContainerNode.swift */, D04281EE200E3D88009DDE36 /* GroupInfoSearchItem.swift */, D04281F0200E4084009DDE36 /* GroupInfoSearchNavigationContentNode.swift */, + D0E8B8BA2044780600605593 /* ItemListSecretChatKeyItem.swift */, + D0E8B8BC204479A500605593 /* SecretChatKeyController.swift */, + D0E8B8BE20447A4600605593 /* SecretChatKeyControllerNode.swift */, ); name = "Peer Info"; sourceTree = ""; @@ -3248,6 +3266,7 @@ D0F69CDE1D6B87D30046BCD6 /* PeerAvatar.swift */, D0F69E9D1D6B8E240046BCD6 /* Resources */, D0177B831DFB095000A5083A /* FileMediaResourceStatus.swift */, + D0FA08BF20483F9600DD23FC /* ExtractVideoData.swift */, ); name = Media; sourceTree = ""; @@ -3517,6 +3536,7 @@ isa = PBXGroup; children = ( D0F69E401D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift */, + D0FA08C7204982DC00DD23FC /* ChatTextInputActionButtonsNode.swift */, D01F66121DE8903300345CBE /* ChatTextInputMediaRecordingButton.swift */, D039EB021DEAEFEE00886EBC /* ChatTextInputAudioRecordingOverlayButton.swift */, D039EB071DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift */, @@ -3677,7 +3697,6 @@ D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */, D04B66B71DD672D00049C3D2 /* GeoLocation.swift */, D023836F1DDF0462004018B6 /* UrlHandling.swift */, - D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */, D0F917B41E0DA396003687E6 /* GenerateTextEntities.swift */, D017494D1E1059570057C89A /* StringWithAppliedEntities.swift */, D01749541E1082770057C89A /* StoredMessageFromSearchPeer.swift */, @@ -3705,6 +3724,8 @@ D0FC194C201F82A000FEDBB2 /* OpenResolvedUrl.swift */, D00ACA592022897D0045D427 /* ProcessedPeerRestrictionText.swift */, D0BCC3D1203F0A6C008126C2 /* StringForMessageTimestampStatus.swift */, + D0E8B8B8204477B600605593 /* SecretChatKeyVisualization.swift */, + D0FA08BD20481EA300DD23FC /* Locale.swift */, ); name = Utils; sourceTree = ""; @@ -4090,7 +4111,6 @@ D0EC6CD11EB9F58800EBF1C3 /* UrlHandling.swift in Sources */, D0FC4FBB1F751E8900B7443F /* SelectablePeerNode.swift in Sources */, D0E9BAD21F0573C000F079A4 /* STPToken.m in Sources */, - D0EC6CD21EB9F58800EBF1C3 /* HapticFeedback.swift in Sources */, D0EC6CD31EB9F58800EBF1C3 /* GenerateTextEntities.swift in Sources */, D0EC6CD41EB9F58800EBF1C3 /* StringWithAppliedEntities.swift in Sources */, D0EC6CD51EB9F58800EBF1C3 /* StoredMessageFromSearchPeer.swift in Sources */, @@ -4124,6 +4144,7 @@ D0EC6CE61EB9F58800EBF1C3 /* PresentationsResourceCache.swift in Sources */, D01776BA1F1D704F0044446D /* RadialStatusIconContentNode.swift in Sources */, D0EC6CE71EB9F58800EBF1C3 /* PresentationTheme.swift in Sources */, + D0E8B8BF20447A4600605593 /* SecretChatKeyControllerNode.swift in Sources */, D0EC6CE81EB9F58800EBF1C3 /* DefaultPresentationTheme.swift in Sources */, D0EC6CE91EB9F58800EBF1C3 /* DefaultDarkPresentationTheme.swift in Sources */, D0EC6CEA1EB9F58800EBF1C3 /* DefaultPresentationStrings.swift in Sources */, @@ -4246,6 +4267,7 @@ D0EC6D2C1EB9F58800EBF1C3 /* TouchDownGestureRecognizer.swift in Sources */, D0EC6D2D1EB9F58800EBF1C3 /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */, D0AF7C461ED84BC500CD8E0F /* LanguageSelectionController.swift in Sources */, + D0FA08C020483F9600DD23FC /* ExtractVideoData.swift in Sources */, D0EC6D2E1EB9F58800EBF1C3 /* ImageNode.swift in Sources */, D0EC6D2F1EB9F58800EBF1C3 /* TransformImageNode.swift in Sources */, D0EC6D301EB9F58800EBF1C3 /* RadialProgressNode.swift in Sources */, @@ -4257,6 +4279,7 @@ D0EC6D341EB9F58800EBF1C3 /* AvatarNode.swift in Sources */, D0EC6D351EB9F58800EBF1C3 /* SearchBarNode.swift in Sources */, D0EC6D361EB9F58800EBF1C3 /* SearchBarPlaceholderNode.swift in Sources */, + D0E8B8B9204477B600605593 /* SecretChatKeyVisualization.swift in Sources */, D0EC6D371EB9F58800EBF1C3 /* SearchDisplayController.swift in Sources */, D04281ED200E3B28009DDE36 /* ItemListControllerSearch.swift in Sources */, D0EC6D381EB9F58800EBF1C3 /* SearchDisplayControllerContentNode.swift in Sources */, @@ -4384,8 +4407,10 @@ D0EC6D8F1EB9F58800EBF1C3 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */, D01776BC1F1E21AF0044446D /* RadialStatusBackgroundNode.swift in Sources */, D0FE4DE61F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift in Sources */, + D0E8B8A72044339500605593 /* PresentationCallToneData.swift in Sources */, D0EC6D901EB9F58900EBF1C3 /* ChatMessageBubbleContentNode.swift in Sources */, D0EC6D911EB9F58900EBF1C3 /* ChatMessageBubbleItemNode.swift in Sources */, + D0E8B8BD204479A500605593 /* SecretChatKeyController.swift in Sources */, D0B85C1C1FF6F76000E795B4 /* AuthorizationSequencePasswordRecoveryController.swift in Sources */, D0471B511EFD872F0074D609 /* CurrencyFormat.swift in Sources */, D0EC6D921EB9F58900EBF1C3 /* ChatMessageDateAndStatusNode.swift in Sources */, @@ -4558,6 +4583,7 @@ D0EC6DFD1EB9F58900EBF1C3 /* GalleryControllerNode.swift in Sources */, D0E9BA571F055A0B00F079A4 /* STPFormTextField.m in Sources */, D0EC6DFE1EB9F58900EBF1C3 /* GalleryControllerPresentationState.swift in Sources */, + D0E8B8BB2044780600605593 /* ItemListSecretChatKeyItem.swift in Sources */, D0EC6DFF1EB9F58900EBF1C3 /* GalleryItem.swift in Sources */, D0EC6E001EB9F58900EBF1C3 /* GalleryItemNode.swift in Sources */, D048EA8B1F4F298A00188713 /* InstantPageSettingsThemeItemNode.swift in Sources */, @@ -4567,6 +4593,7 @@ D0E9BA0A1F0457DD00F079A4 /* BotCheckoutWebInteractionController.swift in Sources */, D0EC6E041EB9F58900EBF1C3 /* SecretMediaPreviewController.swift in Sources */, D0C26D571FDF2388004ABF18 /* OpenChatMessage.swift in Sources */, + D0FA08BE20481EA300DD23FC /* Locale.swift in Sources */, D0EC6E051EB9F58900EBF1C3 /* SecretMediaPreviewControllerNode.swift in Sources */, D007019C2029E8F2006B9E34 /* LegqacyICloudFileController.swift in Sources */, D0208AD61FA33D14001F0D5F /* RaiseToListenActivator.m in Sources */, @@ -4760,6 +4787,7 @@ D0EC6E881EB9F58900EBF1C3 /* FFMpegSwResample.m in Sources */, D0EC6E891EB9F58900EBF1C3 /* FrameworkBundle.swift in Sources */, D0EC6E8B1EB9F58900EBF1C3 /* RingBuffer.m in Sources */, + D0FA08C8204982DC00DD23FC /* ChatTextInputActionButtonsNode.swift in Sources */, D0EC6E8C1EB9F58900EBF1C3 /* RingByteBuffer.swift in Sources */, D0E9BA181F05574500F079A4 /* STPPaymentCardTextFieldViewModel.m in Sources */, D0EC6E8D1EB9F58900EBF1C3 /* SecretChatKeyVisualization.m in Sources */, diff --git a/TelegramUI/AudioRecordningToneData.swift b/TelegramUI/AudioRecordningToneData.swift index 7f1ae867a4..00d5009029 100644 --- a/TelegramUI/AudioRecordningToneData.swift +++ b/TelegramUI/AudioRecordningToneData.swift @@ -44,18 +44,6 @@ private func loadAudioRecordingToneData() -> Data? { if size != 0, let mData = abl.mBuffers.mData { data.append(Data(bytes: mData, count: size)) } - /*AudioBufferList abl; - CMBlockBufferRef blockBuffer = NULL; - CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(nextBuffer, NULL, &abl, sizeof(abl), NULL, NULL, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer); - UInt64 size = CMSampleBufferGetTotalSampleSize(nextBuffer); - if (size != 0) { - [data appendBytes:abl.mBuffers[0].mData length:size]; - } - - CFRelease(nextBuffer); - if (blockBuffer) { - CFRelease(blockBuffer); - }*/ } else { break } diff --git a/TelegramUI/BotCheckoutControllerNode.swift b/TelegramUI/BotCheckoutControllerNode.swift index dba42ef065..4bdbbc1e61 100644 --- a/TelegramUI/BotCheckoutControllerNode.swift +++ b/TelegramUI/BotCheckoutControllerNode.swift @@ -661,8 +661,15 @@ final class BotCheckoutControllerNode: ItemListControllerNode, } if !liabilityNoticeAccepted { - let _ = (combineLatest(ApplicationSpecificNotice.getBotPaymentLiability(postbox: self.account.postbox, peerId: self.messageId.peerId), self.account.postbox.loadedPeerWithId(self.messageId.peerId), self.account.postbox.loadedPeerWithId(PeerId(namespace: Namespaces.Peer.CloudUser, id: paymentForm.providerId))) |> deliverOnMainQueue).start(next: { [weak self] value, botPeer, providerPeer in - if let strongSelf = self { + let messageId = self.messageId + let botPeer: Signal = self.account.postbox.modify { modifier -> Peer? in + if let message = modifier.getMessage(messageId) { + return message.author + } + return nil + } + let _ = (combineLatest(ApplicationSpecificNotice.getBotPaymentLiability(postbox: self.account.postbox, peerId: self.messageId.peerId), botPeer, self.account.postbox.loadedPeerWithId(paymentForm.providerId)) |> deliverOnMainQueue).start(next: { [weak self] value, botPeer, providerPeer in + if let strongSelf = self, let botPeer = botPeer { if value { strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) } else { diff --git a/TelegramUI/CallControllerNode.swift b/TelegramUI/CallControllerNode.swift index a95e5824c3..c2d6537d80 100644 --- a/TelegramUI/CallControllerNode.swift +++ b/TelegramUI/CallControllerNode.swift @@ -164,8 +164,24 @@ final class CallControllerNode: ASDisplayNode { } else { statusValue = .text(self.presentationData.strings.Call_StatusRequesting) } - case .terminating, .terminated: + case .terminating: statusValue = .text(self.presentationData.strings.Call_StatusEnded) + case let .terminated(reason): + if let reason = reason { + switch reason { + case let .ended(type): + switch type { + case .busy: + statusValue = .text(self.presentationData.strings.Call_StatusBusy) + case .hungUp, .missed: + statusValue = .text(self.presentationData.strings.Call_StatusEnded) + } + case .error: + statusValue = .text(self.presentationData.strings.Call_StatusFailed) + } + } else { + statusValue = .text(self.presentationData.strings.Call_StatusEnded) + } case .ringing: statusValue = .text(self.presentationData.strings.Call_StatusIncoming) case let .active(timestamp, keyVisualHash): diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index 491eff02c0..94337cbb2b 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -910,8 +910,8 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr UIPasteboard.general.string = text })]) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in - if let resultItemNode = resultItemNode { - return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) + if let strongController = controller, let resultItemNode = resultItemNode { + return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds) } else { return nil } diff --git a/TelegramUI/ChannelVisibilityController.swift b/TelegramUI/ChannelVisibilityController.swift index 3851c65256..e4a81be553 100644 --- a/TelegramUI/ChannelVisibilityController.swift +++ b/TelegramUI/ChannelVisibilityController.swift @@ -887,8 +887,8 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: UIPasteboard.general.string = text })]) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in - if let resultItemNode = resultItemNode { - return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) + if let strongController = controller, let resultItemNode = resultItemNode { + return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds) } else { return nil } diff --git a/TelegramUI/ChatBotInfoItem.swift b/TelegramUI/ChatBotInfoItem.swift index f1dc691e43..5d041af64e 100644 --- a/TelegramUI/ChatBotInfoItem.swift +++ b/TelegramUI/ChatBotInfoItem.swift @@ -175,15 +175,15 @@ final class ChatBotInfoItemNode: ListViewItemNode { func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.frame if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { - if let url = attributes[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] as? String { + if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { return .url(url) - } else if let peerMention = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerMentionAttribute)] as? TelegramPeerMention { + } else if let peerMention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return .peerMention(peerMention.peerId, peerMention.mention) - } else if let peerName = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute)] as? String { + } else if let peerName = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { return .textMention(peerName) - } else if let botCommand = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramBotCommandAttribute)] as? String { + } else if let botCommand = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.BotCommand)] as? String { return .botCommand(botCommand) - } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute)] as? TelegramHashtag { + } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { return .hashtag(hashtag.peerName, hashtag.hashtag) } else { return .none diff --git a/TelegramUI/ChatBotStartInputPanelNode.swift b/TelegramUI/ChatBotStartInputPanelNode.swift index a5dd11d0d3..53fceb98da 100644 --- a/TelegramUI/ChatBotStartInputPanelNode.swift +++ b/TelegramUI/ChatBotStartInputPanelNode.swift @@ -103,4 +103,8 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { return 47.0 } + + override func minimalHeight(interfaceState: ChatPresentationInterfaceState) -> CGFloat { + return 47.0 + } } diff --git a/TelegramUI/ChatButtonKeyboardInputNode.swift b/TelegramUI/ChatButtonKeyboardInputNode.swift index d8e5dd9e81..ad06c67e83 100644 --- a/TelegramUI/ChatButtonKeyboardInputNode.swift +++ b/TelegramUI/ChatButtonKeyboardInputNode.swift @@ -64,7 +64,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, maximumHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))) if self.theme !== interfaceState.theme { @@ -142,9 +142,9 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))) self.scrollNode.view.contentSize = CGSize(width: width, height: rowsHeight) - return panelHeight + return (panelHeight, 0.0) } else { - return 0.0 + return (0.0, 0.0) } } @@ -173,8 +173,8 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { var found = false for attribute in message.attributes { - if let attribute = attribute as? InlineBotMessageAttribute { - botPeer = message.peers[attribute.peerId] + if let attribute = attribute as? InlineBotMessageAttribute, let peerId = attribute.peerId { + botPeer = message.peers[peerId] found = true } } diff --git a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift index 9b094392d9..cc1f25194b 100644 --- a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift +++ b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift @@ -139,4 +139,8 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { return 47.0 } + + override func minimalHeight(interfaceState: ChatPresentationInterfaceState) -> CGFloat { + return 47.0 + } } diff --git a/TelegramUI/ChatContextResultPeekContentNode.swift b/TelegramUI/ChatContextResultPeekContentNode.swift index ba4b5cf8dd..9dd9be393c 100644 --- a/TelegramUI/ChatContextResultPeekContentNode.swift +++ b/TelegramUI/ChatContextResultPeekContentNode.swift @@ -31,6 +31,14 @@ final class ChatContextResultPeekContent: PeekControllerContent { func node() -> PeekControllerContentNode & ASDisplayNode { return ChatContextResultPeekNode(account: self.account, contextResult: self.contextResult) } + + func isEqual(to: PeekControllerContent) -> Bool { + if let to = to as? ChatContextResultPeekContent { + return self.contextResult == to.contextResult + } else { + return false + } + } } private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerContentNode { diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 657d2848da..f449a07fd0 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -12,9 +12,26 @@ public enum ChatControllerPeekActions { case remove(() -> Void) } -public enum ChatControllerPresentationMode { - case standard +public enum ChatControllerPresentationMode: Equatable { + case standard(previewing: Bool) case overlay + + public static func ==(lhs: ChatControllerPresentationMode, rhs: ChatControllerPresentationMode) -> Bool { + switch lhs { + case let .standard(previewing): + if case .standard(previewing) = rhs { + return true + } else { + return false + } + case .overlay: + if case .overlay = rhs { + return true + } else { + return false + } + } + } } public final class ChatControllerOverlayPresentationData { @@ -77,6 +94,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin private var rightNavigationButton: ChatNavigationButton? private var chatInfoNavigationButton: ChatNavigationButton? + private var peerView: PeerView? + private var historyStateDisposable: Disposable? private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() @@ -156,12 +175,14 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin private var applicationInForegroundDisposable: Disposable? + private var checkedPeerChatServiceActions = false + private var raiseToListen: RaiseToListenManager? private weak var silentPostTooltipController: TooltipController? private weak var mediaRecordingModeTooltipController: TooltipController? - public init(account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard) { + public init(account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false)) { self.account = account self.chatLocation = chatLocation self.messageId = messageId @@ -325,20 +346,6 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } strongSelf.chatDisplayNode.displayMessageActionSheet(stableId: updatedMessages[0].stableId, sheetActions: sheetActions, displayContextMenuController: contextMenuController.flatMap { ($0, node, frame) }) - return - - /*if let controllerInteraction = strongSelf.controllerInteraction { - controllerInteraction.highlightedState = ChatInterfaceHighlightedState(messageStableId: updatedMessages[0].stableId) - strongSelf.updateItemNodesHighlightedStates(animated: true) - } - - strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak node] in - if let node = node { - return (node, frame) - } else { - return nil - } - }))*/ } }) } @@ -372,7 +379,15 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { + $0.withUpdatedReplyMessageId(nil) + + }.updatedInputMode { current in + if case let .media(mode, expanded) = current, expanded { + return .media(mode: mode, expanded: false) + } + return current + } }) } }) @@ -383,7 +398,12 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }.updatedInputMode { current in + if case let .media(mode, expanded) = current, expanded { + return .media(mode: mode, expanded: false) + } + return current + } }) } }) @@ -438,6 +458,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin case .none: break case let .alert(text): + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + case let .toast(text): let message: Signal = .single(text) let noMessage: Signal = .single(nil) let delayedNoMessage: Signal = noMessage |> delay(1.0, queue: Queue.mainQueue()) @@ -725,7 +747,12 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin }, openSearch: { }, setupReply: { [weak self] messageId in self?.interfaceInteraction?.setupReplyMessage(messageId) - }, canSetupReply: { [weak self] in + }, canSetupReply: { [weak self] message in + if !message.flags.contains(.Incoming) { + if !message.flags.intersection([.Failed, .Sending, .Unsent]).isEmpty { + return false + } + } if let strongSelf = self { return canReplyInChat(strongSelf.presentationInterfaceState) } @@ -799,6 +826,10 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.chatTitleView?.titleContent = .peer(peerView) (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) } + strongSelf.peerView = peerView + if strongSelf.isNodeLoaded { + strongSelf.chatDisplayNode.peerView = peerView + } var peerIsMuted = false if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { if case .muted = notificationSettings.muteState { @@ -1079,6 +1110,12 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.networkStateDisposable?.dispose() } + public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { + self.updateChatPresentationInterfaceState(animated: false, interactive: false, { + return $0.updatedMode(mode) + }) + } + var chatDisplayNode: ChatControllerNode { get { return super.displayNode as! ChatControllerNode @@ -1094,6 +1131,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin override public func loadDisplayNode() { self.displayNode = ChatControllerNode(account: self.account, chatLocation: self.chatLocation, messageId: self.messageId, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar!) + self.chatDisplayNode.peerView = self.peerView + let initialData = self.chatDisplayNode.historyNode.initialData |> take(1) |> beforeNext { [weak self] combinedInitialData in @@ -2008,7 +2047,15 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) } + $0.updatedInterfaceState { + $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) + + }.updatedInputMode { current in + if case let .media(mode, expanded) = current, expanded { + return .media(mode: mode, expanded: false) + } + return current + } }) } }) @@ -2346,10 +2393,14 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self?.recentlyUsedInlineBotsValue = peers.filter({ $0.1 >= 0.14 }).map({ $0.0 }) }) - if self.raiseToListen == nil { + if case .standard(false) = self.presentationInterfaceState.mode, self.raiseToListen == nil { self.raiseToListen = RaiseToListenManager(shouldActivate: { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded && strongSelf.canReadHistoryValue, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil, strongSelf.playlistStateAndType == nil { if strongSelf.firstLoadedMessageToListen() != nil || strongSelf.chatDisplayNode.isTextInputPanelActive { + if strongSelf.account.telegramApplicationContext.immediateHasOngoingCall { + return false + } + return true } } @@ -2384,6 +2435,13 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.registerForPreviewing(with: self, sourceView: self.chatDisplayNode.historyNodeContainer.view, theme: PeekControllerTheme(presentationTheme: self.presentationData.theme), onlyNative: true) } } + + if !self.checkedPeerChatServiceActions { + self.checkedPeerChatServiceActions = true + if case let .peer(peerId) = self.chatLocation { + let _ = checkPeerChatServiceActions(postbox: self.account.postbox, peerId: peerId).start() + } + } } override public func viewWillDisappear(_ animated: Bool) { @@ -2391,6 +2449,9 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.chatDisplayNode.historyNode.canReadHistory.set(.single(false)) self.saveInterfaceState() + + self.silentPostTooltipController?.dismiss() + self.mediaRecordingModeTooltipController?.dismiss() } private func saveInterfaceState() { @@ -3623,7 +3684,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } } else { let historyPoint = previewingContext.sourceView.convert(location, to: self.chatDisplayNode.historyNode.view) - var result: (Message, Media)? + var result: (Message, ChatMessagePeekPreviewContent)? self.chatDisplayNode.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { if itemNode.frame.contains(historyPoint) { @@ -3633,29 +3694,45 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } } } - if let (message, media) = result { - var selectedTransitionNode: (ASDisplayNode, () -> UIView?)? - self.chatDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView { - if let result = itemNode.transitionNode(id: message.id, media: media) { - selectedTransitionNode = result + if let (message, content) = result { + switch content { + case let .media(media): + var selectedTransitionNode: (ASDisplayNode, () -> UIView?)? + self.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let result = itemNode.transitionNode(id: message.id, media: media) { + selectedTransitionNode = result + } + } } - } - } - - if let selectedTransitionNode = selectedTransitionNode { - if let previewData = chatMessagePreviewControllerData(account: self.account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: self.navigationController as? NavigationController) { - switch previewData { - case let .gallery(gallery): - gallery.setHintWillBePresentedInPreviewingContext(true) - let rect = selectedTransitionNode.0.view.convert(selectedTransitionNode.0.bounds, to: previewingContext.sourceView) - previewingContext.sourceRect = rect.insetBy(dx: -2.0, dy: -2.0) - gallery.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) - return gallery - case let .instantPage(gallery, centralIndex, galleryMedia): - break + + if let selectedTransitionNode = selectedTransitionNode { + if let previewData = chatMessagePreviewControllerData(account: self.account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: self.navigationController as? NavigationController) { + switch previewData { + case let .gallery(gallery): + gallery.setHintWillBePresentedInPreviewingContext(true) + let rect = selectedTransitionNode.0.view.convert(selectedTransitionNode.0.bounds, to: previewingContext.sourceView) + previewingContext.sourceRect = rect.insetBy(dx: -2.0, dy: -2.0) + gallery.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) + return gallery + case let .instantPage(gallery, centralIndex, galleryMedia): + break + } + } + } + case let .url(node, rect, string): + let targetRect = node.view.convert(rect, to: previewingContext.sourceView) + previewingContext.sourceRect = CGRect(origin: CGPoint(x: floor(targetRect.midX), y: floor(targetRect.midY)), size: CGSize(width: 1.0, height: 1.0)) + if let parsedUrl = URL(string: string) { + if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { + let controller = SFSafariViewController(url: parsedUrl) + if #available(iOSApplicationExtension 10.0, *) { + controller.preferredBarTintColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + controller.preferredControlTintColor = self.presentationData.theme.rootController.navigationBar.accentTextColor + } + return controller + } } - } } } } @@ -3669,8 +3746,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin self.present(gallery, in: .window(.root), with: AvatarGalleryControllerPresentationArguments(animated: false, transitionArguments: { _ in return nil })) - } - if let gallery = viewControllerToCommit as? GalleryController { + } else if let gallery = viewControllerToCommit as? GalleryController { self.chatDisplayNode.dismissInput() gallery.setHintWillBePresentedInPreviewingContext(false) @@ -3697,6 +3773,14 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } else if let gallery = viewControllerToCommit as? InstantPageGalleryController { } + + if #available(iOSApplicationExtension 9.0, *) { + if let safariController = viewControllerToCommit as? SFSafariViewController { + if let window = self.navigationController?.view.window { + window.rootViewController?.present(safariController, animated: true) + } + } + } } @available(iOSApplicationExtension 9.0, *) @@ -3922,7 +4006,11 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if options.contains(.deleteLocally) { var localOptionText = self.presentationData.strings.Conversation_DeleteMessagesForMe if case .peer(self.account.peerId) = self.chatLocation { - localOptionText = self.presentationData.strings.Conversation_Moderate_Delete + if messageIds.count == 1 { + localOptionText = self.presentationData.strings.Conversation_Moderate_Delete + } else { + localOptionText = self.presentationData.strings.Conversation_DeleteManyMessages + } } items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 7b2a48c7c1..4eeb56ddbb 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -65,7 +65,7 @@ public final class ChatControllerInteraction { let openCheckoutOrReceipt: (MessageId) -> Void let openSearch: () -> Void let setupReply: (MessageId) -> Void - let canSetupReply: () -> Bool + let canSetupReply: (Message) -> Bool let requestMessageUpdate: (MessageId) -> Void @@ -75,7 +75,7 @@ public final class ChatControllerInteraction { var contextHighlightedState: ChatInterfaceHighlightedState? var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings - public init(openMessage: @escaping (Message) -> Bool, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping () -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { + public init(openMessage: @escaping (Message) -> Bool, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { self.openMessage = openMessage self.openSecretMessagePreview = openSecretMessagePreview self.closeSecretMessagePreview = closeSecretMessagePreview diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index d4d89325dd..2a31a66c78 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -34,6 +34,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var containerNode: ASDisplayNode? private var overlayNavigationBar: ChatOverlayNavigationBar? + var peerView: PeerView? { + didSet { + self.overlayNavigationBar?.peerView = self.peerView + } + } + let backgroundNode: ASDisplayNode let historyNode: ChatHistoryListNode let historyNodeContainer: ASDisplayNode @@ -102,6 +108,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var messageActionSheetTopDimNode: ASDisplayNode? private var messageActionSheetBottomDimNode: ASDisplayNode? + private var expandedInputDimNode: ASDisplayNode? + private var containerLayoutAndNavigationBarHeight: (ContainerViewLayout, CGFloat)? private var scheduledLayoutTransitionRequestId: Int = 0 @@ -333,6 +341,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let overlayNavigationBar = ChatOverlayNavigationBar(theme: self.chatPresentationInterfaceState.theme, close: { [weak self] in self?.dismissAsOverlay() }) + overlayNavigationBar.peerView = self.peerView self.overlayNavigationBar = overlayNavigationBar self.containerNode?.addSubnode(overlayNavigationBar) } @@ -427,9 +436,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.titleAccessoryPanelNode = nil } + var inputPanelNodeBaseHeight: CGFloat = 0.0 + if let inputPanelNode = self.inputPanelNode { + inputPanelNodeBaseHeight = inputPanelNode.minimalHeight(interfaceState: self.chatPresentationInterfaceState) + } + + let maximumInputNodeHeight = layout.size.height - max(navigationBarHeight, layout.safeInsets.top) - inputPanelNodeBaseHeight + var dismissedInputNode: ChatInputNode? var immediatelyLayoutInputNodeAndAnimateAppearance = false - var inputNodeHeight: CGFloat? + var inputNodeHeightAndOverflow: (CGFloat, CGFloat)? if let inputNode = inputNodeForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentNode: self.inputNode, interfaceInteraction: self.interfaceInteraction, inputMediaNode: self.inputMediaNode, controllerInteraction: self.controllerInteraction, inputPanelNode: self.inputPanelNode) { if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { inputTextPanelNode.ensureUnfocused() @@ -449,16 +465,18 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.insertSubnode(inputNode, aboveSubnode: self.inputPanelBackgroundNode) } } - inputNodeHeight = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + inputNodeHeightAndOverflow = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, maximumHeight: maximumInputNodeHeight, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) } else if let inputNode = self.inputNode { dismissedInputNode = inputNode self.inputNode = nil } var insets: UIEdgeInsets - if let inputNodeHeight = inputNodeHeight { + var bottomOverflowOffset: CGFloat = 0.0 + if let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { insets = layout.insets(options: []) - insets.bottom = max(inputNodeHeight, insets.bottom) + insets.bottom = max(inputNodeHeightAndOverflow.0, insets.bottom) + bottomOverflowOffset = inputNodeHeightAndOverflow.1 } else { insets = layout.insets(options: [.input]) } @@ -505,8 +523,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputPanelNode = nil } + if case .standard(true) = self.chatPresentationInterfaceState.mode { + inputPanelSize = CGSize(width: layout.size.width, height: 47.0) + } + if let inputMediaNode = self.inputMediaNode, inputMediaNode != self.inputNode { - let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, maximumHeight: maximumInputNodeHeight, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 56.0))) @@ -533,7 +555,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - let contentBounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width - wrappingInsets.left - wrappingInsets.right, height: layout.size.height - wrappingInsets.top - wrappingInsets.bottom) + let contentBounds = CGRect(x: 0.0, y: -bottomOverflowOffset, width: layout.size.width - wrappingInsets.left - wrappingInsets.right, height: layout.size.height - wrappingInsets.top - wrappingInsets.bottom) if let backgroundEffectNode = self.backgroundEffectNode { transition.updateFrame(node: backgroundEffectNode, frame: CGRect(origin: CGPoint(), size: layout.size)) @@ -542,7 +564,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { transition.updateFrame(node: self.backgroundNode, frame: contentBounds) transition.updateFrame(node: self.historyNodeContainer, frame: contentBounds) transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size)) - transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.midX, y: contentBounds.midY)) + transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0)) self.loadingNode.updateLayout(size: contentBounds.size, insets: insets, transition: transition) transition.updateFrame(node: self.loadingNode, frame: contentBounds) @@ -628,7 +650,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var inputPanelFrame: CGRect? if self.inputPanelNode != nil { assert(inputPanelSize != nil) - inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight - inputPanelSize!.height), size: CGSize(width: layout.size.width, height: inputPanelSize!.height)) + inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight - inputPanelSize!.height), size: CGSize(width: layout.size.width, height: inputPanelSize!.height)) if self.dismissedAsOverlay { inputPanelFrame = inputPanelFrame!.offsetBy(dx: 0.0, dy: inputPanelsHeight + inputPanelSize!.height) } @@ -638,7 +660,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var accessoryPanelFrame: CGRect? if self.accessoryPanelNode != nil { assert(accessoryPanelSize != nil) - accessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight - accessoryPanelSize!.height), size: CGSize(width: layout.size.width, height: accessoryPanelSize!.height)) + accessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomOverflowOffset - insets.bottom - inputPanelsHeight - accessoryPanelSize!.height), size: CGSize(width: layout.size.width, height: accessoryPanelSize!.height)) if self.dismissedAsOverlay { accessoryPanelFrame = accessoryPanelFrame!.offsetBy(dx: 0.0, dy: inputPanelsHeight + accessoryPanelSize!.height) } @@ -656,7 +678,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputBackgroundInset = cleanInsets.bottom } - let inputBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight), size: CGSize(width: layout.size.width, height: inputPanelsHeight + inputBackgroundInset)) + let inputBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight), size: CGSize(width: layout.size.width, height: inputPanelsHeight + inputBackgroundInset)) let additionalScrollDistance: CGFloat = 0.0 var scrollToTop = false @@ -676,9 +698,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var containerInsets = insets if let dismissAsOverlayLayout = self.dismissAsOverlayLayout { - if let inputNodeHeight = inputNodeHeight { + if let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { containerInsets = dismissAsOverlayLayout.insets(options: []) - containerInsets.bottom = max(inputNodeHeight, insets.bottom) + containerInsets.bottom = max(inputNodeHeightAndOverflow.0 + inputNodeHeightAndOverflow.1, insets.bottom) } else { containerInsets = dismissAsOverlayLayout.insets(options: [.input]) } @@ -697,7 +719,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let overlayNavigationBar = self.overlayNavigationBar { let barFrame = CGRect(origin: CGPoint(), size: CGSize(width: contentBounds.size.width, height: 44.0)) transition.updateFrame(node: overlayNavigationBar, frame: barFrame) - overlayNavigationBar.updateLayout(size: barFrame.size, presentationInterfaceState: self.chatPresentationInterfaceState, transition: transition) + overlayNavigationBar.updateLayout(size: barFrame.size, transition: transition) } var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left) @@ -706,31 +728,18 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { listInsets.right += layout.safeInsets.right } + var displayTopDimNode = false + var ensureTopInsetForOverlayHighlightedItems: CGFloat? if let (controller, _) = self.messageActionSheetController { + displayTopDimNode = true + let menuHeight = controller.controllerNode.updateLayout(layout: layout, transition: transition) ensureTopInsetForOverlayHighlightedItems = menuHeight - let topInset = listInsets.bottom + UIScreenPixel - let topFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: max(0.0, topInset))) let bottomInset = containerInsets.bottom + inputPanelsHeight + UIScreenPixel let bottomFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomInset), size: CGSize(width: layout.size.width, height: max(0.0, bottomInset - (layout.inputHeight ?? 0.0)))) - let messageActionSheetTopDimNode: ASDisplayNode - if let current = self.messageActionSheetTopDimNode { - messageActionSheetTopDimNode = current - transition.updateFrame(node: messageActionSheetTopDimNode, frame: topFrame) - } else { - messageActionSheetTopDimNode = ASDisplayNode() - messageActionSheetTopDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - messageActionSheetTopDimNode.alpha = 0.0 - messageActionSheetTopDimNode.isLayerBacked = true - self.messageActionSheetTopDimNode = messageActionSheetTopDimNode - self.addSubnode(messageActionSheetTopDimNode) - transition.updateAlpha(node: messageActionSheetTopDimNode, alpha: 1.0) - messageActionSheetTopDimNode.frame = topFrame - } - let messageActionSheetBottomDimNode: ASDisplayNode if let current = self.messageActionSheetBottomDimNode { messageActionSheetBottomDimNode = current @@ -745,6 +754,73 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { transition.updateAlpha(node: messageActionSheetBottomDimNode, alpha: 1.0) messageActionSheetBottomDimNode.frame = bottomFrame } + } else { + if let messageActionSheetBottomDimNode = self.messageActionSheetBottomDimNode { + self.messageActionSheetBottomDimNode = nil + transition.updateAlpha(node: messageActionSheetBottomDimNode, alpha: 0.0, completion: { [weak messageActionSheetBottomDimNode] _ in + messageActionSheetBottomDimNode?.removeFromSupernode() + }) + } + } + + var expandTopDimNode = false + if case let .media(_, expanded) = self.chatPresentationInterfaceState.inputMode { + if expanded { + displayTopDimNode = true + expandTopDimNode = true + } + } + + if displayTopDimNode { + let topInset = listInsets.bottom + UIScreenPixel + let topFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: max(0.0, topInset))) + + let messageActionSheetTopDimNode: ASDisplayNode + if let current = self.messageActionSheetTopDimNode { + messageActionSheetTopDimNode = current + transition.updateFrame(node: messageActionSheetTopDimNode, frame: topFrame) + } else { + messageActionSheetTopDimNode = ASDisplayNode() + messageActionSheetTopDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + messageActionSheetTopDimNode.alpha = 0.0 + self.messageActionSheetTopDimNode = messageActionSheetTopDimNode + self.addSubnode(messageActionSheetTopDimNode) + transition.updateAlpha(node: messageActionSheetTopDimNode, alpha: 1.0) + messageActionSheetTopDimNode.frame = topFrame + messageActionSheetTopDimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.topDimNodeTapGesture(_:)))) + } + + let inputPanelOrigin = layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight + + if expandTopDimNode { + let exandedFrame = CGRect(origin: CGPoint(x: 0.0, y: inputPanelOrigin - layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height)) + let expandedInputDimNode: ASDisplayNode + if let current = self.expandedInputDimNode { + expandedInputDimNode = current + transition.updateFrame(node: expandedInputDimNode, frame: exandedFrame) + } else { + expandedInputDimNode = ASDisplayNode() + expandedInputDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + expandedInputDimNode.alpha = 0.0 + self.expandedInputDimNode = expandedInputDimNode + if let inputNode = self.inputNode, inputNode.supernode != nil { + self.insertSubnode(expandedInputDimNode, belowSubnode: inputNode) + } else { + self.addSubnode(expandedInputDimNode) + } + transition.updateAlpha(node: expandedInputDimNode, alpha: 1.0) + expandedInputDimNode.frame = exandedFrame + transition.animatePositionAdditive(node: expandedInputDimNode, offset: previousInputPanelOrigin.y - inputPanelOrigin) + } + } else { + if let expandedInputDimNode = self.expandedInputDimNode { + self.expandedInputDimNode = nil + transition.animatePositionAdditive(node: expandedInputDimNode, offset: previousInputPanelOrigin.y - inputPanelOrigin) + transition.updateAlpha(node: expandedInputDimNode, alpha: 0.0, completion: { [weak expandedInputDimNode] _ in + expandedInputDimNode?.removeFromSupernode() + }) + } + } } else { if let messageActionSheetTopDimNode = self.messageActionSheetTopDimNode { self.messageActionSheetTopDimNode = nil @@ -752,10 +828,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { messageActionSheetTopDimNode?.removeFromSupernode() }) } - if let messageActionSheetBottomDimNode = self.messageActionSheetBottomDimNode { - self.messageActionSheetBottomDimNode = nil - transition.updateAlpha(node: messageActionSheetBottomDimNode, alpha: 0.0, completion: { [weak messageActionSheetBottomDimNode] _ in - messageActionSheetBottomDimNode?.removeFromSupernode() + + if let expandedInputDimNode = self.expandedInputDimNode { + self.expandedInputDimNode = nil + let inputPanelOrigin = layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight + let exandedFrame = CGRect(origin: CGPoint(x: 0.0, y: inputPanelOrigin - layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height)) + transition.updateFrame(node: expandedInputDimNode, frame: exandedFrame) + transition.updateAlpha(node: expandedInputDimNode, alpha: 0.0, completion: { [weak expandedInputDimNode] _ in + expandedInputDimNode?.removeFromSupernode() }) } } @@ -769,7 +849,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let navigateButtonsSize = self.navigateButtons.updateLayout(transition: transition) var navigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - navigateButtonsSize.width - 6.0, y: layout.size.height - containerInsets.bottom - inputPanelsHeight - navigateButtonsSize.height - 6.0), size: navigateButtonsSize) if case .overlay = self.chatPresentationInterfaceState.mode { - navigateButtonsFrame = navigateButtonsFrame.offsetBy(dx: -8.0, dy: -8.0) + navigateButtonsFrame = navigateButtonsFrame.offsetBy(dx: -8.0, dy: -8.0 - bottomOverflowOffset) } transition.updateFrame(node: self.inputPanelBackgroundNode, frame: inputBackgroundFrame) @@ -830,7 +910,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - if let inputNode = self.inputNode, let inputNodeHeight = inputNodeHeight { + if let inputNode = self.inputNode, let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { + let inputNodeHeight = inputNodeHeightAndOverflow.0 + inputNodeHeightAndOverflow.1 let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - inputNodeHeight), size: CGSize(width: layout.size.width, height: inputNodeHeight)) if immediatelyLayoutInputNodeAndAnimateAppearance { var adjustedForPreviousInputHeightFrame = inputNodeFrame @@ -1149,11 +1230,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let inputNode = ChatMediaInputNode(account: self.account, controllerInteraction: self.controllerInteraction, theme: theme, strings: strings, gifPaneIsActiveUpdated: { [weak self] value in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId { state in - if case .media = state.inputMode { + if case let .media(_, expanded) = state.inputMode { if value { - return (.media(.gif), nil) + return (.media(mode: .gif, expanded: expanded), nil) } else { - return (.media(.other), nil) + return (.media(mode: .other, expanded: expanded), nil) } } else { return (state.inputMode, nil) @@ -1164,7 +1245,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputNode.interfaceInteraction = interfaceInteraction self.inputMediaNode = inputNode if let (validLayout, _) = self.validLayout { - let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, maximumHeight: validLayout.standardInputHeight, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } } } @@ -1390,11 +1471,17 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if animateIn, let controller = self.messageActionSheetController?.0 { controller.controllerNode.animateIn(transition: transition) } - if let _ = menuHeight { + if let menuHeight = menuHeight { if let _ = self.controllerInteraction.contextHighlightedState?.messageStableId, let (menuController, node, frame) = displayContextMenuController { - self.controllerInteraction.presentController(menuController, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { - return (node, frame) + self.controllerInteraction.presentController(menuController, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + if let strongSelf = self { + var bounds = strongSelf.bounds + bounds.size.height -= menuHeight + return (node, frame, strongSelf, bounds) + } else { + return nil + } })) } } @@ -1409,4 +1496,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { return nil } + + @objc func topDimNodeTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { state in + if case let .media(mode, true) = state.inputMode { + return (.media(mode: mode, expanded: false), nil) + } else { + return (state.inputMode, nil) + } + } + } + } } diff --git a/TelegramUI/ChatFeedNavigationInputPanelNode.swift b/TelegramUI/ChatFeedNavigationInputPanelNode.swift index 0d76bb6501..4ab968c213 100644 --- a/TelegramUI/ChatFeedNavigationInputPanelNode.swift +++ b/TelegramUI/ChatFeedNavigationInputPanelNode.swift @@ -64,5 +64,9 @@ final class ChatFeedNavigationInputPanelNode: ChatInputPanelNode { return 47.0 } + + override func minimalHeight(interfaceState: ChatPresentationInterfaceState) -> CGFloat { + return 47.0 + } } diff --git a/TelegramUI/ChatInputNode.swift b/TelegramUI/ChatInputNode.swift index f78dd429fd..b532c15a0e 100644 --- a/TelegramUI/ChatInputNode.swift +++ b/TelegramUI/ChatInputNode.swift @@ -5,7 +5,7 @@ import AsyncDisplayKit class ChatInputNode: ASDisplayNode { var interfaceInteraction: ChatPanelInterfaceInteraction? - func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { - return 0.0 + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, maximumHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { + return (0.0, 0.0) } } diff --git a/TelegramUI/ChatInputPanelNode.swift b/TelegramUI/ChatInputPanelNode.swift index 23ffb8fb2a..ddf091229c 100644 --- a/TelegramUI/ChatInputPanelNode.swift +++ b/TelegramUI/ChatInputPanelNode.swift @@ -11,4 +11,8 @@ class ChatInputPanelNode: ASDisplayNode { func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { return 0.0 } + + func minimalHeight(interfaceState: ChatPresentationInterfaceState) -> CGFloat { + return 0.0 + } } diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index de22000351..9c5f951733 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -182,7 +182,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte } switch chatPresentationInterfaceState.inputMode { case .media: - if contextPlaceholder == nil && chatPresentationInterfaceState.interfaceState.editMessage == nil && chatPresentationInterfaceState.interfaceState.composeInputState.inputText.length == 0 && chatPresentationInterfaceState.inputMode == .media(.gif) { + if contextPlaceholder == nil && chatPresentationInterfaceState.interfaceState.editMessage == nil && chatPresentationInterfaceState.interfaceState.composeInputState.inputText.length == 0, case .media(.gif, _) = chatPresentationInterfaceState.inputMode { let baseFontSize: CGFloat = max(17.0, chatPresentationInterfaceState.fontSize.baseDisplaySize) contextPlaceholder = NSAttributedString(string: "@gif", font: Font.regular(baseFontSize), textColor: chatPresentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor) diff --git a/TelegramUI/ChatInterfaceInputNodes.swift b/TelegramUI/ChatInterfaceInputNodes.swift index 3f7f721992..a9bf4c2826 100644 --- a/TelegramUI/ChatInterfaceInputNodes.swift +++ b/TelegramUI/ChatInterfaceInputNodes.swift @@ -16,11 +16,11 @@ func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: let inputNode = ChatMediaInputNode(account: account, controllerInteraction: controllerInteraction, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, gifPaneIsActiveUpdated: { [weak interfaceInteraction] value in if let interfaceInteraction = interfaceInteraction { interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId { state in - if case .media = state.inputMode { + if case let .media(_, expanded) = state.inputMode { if value { - return (.media(.gif), nil) + return (.media(mode: .gif, expanded: expanded), nil) } else { - return (.media(.other), nil) + return (.media(mode: .other, expanded: expanded), nil) } } else { return (state.inputMode, nil) diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index 1ae6eedf5c..97066f0320 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -356,7 +356,9 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag banPeer = nil } } - optionsMap[id]!.insert(.forward) + if message.id.peerId.namespace != Namespaces.Peer.SecretChat { + optionsMap[id]!.insert(.forward) + } if !message.flags.contains(.Incoming) { optionsMap[id]!.insert(.deleteGlobally) } else { @@ -365,7 +367,9 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag } } } else if let group = peer as? TelegramGroup { - optionsMap[id]!.insert(.forward) + if message.id.peerId.namespace != Namespaces.Peer.SecretChat { + optionsMap[id]!.insert(.forward) + } optionsMap[id]!.insert(.deleteLocally) if !message.flags.contains(.Incoming) { optionsMap[id]!.insert(.deleteGlobally) @@ -378,7 +382,9 @@ func chatAvailableMessageActions(postbox: Postbox, accountPeerId: PeerId, messag } } } else if let _ = peer as? TelegramUser { - optionsMap[id]!.insert(.forward) + if message.id.peerId.namespace != Namespaces.Peer.SecretChat { + optionsMap[id]!.insert(.forward) + } optionsMap[id]!.insert(.deleteLocally) if !message.flags.contains(.Incoming) { optionsMap[id]!.insert(.deleteGlobally) diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index d7237998d0..646278bb4e 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -404,14 +404,14 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD if let searchController = self.chatListDisplayNode.searchDisplayController { if let (view, action) = searchController.previewViewAndActionAtLocation(location) { - if let peerId = action as? PeerId { + if let peerId = action as? PeerId, peerId.namespace != Namespaces.Peer.SecretChat { if #available(iOSApplicationExtension 9.0, *) { var sourceRect = view.superview!.convert(view.frame, to: self.view) sourceRect.size.height -= UIScreenPixel previewingContext.sourceRect = sourceRect } - let chatController = ChatController(account: self.account, chatLocation: .peer(peerId)) + let chatController = ChatController(account: self.account, chatLocation: .peer(peerId), mode: .standard(previewing: true)) chatController.peekActions = .remove({ [weak self] in if let strongSelf = self { let _ = removeRecentPeer(account: strongSelf.account, peerId: peerId).start() @@ -420,18 +420,18 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } }) chatController.canReadHistory.set(false) - chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) + chatController.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) return chatController - } else if let messageId = action as? MessageId { + } else if let messageId = action as? MessageId, messageId.peerId.namespace != Namespaces.Peer.SecretChat { if #available(iOSApplicationExtension 9.0, *) { var sourceRect = view.superview!.convert(view.frame, to: self.view) sourceRect.size.height -= UIScreenPixel previewingContext.sourceRect = sourceRect } - let chatController = ChatController(account: self.account, chatLocation: .peer(messageId.peerId), messageId: messageId) + let chatController = ChatController(account: self.account, chatLocation: .peer(messageId.peerId), messageId: messageId, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) + chatController.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) return chatController } } @@ -464,10 +464,14 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } switch item.content { case let .peer(_, peer, _, _, _, _, _): - let chatController = ChatController(account: self.account, chatLocation: .peer(peer.peerId)) - chatController.canReadHistory.set(false) - chatController.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) - return chatController + if peer.peerId.namespace != Namespaces.Peer.SecretChat { + let chatController = ChatController(account: self.account, chatLocation: .peer(peer.peerId), mode: .standard(previewing: true)) + chatController.canReadHistory.set(false) + chatController.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) + return chatController + } else { + return nil + } case let .groupReference(groupId, _, _, _): let chatListController = ChatListController(account: self.account, groupId: groupId, controlsHistoryPreload: false) chatListController.containerLayoutUpdated(ContainerViewLayout(size: contentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) @@ -482,6 +486,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD if let viewControllerToCommit = viewControllerToCommit as? ViewController { if let chatController = viewControllerToCommit as? ChatController { chatController.canReadHistory.set(true) + chatController.updatePresentationMode(.standard(previewing: false)) } (self.navigationController as? NavigationController)?.pushViewController(viewControllerToCommit, animated: false) } diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 45a6efb66c..1f4911da59 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -830,7 +830,28 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.mentionBadgeNode.isHidden = true } - var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleLayout.size.width + 3.0 + var titleOffset: CGFloat = 0.0 + if let currentSecretIconImage = currentSecretIconImage { + let iconNode: ASImageNode + if let current = strongSelf.secretIconNode { + iconNode = current + } else { + iconNode = ASImageNode() + iconNode.isLayerBacked = true + iconNode.displaysAsynchronously = false + iconNode.displayWithoutProcessing = true + strongSelf.addSubnode(iconNode) + strongSelf.secretIconNode = iconNode + } + iconNode.image = currentSecretIconImage + transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y + 4.0), size: currentSecretIconImage.size)) + titleOffset += currentSecretIconImage.size.width + 3.0 + } else if let secretIconNode = strongSelf.secretIconNode { + strongSelf.secretIconNode = nil + secretIconNode.removeFromSupernode() + } + + var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleLayout.size.width + 3.0 + titleOffset if let currentVerificationIconImage = currentVerificationIconImage { let iconNode: ASImageNode @@ -862,27 +883,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.mutedIconNode.isHidden = true } - var titleOffset: CGFloat = 0.0 - if let currentSecretIconImage = currentSecretIconImage { - let iconNode: ASImageNode - if let current = strongSelf.secretIconNode { - iconNode = current - } else { - iconNode = ASImageNode() - iconNode.isLayerBacked = true - iconNode.displaysAsynchronously = false - iconNode.displayWithoutProcessing = true - strongSelf.addSubnode(iconNode) - strongSelf.secretIconNode = iconNode - } - iconNode.image = currentSecretIconImage - transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y + 4.0), size: currentSecretIconImage.size)) - titleOffset += currentSecretIconImage.size.width + 3.0 - } else if let secretIconNode = strongSelf.secretIconNode { - strongSelf.secretIconNode = nil - secretIconNode.removeFromSupernode() - } - let contentDeltaX = contentRect.origin.x - (strongSelf.titleNode.frame.minX - titleOffset) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size) let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height), size: authorLayout.size) @@ -1048,7 +1048,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let statusFrame = self.statusNode.frame transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width - 2.0 - statusFrame.size.width, y: statusFrame.minY), size: statusFrame.size)) - var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleFrame.size.width + 3.0 + var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleFrame.size.width + 3.0 + titleOffset if let verificationIconNode = self.verificationIconNode { transition.updateFrame(node: verificationIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: verificationIconNode.frame.origin.y), size: verificationIconNode.bounds.size)) diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index 911b973f65..823a1f570b 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -741,8 +741,8 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { return (selectedItemNode.view, peer.id) } else if let selectedItemNode = selectedItemNode as? ChatListItemNode, let item = selectedItemNode.item { switch item.content { - case let .peer(_, peer, _, _, _, _, _): - return (selectedItemNode.view, peer.peerId) + case let .peer(message, peer, _, _, _, _, _): + return (selectedItemNode.view, message?.id ?? peer.peerId) case let .groupReference(groupId, _, _, _): return (selectedItemNode.view, groupId) } diff --git a/TelegramUI/ChatMediaInputGifPane.swift b/TelegramUI/ChatMediaInputGifPane.swift index e4e9ffa2e2..840a9be03e 100644 --- a/TelegramUI/ChatMediaInputGifPane.swift +++ b/TelegramUI/ChatMediaInputGifPane.swift @@ -28,8 +28,12 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = size - self.multiplexedNode?.bottomInset = bottomInset - self.multiplexedNode?.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: size.height - topInset)) + if let multiplexedNode = self.multiplexedNode { + multiplexedNode.bottomInset = bottomInset + let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: size.height - topInset)) + transition.updateFrame(layer: multiplexedNode.layer, frame: nodeFrame) + multiplexedNode.updateLayout(size: nodeFrame.size, transition: transition) + } } func fileAt(point: CGPoint) -> TelegramMediaFile? { diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index 266b7fa082..50fc73333a 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -273,9 +273,7 @@ final class ChatMediaInputNode: ChatInputNode { private var currentStickerPacksCollectionPosition: StickerPacksCollectionPosition? private var currentView: ItemCollectionsView? - private var stickerPreviewController: StickerPreviewController? - - private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState)? + private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState)? private var paneArrangement: ChatMediaInputPaneArrangement private var theme: PresentationTheme @@ -576,6 +574,14 @@ final class ChatMediaInputNode: ChatInputNode { return controller } return nil + }, updateContent: { [weak self] content in + if let strongSelf = self { + var item: StickerPackItem? + if let content = content as? StickerPreviewPeekContent { + item = content.item + } + strongSelf.updatePreviewingItem(item: item, animated: true) + } })) self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) } @@ -584,8 +590,8 @@ final class ChatMediaInputNode: ChatInputNode { if let index = self.paneArrangement.panes.index(of: pane), index != self.paneArrangement.currentIndex { let previousGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs self.paneArrangement = self.paneArrangement.withIndexTransition(0.0).withCurrentIndex(index) - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, interfaceState) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, interfaceState) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) } let updatedGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs if updatedGifPanelWasActive != previousGifPanelWasActive { @@ -602,8 +608,8 @@ final class ChatMediaInputNode: ChatInputNode { self.setHighlightedItemCollectionId(ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue, id: 0)) } } else { - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, interfaceState) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, interfaceState) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) } } } @@ -684,15 +690,20 @@ final class ChatMediaInputNode: ChatInputNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { - self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, interfaceState) + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, maximumHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { + self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, interfaceState) if self.theme !== interfaceState.theme || self.strings !== interfaceState.strings { self.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings) } let separatorHeight = UIScreenPixel - let panelHeight = standardInputHeight + let panelHeight: CGFloat + if case .media(_, true) = interfaceState.inputMode { + panelHeight = maximumHeight + } else { + panelHeight = standardInputHeight + } transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: self.collectionListPanelOffset), size: CGSize(width: width, height: 41.0))) transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + self.collectionListPanelOffset), size: CGSize(width: width, height: separatorHeight))) @@ -849,7 +860,7 @@ final class ChatMediaInputNode: ChatInputNode { self.animatingTrendingPaneOut = false } - return panelHeight + return (standardInputHeight, max(0.0, panelHeight - standardInputHeight)) } private func enqueuePanelTransition(_ transition: ChatMediaInputPanelTransition, firstTime: Bool, thenGridTransition gridTransition: ChatMediaInputGridTransition, gridFirstTime: Bool) { @@ -875,25 +886,6 @@ final class ChatMediaInputNode: ChatInputNode { self.stickerPane.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: itemTransition, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in }) } - @objc func previewGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - switch recognizer.state { - case .began: - if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture { - if let itemNode = self.stickerPane.gridNode.itemNodeAtPoint(location) as? ChatMediaInputStickerGridItemNode { - self.updatePreviewingItem(item: itemNode.stickerPackItem, animated: true) - } - } - case .ended, .cancelled: - self.updatePreviewingItem(item: nil, animated: true) - case .changed: - if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture, let itemNode = self.stickerPane.gridNode.itemNodeAtPoint(location) as? ChatMediaInputStickerGridItemNode { - self.updatePreviewingItem(item: itemNode.stickerPackItem, animated: true) - } - default: - break - } - } - private func updatePreviewingItem(item: StickerPackItem?, animated: Bool) { if self.inputNodeInteraction.previewedStickerPackItem != item { self.inputNodeInteraction.previewedStickerPackItem = item @@ -903,30 +895,6 @@ final class ChatMediaInputNode: ChatInputNode { itemNode.updatePreviewing(animated: animated) } } - - if let item = item { - if let stickerPreviewController = self.stickerPreviewController { - stickerPreviewController.updateItem(item) - } else { - let stickerPreviewController = StickerPreviewController(account: self.account, item: item) - self.stickerPreviewController = stickerPreviewController - self.controllerInteraction.presentController(stickerPreviewController, StickerPreviewControllerPresentationArguments(transitionNode: { [weak self] item in - if let strongSelf = self { - var result: ASDisplayNode? - strongSelf.stickerPane.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMediaInputStickerGridItemNode, itemNode.stickerPackItem == item { - result = itemNode.transitionNode() - } - } - return result - } - return nil - })) - } - } else if let stickerPreviewController = self.stickerPreviewController { - stickerPreviewController.dismiss() - self.stickerPreviewController = nil - } } } @@ -935,7 +903,7 @@ final class ChatMediaInputNode: ChatInputNode { case .began: break case .changed: - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, interfaceState) = self.validLayout { + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, interfaceState) = self.validLayout { let translationX = -recognizer.translation(in: self.view).x var indexTransition = translationX / width if self.paneArrangement.currentIndex == 0 { @@ -944,10 +912,10 @@ final class ChatMediaInputNode: ChatInputNode { indexTransition = min(0.0, indexTransition) } self.paneArrangement = self.paneArrangement.withIndexTransition(indexTransition) - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, transition: .immediate, interfaceState: interfaceState) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, transition: .immediate, interfaceState: interfaceState) } case .ended: - if let (width, _, _, _, _, _) = self.validLayout { + if let (width, _, _, _, _, _, _) = self.validLayout { var updatedIndex = self.paneArrangement.currentIndex if abs(self.paneArrangement.indexTransition * width) > 30.0 { if self.paneArrangement.indexTransition < 0.0 { @@ -960,9 +928,9 @@ final class ChatMediaInputNode: ChatInputNode { self.setCurrentPane(self.paneArrangement.panes[updatedIndex], transition: .animated(duration: 0.25, curve: .spring)) } case .cancelled: - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, interfaceState) = self.validLayout { + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, interfaceState) = self.validLayout { self.paneArrangement = self.paneArrangement.withIndexTransition(0.0) - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) } default: break diff --git a/TelegramUI/ChatMediaInputStickerPane.swift b/TelegramUI/ChatMediaInputStickerPane.swift index 39e37e9567..3b615c6ae8 100644 --- a/TelegramUI/ChatMediaInputStickerPane.swift +++ b/TelegramUI/ChatMediaInputStickerPane.swift @@ -41,9 +41,9 @@ final class ChatMediaInputStickerPane: ChatMediaInputPane { } override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - self.gridNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) + transition.updateFrame(node: self.gridNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) } func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? { diff --git a/TelegramUI/ChatMediaInputTrendingPane.swift b/TelegramUI/ChatMediaInputTrendingPane.swift index a0a17b11f8..7f67687e31 100644 --- a/TelegramUI/ChatMediaInputTrendingPane.swift +++ b/TelegramUI/ChatMediaInputTrendingPane.swift @@ -123,7 +123,22 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { let interaction = TrendingPaneInteraction(installPack: { [weak self] info in if let strongSelf = self, let info = info as? StickerPackCollectionInfo { - strongSelf.controllerInteraction.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash)), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + let _ = (loadedStickerPack(postbox: strongSelf.account.postbox, network: strongSelf.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash)) + |> mapToSignal { result -> Signal in + switch result { + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return addStickerPackInteractively(postbox: strongSelf.account.postbox, info: info, items: items) + } + case .fetching: + break + case .none: + break + } + return .complete() + }).start() } }, openPack: { [weak self] info in if let strongSelf = self, let info = info as? StickerPackCollectionInfo { @@ -156,8 +171,25 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { let hadValidLayout = self.validLayout != nil self.validLayout = (size, bottomInset) - self.listNode.frame = CGRect(origin: CGPoint(), size: size) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) + + var duration: Double = 0.0 + var listViewCurve: ListViewAnimationCurve = .Default + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + listViewCurve = .Default + case .spring: + listViewCurve = .Spring(duration: duration) + } + } + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index bee674bf67..04dd2efa94 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -9,7 +9,7 @@ private let titleFont = Font.regular(13.0) private let titleBoldFont = Font.bold(13.0) private func peerMentionAttributes(primaryTextColor: UIColor, peerId: PeerId) -> MarkdownAttributeSet { - return MarkdownAttributeSet(font: titleBoldFont, textColor: primaryTextColor, additionalAttributes: [TextNode.TelegramPeerMentionAttribute: TelegramPeerMention(peerId: peerId, mention: "")]) + return MarkdownAttributeSet(font: titleBoldFont, textColor: primaryTextColor, additionalAttributes: [TelegramTextAttributes.PeerMention: TelegramPeerMention(peerId: peerId, mention: "")]) } private func peerMentionsAttributes(primaryTextColor: UIColor, peerIds: [(Int, PeerId?)]) -> [Int: MarkdownAttributeSet] { @@ -611,11 +611,11 @@ class ChatMessageActionItemNode: ChatMessageItemView { 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 + TelegramTextAttributes.Url, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag ] for name in possibleNames { if let _ = attributes[NSAttributedStringKey(rawValue: name)] { @@ -658,15 +658,15 @@ class ChatMessageActionItemNode: ChatMessageItemView { 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[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] as? String { + if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { return .url(url) - } else if let peerMention = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerMentionAttribute)] as? TelegramPeerMention { + } else if let peerMention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return .peerMention(peerMention.peerId, peerMention.mention) - } else if let peerName = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute)] as? String { + } else if let peerName = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { return .textMention(peerName) - } else if let botCommand = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramBotCommandAttribute)] as? String { + } else if let botCommand = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.BotCommand)] as? String { return .botCommand(botCommand) - } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute)] as? TelegramHashtag { + } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { return .hashtag(hashtag.peerName, hashtag.hashtag) } else { return .none diff --git a/TelegramUI/ChatMessageActionSheetControllerNode.swift b/TelegramUI/ChatMessageActionSheetControllerNode.swift index 3bd8bac4a6..6eee3c37c4 100644 --- a/TelegramUI/ChatMessageActionSheetControllerNode.swift +++ b/TelegramUI/ChatMessageActionSheetControllerNode.swift @@ -2,6 +2,17 @@ import Foundation import Display import AsyncDisplayKit +private let shadowInset: CGFloat = 8.0 + +private func generateShadowImage(theme: PresentationTheme) -> UIImage? { + return generateImage(CGSize(width: 32.0 + shadowInset * 2.0, height: 32.0 + shadowInset * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 20.0, color: UIColor(white: 0.0, alpha: 0.2).cgColor) + context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowInset, y: shadowInset), size: CGSize(width: size.width - shadowInset * 2.0, height: size.height - shadowInset * 2.0))) + })?.stretchableImage(withLeftCapWidth: 16 + Int(shadowInset) / 2, topCapHeight: 16 + Int(shadowInset) / 2) +} + private final class MessageActionButtonNode: HighlightableButtonNode { let theme: PresentationTheme let separatorNode: ASDisplayNode @@ -15,8 +26,8 @@ private final class MessageActionButtonNode: HighlightableButtonNode { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = theme.actionSheet.opaqueItemHighlightedBackgroundColor self.backgroundNode.alpha = 0.0 + self.backgroundNode.backgroundColor = theme.actionSheet.opaqueItemHighlightedBackgroundColor super.init() @@ -53,6 +64,7 @@ final class ChatMessageActionSheetControllerNode: ViewControllerTracingNode { private let theme: PresentationTheme private let inputDimNode: ASDisplayNode + private let itemsShadowNode: ASImageNode private let itemsContainerNode: ASDisplayNode private let actions: [ChatMessageContextMenuSheetAction] @@ -70,6 +82,12 @@ final class ChatMessageActionSheetControllerNode: ViewControllerTracingNode { self.inputDimNode = ASDisplayNode() self.inputDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.itemsShadowNode = ASImageNode() + self.itemsShadowNode.isLayerBacked = true + self.itemsShadowNode.displayWithoutProcessing = true + self.itemsShadowNode.displaysAsynchronously = false + self.itemsShadowNode.image = generateShadowImage(theme: theme) + self.itemsContainerNode = ASDisplayNode() self.itemsContainerNode.backgroundColor = theme.actionSheet.opaqueItemBackgroundColor self.itemsContainerNode.cornerRadius = 16.0 @@ -84,6 +102,7 @@ final class ChatMessageActionSheetControllerNode: ViewControllerTracingNode { super.init() self.addSubnode(self.inputDimNode) + self.addSubnode(self.itemsShadowNode) self.addSubnode(self.itemsContainerNode) for actionNode in actionNodes { @@ -104,15 +123,18 @@ final class ChatMessageActionSheetControllerNode: ViewControllerTracingNode { self.inputDimNode.alpha = 0.0 transition.updateAlpha(node: self.inputDimNode, alpha: 1.0) transition.animatePositionAdditive(node: self.itemsContainerNode, offset: self.bounds.size.height) + transition.animatePositionAdditive(node: self.itemsShadowNode, offset: self.bounds.size.height) self.feedback.impact() } func animateOut(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { transition.updateAlpha(node: self.inputDimNode, alpha: 0.0) - transition.updatePosition(node: self.itemsContainerNode, position: CGPoint(x: self.itemsContainerNode.position.x, y: self.bounds.size.height + self.itemsContainerNode.bounds.height), completion: { _ in + let position = CGPoint(x: self.itemsContainerNode.position.x, y: self.bounds.size.height + self.itemsContainerNode.bounds.height) + transition.updatePosition(node: self.itemsContainerNode, position: position, completion: { _ in completion() }) + transition.updatePosition(node: self.itemsShadowNode, position: position) } func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { @@ -132,7 +154,9 @@ final class ChatMessageActionSheetControllerNode: ViewControllerTracingNode { itemsHeight += actionNode.bounds.height } - transition.updateFrame(node: self.itemsContainerNode, frame: CGRect(origin: CGPoint(x: 14.0, y: layout.size.height - height - itemsHeight), size: CGSize(width: layout.size.width - 14.0 * 2.0, height: itemsHeight))) + let containerFrame = CGRect(origin: CGPoint(x: 14.0, y: layout.size.height - height - itemsHeight), size: CGSize(width: layout.size.width - 14.0 * 2.0, height: itemsHeight)) + transition.updateFrame(node: self.itemsContainerNode, frame: containerFrame) + transition.updateFrame(node: self.itemsShadowNode, frame: containerFrame.insetBy(dx: -shadowInset, dy: -shadowInset)) height += itemsHeight diff --git a/TelegramUI/ChatMessageAttachedContentNode.swift b/TelegramUI/ChatMessageAttachedContentNode.swift index 348d29f09e..45512ab754 100644 --- a/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/TelegramUI/ChatMessageAttachedContentNode.swift @@ -862,15 +862,15 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.frame if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { - if let url = attributes[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] as? String { + if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { return .url(url) - } else if let peerMention = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerMentionAttribute)] as? TelegramPeerMention { + } else if let peerMention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return .peerMention(peerMention.peerId, peerMention.mention) - } else if let peerName = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute)] as? String { + } else if let peerName = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { return .textMention(peerName) - } else if let botCommand = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramBotCommandAttribute)] as? String { + } else if let botCommand = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.BotCommand)] as? String { return .botCommand(botCommand) - } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute)] as? TelegramHashtag { + } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { return .hashtag(hashtag.peerName, hashtag.hashtag) } else { return .none @@ -887,11 +887,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { let textNodeFrame = self.textNode.frame if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { let possibleNames: [String] = [ - TextNode.UrlAttribute, - TextNode.TelegramPeerMentionAttribute, - TextNode.TelegramPeerTextMentionAttribute, - TextNode.TelegramBotCommandAttribute, - TextNode.TelegramHashtagAttribute + TelegramTextAttributes.Url, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag ] for name in possibleNames { if let _ = attributes[NSAttributedStringKey(rawValue: name)] { diff --git a/TelegramUI/ChatMessageBackground.swift b/TelegramUI/ChatMessageBackground.swift index ff26074d8c..621238350b 100644 --- a/TelegramUI/ChatMessageBackground.swift +++ b/TelegramUI/ChatMessageBackground.swift @@ -11,7 +11,11 @@ enum ChatMessageBackgroundMergeType { } else if top { self = .Top } else if bottom { - self = .Bottom + if side { + self = .Side + } else { + self = .Bottom + } } else { if side { self = .Side diff --git a/TelegramUI/ChatMessageBubbleContentNode.swift b/TelegramUI/ChatMessageBubbleContentNode.swift index 8306dfc8b2..35cd0d219a 100644 --- a/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageBubbleContentNode.swift @@ -118,7 +118,7 @@ class ChatMessageBubbleContentNode: ASDisplayNode { return nil } - func peekPreviewContent(at point: CGPoint) -> (Message, Media)? { + func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { return nil } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 397b7766c8..1348560bb1 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -247,7 +247,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if strongSelf.selectionNode != nil { return false } - return item.controllerInteraction.canSetupReply() + return item.controllerInteraction.canSetupReply(item.message) } return false } @@ -416,8 +416,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var authorNameColor: UIColor? for attribute in firstMessage.attributes { - if let attribute = attribute as? InlineBotMessageAttribute, let bot = firstMessage.peers[attribute.peerId] as? TelegramUser { - inlineBotNameString = bot.username + if let attribute = attribute as? InlineBotMessageAttribute { + if let peerId = attribute.peerId, let bot = firstMessage.peers[peerId] as? TelegramUser { + inlineBotNameString = bot.username + } else { + inlineBotNameString = attribute.title + } } else if let attribute = attribute as? ReplyMessageAttribute { replyMessage = firstMessage.associatedMessages[attribute.messageId] } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty { @@ -982,14 +986,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme) - var udpdatedMergedTop = mergedBottom - var udpdatedMergedBottom = mergedTop + var updatedMergedTop = mergedBottom + var updatedMergedBottom = mergedTop if mosaicRange == nil { if contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false { - udpdatedMergedTop = .semanticallyMerged + updatedMergedTop = .semanticallyMerged } if headerSize.height.isZero && contentNodePropertiesAndFinalize.first?.0.forceFullCorners ?? false { - udpdatedMergedBottom = .none + updatedMergedBottom = .none } } @@ -1005,10 +1009,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var forceBackgroundSide = false if actionButtonsSizeAndApply != nil { forceBackgroundSide = true - } else if case .semanticallyMerged = udpdatedMergedTop { + } else if case .semanticallyMerged = updatedMergedTop { forceBackgroundSide = true } - let mergeType = ChatMessageBackgroundMergeType(top: udpdatedMergedTop == .fullyMerged, bottom: udpdatedMergedBottom == .fullyMerged, side: forceBackgroundSide) + let mergeType = ChatMessageBackgroundMergeType(top: updatedMergedTop == .fullyMerged, bottom: updatedMergedBottom == .fullyMerged, side: forceBackgroundSide) let backgroundType: ChatMessageBackgroundType if hideBackground { backgroundType = .none @@ -1329,9 +1333,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let nameNode = self.nameNode, nameNode.frame.contains(location) { if let item = self.item { for attribute in item.message.attributes { - if let attribute = attribute as? InlineBotMessageAttribute, let botPeer = item.message.peers[attribute.peerId], let addressName = botPeer.addressName { - item.controllerInteraction.updateInputState { textInputState in - return ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " ")) + if let attribute = attribute as? InlineBotMessageAttribute { + var botAddressName: String? + if let peerId = attribute.peerId, let botPeer = item.message.peers[peerId], let addressName = botPeer.addressName { + botAddressName = addressName + } else { + botAddressName = attribute.title + } + + if let botAddressName = botAddressName { + item.controllerInteraction.updateInputState { textInputState in + return ChatTextInputState(inputText: NSAttributedString(string: "@" + botAddressName + " ")) + } } return } @@ -1522,7 +1535,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { return nil } - override func peekPreviewContent(at point: CGPoint) -> (Message, Media)? { + override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { for contentNode in self.contentNodes { let frame = contentNode.frame if let result = contentNode.peekPreviewContent(at: point.offsetBy(dx: -frame.minX, dy: -frame.minY)) { @@ -1704,8 +1717,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var found = false for attribute in item.message.attributes { if let attribute = attribute as? InlineBotMessageAttribute { - botPeer = item.message.peers[attribute.peerId] - found = true + if let peerId = attribute.peerId { + botPeer = item.message.peers[peerId] + found = true + } } } if !found { diff --git a/TelegramUI/ChatMessageInstantVideoItemNode.swift b/TelegramUI/ChatMessageInstantVideoItemNode.swift index e4e0b9c166..8cd45e838a 100644 --- a/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -107,7 +107,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { if strongSelf.selectionNode != nil { return false } - return item.controllerInteraction.canSetupReply() + return item.controllerInteraction.canSetupReply(item.message) } return false } diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index 99e2c3b3f9..2dd77fe208 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -63,7 +63,19 @@ public enum ChatMessageItemContent: Sequence { } private func mediaIsNotMergeable(_ media: Media) -> Bool { - if let file = media as? TelegramMediaFile, file.isSticker { + if let file = media as? TelegramMediaFile { + for attribute in file.attributes { + switch attribute { + case .Sticker: + return false + case let .Video(_, _, flags): + if flags.contains(.instantRoundVideo) { + return false + } + default: + break + } + } return true } if let _ = media as? TelegramMediaAction { diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index fdfe94d7d8..5a423b5667 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -78,6 +78,11 @@ enum ChatMessageItemBottomNeighbor { let defaultChatMessageItemLayoutConstants = ChatMessageItemLayoutConstants() +enum ChatMessagePeekPreviewContent { + case media(Media) + case url(ASDisplayNode, CGRect, String) +} + public class ChatMessageItemView: ListViewItemNode { let layoutConstants = defaultChatMessageItemLayoutConstants @@ -145,7 +150,7 @@ public class ChatMessageItemView: ListViewItemNode { return nil } - func peekPreviewContent(at point: CGPoint) -> (Message, Media)? { + func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { return nil } diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index c5dcc9222d..2e3f8545cb 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -237,10 +237,10 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return nil } - override func peekPreviewContent(at point: CGPoint) -> (Message, Media)? { + override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { if let message = self.item?.message, let currentMedia = self.media { if self.interactiveImageNode.frame.contains(point) { - return (message, currentMedia) + return (message, .media(currentMedia)) } } return nil diff --git a/TelegramUI/ChatMessageReplyInfoNode.swift b/TelegramUI/ChatMessageReplyInfoNode.swift index 06de78ea7a..e508249ee1 100644 --- a/TelegramUI/ChatMessageReplyInfoNode.swift +++ b/TelegramUI/ChatMessageReplyInfoNode.swift @@ -78,6 +78,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { var updatedMedia: Media? var imageDimensions: CGSize? + var hasRoundImage = false if !message.containsSecretMedia { for media in message.media { if let image = media as? TelegramMediaImage { @@ -88,14 +89,18 @@ class ChatMessageReplyInfoNode: ASDisplayNode { break } else if let file = media as? TelegramMediaFile, file.isVideo { updatedMedia = file - if !file.isInstantVideo { - if let dimensions = file.dimensions { - imageDimensions = dimensions - } else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { - imageDimensions = representation.dimensions - } + + if let dimensions = file.dimensions { + imageDimensions = dimensions + } else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { + imageDimensions = representation.dimensions + } + if !file.isInstantVideo && !file.isAnimated { overlayIcon = PresentationResourcesChat.chatBubbleReplyThumbnailPlayImage(theme) } + if file.isInstantVideo { + hasRoundImage = true + } break } } @@ -105,7 +110,11 @@ class ChatMessageReplyInfoNode: ASDisplayNode { if let imageDimensions = imageDimensions { leftInset += 36.0 let boundingSize = CGSize(width: 30.0, height: 30.0) - applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) + var radius: CGFloat = 2.0 + if hasRoundImage { + radius = boundingSize.width / 2.0 + } + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) } var mediaUpdated = false diff --git a/TelegramUI/ChatMessageSelectionInputPanelNode.swift b/TelegramUI/ChatMessageSelectionInputPanelNode.swift index 9a9f79a9b3..808d49d018 100644 --- a/TelegramUI/ChatMessageSelectionInputPanelNode.swift +++ b/TelegramUI/ChatMessageSelectionInputPanelNode.swift @@ -106,4 +106,8 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { return 47.0 } + + override func minimalHeight(interfaceState: ChatPresentationInterfaceState) -> CGFloat { + return 47.0 + } } diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index 405f002fc5..665bf91a6a 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -60,7 +60,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if strongSelf.selectionNode != nil { return false } - return item.controllerInteraction.canSetupReply() + return item.controllerInteraction.canSetupReply(item.message) } return false } diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index ab88b66123..2f0d08984e 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -144,6 +144,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { break } } + if message.id.peerId.namespace == Namespaces.Peer.SecretChat { + generateEntities = true + } if generateEntities { let parsedEntities = generateTextEntities(message.text, enabledTypes: .all) if !parsedEntities.isEmpty { @@ -279,15 +282,15 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.frame if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { - if let url = attributes[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] as? String { + if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { return .url(url) - } else if let peerMention = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerMentionAttribute)] as? TelegramPeerMention { + } else if let peerMention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return .peerMention(peerMention.peerId, peerMention.mention) - } else if let peerName = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute)] as? String { + } else if let peerName = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { return .textMention(peerName) - } else if let botCommand = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramBotCommandAttribute)] as? String { + } else if let botCommand = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.BotCommand)] as? String { return .botCommand(botCommand) - } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute)] as? TelegramHashtag { + } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { return .hashtag(hashtag.peerName, hashtag.hashtag) } else { return .none @@ -304,11 +307,11 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let textNodeFrame = self.textNode.frame if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { let possibleNames: [String] = [ - TextNode.UrlAttribute, - TextNode.TelegramPeerMentionAttribute, - TextNode.TelegramPeerTextMentionAttribute, - TextNode.TelegramBotCommandAttribute, - TextNode.TelegramHashtagAttribute + TelegramTextAttributes.Url, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag ] for name in possibleNames { if let _ = attributes[NSAttributedStringKey(rawValue: name)] { @@ -338,4 +341,22 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } } + + override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? { + if let item = self.item { + let textNodeFrame = self.textNode.frame + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let value = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { + if let rects = self.textNode.attributeRects(name: TelegramTextAttributes.Url, at: index), !rects.isEmpty { + var rect = rects[0] + for i in 1 ..< rects.count { + rect = rect.union(rects[i]) + } + return (item.message, .url(self, rect, value)) + } + } + } + } + return nil + } } diff --git a/TelegramUI/ChatOverlayNavigationBar.swift b/TelegramUI/ChatOverlayNavigationBar.swift index e1a8cec513..e4e1ec30fc 100644 --- a/TelegramUI/ChatOverlayNavigationBar.swift +++ b/TelegramUI/ChatOverlayNavigationBar.swift @@ -1,6 +1,8 @@ import Foundation import AsyncDisplayKit import Display +import Postbox +import TelegramCore private let titleFont = Font.regular(14.0) @@ -12,6 +14,26 @@ final class ChatOverlayNavigationBar: ASDisplayNode { private let titleNode: TextNode private let closeButton: HighlightableButtonNode + private var validLayout: CGSize? + + private var peerTitle = "" + var peerView: PeerView? { + didSet { + var title = "" + if let peerView = self.peerView { + if let peer = peerViewMainPeer(peerView) { + title = peer.displayTitle + } + } + if self.peerTitle != title { + self.peerTitle = title + if let size = self.validLayout { + self.updateLayout(size: size, transition: .immediate) + } + } + } + } + init(theme: PresentationTheme, close: @escaping () -> Void) { self.theme = theme self.close = close @@ -53,13 +75,13 @@ final class ChatOverlayNavigationBar: ASDisplayNode { self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) } - func updateLayout(size: CGSize, presentationInterfaceState: ChatPresentationInterfaceState, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) let sideInset: CGFloat = 10.0 let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: presentationInterfaceState.peer?.displayTitle ?? "", font: titleFont, textColor: self.theme.inAppNotification.expandedNotification.navigationBar.primaryTextColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: size.width - sideInset * 2.0 - 40.0, height: size.height))) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.peerTitle, font: titleFont, textColor: self.theme.inAppNotification.expandedNotification.navigationBar.primaryTextColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: size.width - sideInset * 2.0 - 40.0, height: size.height))) let _ = titleApply() transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)) diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index cbeb7fe52c..f41b05cc6b 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -169,7 +169,7 @@ enum ChatMediaInputMode { enum ChatInputMode: Equatable { case none case text - case media(ChatMediaInputMode) + case media(mode: ChatMediaInputMode, expanded: Bool) case inputButtons static func ==(lhs: ChatInputMode, rhs: ChatInputMode) -> Bool { @@ -186,8 +186,8 @@ enum ChatInputMode: Equatable { } else { return false } - case let .media(mode): - if case .media(mode) = rhs { + case let .media(mode, expanded): + if case .media(mode, expanded) = rhs { return true } else { return false diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index 826fedc29f..128ba9bdf5 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -313,7 +313,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } } }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in - }, canSetupReply: { + }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings) @@ -581,9 +581,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } } - self.presentController(contextMenuController, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak node] in - if let node = node { - return (node, frame) + self.presentController(contextMenuController, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak node] in + if let strongSelf = self, let node = node { + return (node, frame, strongSelf, strongSelf.bounds) } else { return nil } diff --git a/TelegramUI/ChatRecordingPreviewInputPanelNode.swift b/TelegramUI/ChatRecordingPreviewInputPanelNode.swift index 41910c341f..2b8773741f 100644 --- a/TelegramUI/ChatRecordingPreviewInputPanelNode.swift +++ b/TelegramUI/ChatRecordingPreviewInputPanelNode.swift @@ -156,5 +156,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { @objc func waveformPressed() { self.mediaPlayer?.togglePlayPause() } + + override func minimalHeight(interfaceState: ChatPresentationInterfaceState) -> CGFloat { + return 47.0 + } } diff --git a/TelegramUI/ChatSearchInputPanelNode.swift b/TelegramUI/ChatSearchInputPanelNode.swift index 0aaaa63754..6b76247fbc 100644 --- a/TelegramUI/ChatSearchInputPanelNode.swift +++ b/TelegramUI/ChatSearchInputPanelNode.swift @@ -152,4 +152,8 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { return panelHeight } + + override func minimalHeight(interfaceState: ChatPresentationInterfaceState) -> CGFloat { + return 47.0 + } } diff --git a/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift b/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift index b466721ab5..057261a6d1 100644 --- a/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift +++ b/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift @@ -135,9 +135,9 @@ private final class AutoremoveTimeoutSelectorItemNode: ActionSheetItemNode, UIPi func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { if timeoutValues[row] == 0 { - return NSAttributedString(string: self.strings.Profile_MessageLifetimeForever, font: Font.medium(15.0), textColor: UIColor.black) + return NSAttributedString(string: self.strings.Profile_MessageLifetimeForever, font: Font.medium(15.0), textColor: self.theme.primaryTextColor) } else { - return NSAttributedString(string: timeIntervalString(strings: self.strings, value: timeoutValues[row]), font: Font.medium(15.0), textColor: UIColor.black) + return NSAttributedString(string: timeIntervalString(strings: self.strings, value: timeoutValues[row]), font: Font.medium(15.0), textColor: self.theme.primaryTextColor) } } diff --git a/TelegramUI/ChatTextInputActionButtonsNode.swift b/TelegramUI/ChatTextInputActionButtonsNode.swift new file mode 100644 index 0000000000..c3b1580f8b --- /dev/null +++ b/TelegramUI/ChatTextInputActionButtonsNode.swift @@ -0,0 +1,40 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class ChatTextInputActionButtonsNode: ASDisplayNode { + let micButton: ChatTextInputMediaRecordingButton + let sendButton: HighlightableButton + var sendButtonHasApplyIcon = false + var animatingSendButton = false + let expandMediaInputButton: HighlightableButtonNode + + init(theme: PresentationTheme, presentController: @escaping (ViewController) -> Void) { + self.micButton = ChatTextInputMediaRecordingButton(theme: theme, presentController: presentController) + self.sendButton = HighlightableButton() + self.expandMediaInputButton = HighlightableButtonNode() + + super.init() + + self.view.addSubview(self.micButton) + self.view.addSubview(self.sendButton) + self.addSubnode(self.expandMediaInputButton) + } + + func updateTheme(theme: PresentationTheme) { + self.micButton.updateTheme(theme: theme) + self.expandMediaInputButton.setImage(PresentationResourcesChat.chatInputPanelExpandButtonImage(theme), for: []) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { + transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(), size: size)) + self.micButton.layoutItems() + transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.expandMediaInputButton, frame: CGRect(origin: CGPoint(), size: size)) + var expanded = false + if case .media(_, true) = interfaceState.inputMode { + expanded = true + } + transition.updateSublayerTransformScale(node: self.expandMediaInputButton, scale: CGPoint(x: 1.0, y: expanded ? 1.0 : -1.0)) + } +} diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index f2b0aed688..757f0016f2 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -146,10 +146,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var textInputNode: ASEditableTextNode? let textInputBackgroundView: UIImageView - let micButton: ChatTextInputMediaRecordingButton - let sendButton: HighlightableButton - private var sendButtonHasApplyIcon = false - private var animatingSendButton = false + let actionButtons: ChatTextInputActionButtonsNode + let attachmentButton: HighlightableButton let searchLayoutClearButton: HighlightableButton let searchLayoutProgressView: UIImageView @@ -194,7 +192,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { override var account: Account? { didSet { - self.micButton.account = self.account + self.actionButtons.micButton.account = self.account } } @@ -241,7 +239,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor baseFontSize = max(17.0, presentationInterfaceState.fontSize.baseDisplaySize) } - textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(17.0), textColor: textColor) + textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(baseFontSize), textColor: textColor) self.editableTextNodeDidUpdateText(textInputNode) } } @@ -265,15 +263,17 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.searchLayoutClearButton = HighlightableButton() self.searchLayoutProgressView = UIImageView(image: searchLayoutProgressImage) self.searchLayoutProgressView.isHidden = true - self.micButton = ChatTextInputMediaRecordingButton(theme: theme, presentController: presentController) - self.sendButton = HighlightableButton() + + self.actionButtons = ChatTextInputActionButtonsNode(theme: theme, presentController: presentController) super.init() self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), for: .touchUpInside) self.view.addSubview(self.attachmentButton) - self.micButton.beginRecording = { [weak self] in + self.addSubnode(self.actionButtons) + + self.actionButtons.micButton.beginRecording = { [weak self] in if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let interfaceInteraction = strongSelf.interfaceInteraction { let isVideo: Bool switch presentationInterfaceState.interfaceState.mediaRecordingMode { @@ -285,7 +285,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { interfaceInteraction.beginMediaRecording(isVideo) } } - self.micButton.endRecording = { [weak self] sendMedia in + self.actionButtons.micButton.endRecording = { [weak self] sendMedia in if let strongSelf = self, let interfaceState = strongSelf.presentationInterfaceState, let interfaceInteraction = strongSelf.interfaceInteraction, let _ = interfaceState.inputTextPanelState.mediaRecordingState { if sendMedia { interfaceInteraction.finishMediaRecording(.send) @@ -294,33 +294,34 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } } - self.micButton.offsetRecordingControls = { [weak self] in + self.actionButtons.micButton.offsetRecordingControls = { [weak self] in if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState { if let (width, leftInset, rightInset, maxHeight) = strongSelf.validLayout { let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, transition: .immediate, interfaceState: presentationInterfaceState) } } } - self.micButton.stopRecording = { [weak self] in + self.actionButtons.micButton.stopRecording = { [weak self] in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.stopMediaRecording() } } - self.micButton.updateLocked = { [weak self] _ in + self.actionButtons.micButton.updateLocked = { [weak self] _ in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.lockMediaRecording() } } - self.micButton.switchMode = { [weak self] in + self.actionButtons.micButton.switchMode = { [weak self] in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.switchMediaRecordingMode() } } - self.view.addSubview(self.micButton) - self.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), for: .touchUpInside) - self.sendButton.alpha = 0.0 - self.view.addSubview(self.sendButton) + self.actionButtons.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), for: .touchUpInside) + self.actionButtons.sendButton.alpha = 0.0 + + self.actionButtons.expandMediaInputButton.addTarget(self, action: #selector(self.expandButtonPressed), forControlEvents: .touchUpInside) + self.actionButtons.expandMediaInputButton.alpha = 0.0 self.searchLayoutClearButton.addTarget(self, action: #selector(self.searchLayoutClearButtonPressed), for: .touchUpInside) self.searchLayoutClearButton.alpha = 0.0 @@ -467,6 +468,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return textFieldHeight + self.textFieldInsets.top + self.textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom } + override func minimalHeight(interfaceState: ChatPresentationInterfaceState) -> CGFloat { + let textFieldMinHeight = cauclulateTextFieldMinHeight(interfaceState) + let minimalHeight: CGFloat = 14.0 + textFieldMinHeight + return minimalHeight + } + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { self.validLayout = (width, leftInset, rightInset, maxHeight) let baseWidth = width - leftInset - rightInset @@ -511,7 +518,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme), for: []) - self.micButton.updateTheme(theme: interfaceState.theme) + self.actionButtons.updateTheme(theme: interfaceState.theme) let textFieldMinHeight = cauclulateTextFieldMinHeight(interfaceState) let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight @@ -557,10 +564,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil if updateSendButtonIcon { - if !self.animatingSendButton { - if transition.isAnimated && !self.sendButton.alpha.isZero && self.sendButton.layer.animation(forKey: "opacity") == nil, let imageView = self.sendButton.imageView, let previousImage = imageView.image { + if !self.actionButtons.animatingSendButton { + if transition.isAnimated && !self.actionButtons.sendButton.alpha.isZero && self.actionButtons.sendButton.layer.animation(forKey: "opacity") == nil, let imageView = self.actionButtons.sendButton.imageView, let previousImage = imageView.image { let tempView = UIImageView(image: previousImage) - self.sendButton.addSubview(tempView) + self.actionButtons.sendButton.addSubview(tempView) tempView.frame = imageView.frame tempView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempView] _ in tempView?.removeFromSuperview() @@ -570,11 +577,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { imageView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) imageView.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) } - self.sendButtonHasApplyIcon = sendButtonHasApplyIcon - if self.sendButtonHasApplyIcon { - self.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(interfaceState.theme), for: []) + self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon + if self.actionButtons.sendButtonHasApplyIcon { + self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(interfaceState.theme), for: []) } else { - self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: []) + self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: []) } } } @@ -639,7 +646,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight) - self.micButton.updateMode(mode: interfaceState.interfaceState.mediaRecordingMode, animated: transition.isAnimated) + self.actionButtons.micButton.updateMode(mode: interfaceState.interfaceState.mediaRecordingMode, animated: transition.isAnimated) var hideMicButton = false var audioRecordingItemsVerticalOffset: CGFloat = 0.0 @@ -655,7 +662,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { switch mediaRecordingState { case let .audio(recorder, isLocked): - self.micButton.audioRecorder = recorder + self.actionButtons.micButton.audioRecorder = recorder let audioRecordingInfoContainerNode: ASDisplayNode if let currentAudioRecordingInfoContainerNode = self.audioRecordingInfoContainerNode { audioRecordingInfoContainerNode = currentAudioRecordingInfoContainerNode @@ -679,7 +686,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.insertSubnode(audioRecordingCancelIndicator, at: 0) } - audioRecordingCancelIndicator.frame = CGRect(origin: CGPoint(x: leftInset + floor((baseWidth - audioRecordingCancelIndicator.bounds.size.width) / 2.0) - self.micButton.controlsOffset, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)), size: audioRecordingCancelIndicator.bounds.size) + audioRecordingCancelIndicator.frame = CGRect(origin: CGPoint(x: leftInset + floor((baseWidth - audioRecordingCancelIndicator.bounds.size.width) / 2.0) - self.actionButtons.micButton.controlsOffset, y: panelHeight - minimalHeight + floor((minimalHeight - audioRecordingCancelIndicator.bounds.size.height) / 2.0)), size: audioRecordingCancelIndicator.bounds.size) if animateCancelSlideIn { let position = audioRecordingCancelIndicator.layer.position @@ -745,15 +752,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { case let .video(status, _): switch status { case let .recording(recordingStatus): - self.micButton.videoRecordingStatus = recordingStatus + self.actionButtons.micButton.videoRecordingStatus = recordingStatus case .editing: - self.micButton.videoRecordingStatus = nil + self.actionButtons.micButton.videoRecordingStatus = nil hideMicButton = true } } } else { - self.micButton.audioRecorder = nil - self.micButton.videoRecordingStatus = nil + self.actionButtons.micButton.audioRecorder = nil + self.actionButtons.micButton.videoRecordingStatus = nil transition.updateAlpha(layer: self.textInputBackgroundView.layer, alpha: 1.0) if let textInputNode = self.textInputNode { transition.updateAlpha(node: textInputNode, alpha: 1.0) @@ -806,9 +813,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputBackgroundWidthOffset = 36.0 } - transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight))) - self.micButton.layoutItems() - transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight))) + transition.updateFrame(node: self.actionButtons, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight - UIScreenPixel), size: CGSize(width: 44.0, height: minimalHeight))) + if let presentationInterfaceState = self.presentationInterfaceState { + self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, interfaceState: presentationInterfaceState) + } let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight) transition.updateFrame(layer: self.searchLayoutClearButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - self.textFieldInsets.left - self.textFieldInsets.right + textInputBackgroundWidthOffset + 3.0, y: panelHeight - minimalHeight), size: searchLayoutClearButtonSize)) @@ -895,7 +903,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + var hasText = false if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { + hasText = true hideMicButton = true } @@ -903,15 +913,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { hideMicButton = true } - if hideMicButton { - if !self.micButton.alpha.isZero { - transition.updateAlpha(layer: self.micButton.layer, alpha: 0.0) - } - } else { - if self.micButton.alpha.isZero { - transition.updateAlpha(layer: self.micButton.layer, alpha: 1.0) - } - } + self.updateActionButtons(hasText: hasText, hideMicButton: hideMicButton, animated: transition.isAnimated) return panelHeight } @@ -937,30 +939,39 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { hideMicButton = true } self.textPlaceholderNode.isHidden = hasText + self.updateActionButtons(hasText: hasText, hideMicButton: hideMicButton, animated: animated) + } + + private func updateActionButtons(hasText: Bool, hideMicButton: Bool, animated: Bool) { + var hideMicButton = hideMicButton + var mediaInputIsActive = false if let presentationInterfaceState = self.presentationInterfaceState { if let mediaRecordingState = presentationInterfaceState.inputTextPanelState.mediaRecordingState { if case .video(.editing, false) = mediaRecordingState { hideMicButton = true } } + if case .media = presentationInterfaceState.inputMode { + mediaInputIsActive = true + } } var animateWithBounce = false if self.extendedSearchLayout { hideMicButton = true - if !self.sendButton.alpha.isZero { - self.sendButton.alpha = 0.0 + if !self.actionButtons.sendButton.alpha.isZero { + self.actionButtons.sendButton.alpha = 0.0 if animated { - self.animatingSendButton = true - self.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self.actionButtons.animatingSendButton = true + self.actionButtons.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in if let strongSelf = self { - strongSelf.animatingSendButton = false + strongSelf.actionButtons.animatingSendButton = false strongSelf.applyUpdateSendButtonIcon() } }) - self.sendButton.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2) + self.actionButtons.sendButton.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2) } } if self.searchLayoutClearButton.alpha.isZero { @@ -981,27 +992,27 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - if hasText || self.keepSendButtonEnabled { + if (hasText || self.keepSendButtonEnabled && !mediaInputIsActive) { hideMicButton = true - if self.sendButton.alpha.isZero { - self.sendButton.alpha = 1.0 + if self.actionButtons.sendButton.alpha.isZero { + self.actionButtons.sendButton.alpha = 1.0 if animated { - self.sendButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.actionButtons.sendButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) if animateWithBounce { - self.sendButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) + self.actionButtons.sendButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) } else { - self.sendButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) + self.actionButtons.sendButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) } } } } else { - if !self.sendButton.alpha.isZero { - self.sendButton.alpha = 0.0 + if !self.actionButtons.sendButton.alpha.isZero { + self.actionButtons.sendButton.alpha = 0.0 if animated { - self.animatingSendButton = true - self.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self.actionButtons.animatingSendButton = true + self.actionButtons.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in if let strongSelf = self { - strongSelf.animatingSendButton = false + strongSelf.actionButtons.animatingSendButton = false strongSelf.applyUpdateSendButtonIcon() } }) @@ -1010,27 +1021,52 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + if mediaInputIsActive { + hideMicButton = true + } + if hideMicButton { - if !self.micButton.alpha.isZero { - self.micButton.alpha = 0.0 + if !self.actionButtons.micButton.alpha.isZero { + self.actionButtons.micButton.alpha = 0.0 if animated { - self.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.actionButtons.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } } } else { - if self.micButton.alpha.isZero { - self.micButton.alpha = 1.0 + if self.actionButtons.micButton.alpha.isZero { + self.actionButtons.micButton.alpha = 1.0 if animated { - self.micButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.actionButtons.micButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) if animateWithBounce { - self.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) + self.actionButtons.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) } else { - self.micButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) + self.actionButtons.micButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) } } } } + if mediaInputIsActive { + if self.actionButtons.expandMediaInputButton.alpha.isZero { + self.actionButtons.expandMediaInputButton.alpha = 1.0 + if animated { + self.actionButtons.expandMediaInputButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + if animateWithBounce { + self.actionButtons.expandMediaInputButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) + } else { + self.actionButtons.expandMediaInputButton.layer.animateScale(from: 0.2, to: 1.0, duration: 0.25) + } + } + } + } else { + if !self.actionButtons.expandMediaInputButton.alpha.isZero { + self.actionButtons.expandMediaInputButton.alpha = 0.0 + if animated { + self.actionButtons.expandMediaInputButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + } + if let (width, leftInset, rightInset, maxHeight) = self.validLayout { let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset, maxHeight: maxHeight) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight) @@ -1040,16 +1076,23 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + @objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { + if self.actionButtons.sendButton.superview != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero { + self.sendButtonPressed() + } + return false + } + private func applyUpdateSendButtonIcon() { if let interfaceState = self.presentationInterfaceState { let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil - if sendButtonHasApplyIcon != self.sendButtonHasApplyIcon { - self.sendButtonHasApplyIcon = sendButtonHasApplyIcon - if self.sendButtonHasApplyIcon { - self.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(interfaceState.theme), for: []) + if sendButtonHasApplyIcon != self.actionButtons.sendButtonHasApplyIcon { + self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon + if self.actionButtons.sendButtonHasApplyIcon { + self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(interfaceState.theme), for: []) } else { - self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: []) + self.actionButtons.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: []) } } } @@ -1070,7 +1113,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { var activateGifInput = false if let presentationInterfaceState = self.presentationInterfaceState { - if case .media(.gif) = presentationInterfaceState.inputMode { + if case .media(.gif, _) = presentationInterfaceState.inputMode { activateGifInput = true } } @@ -1225,13 +1268,23 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputNode?.becomeFirstResponder() } + @objc func expandButtonPressed() { + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + if case let .media(mode, expanded) = state.inputMode { + return (.media(mode: mode, expanded: !expanded), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + } else { + return (state.inputMode, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + } + }) + } + @objc func accessoryItemButtonPressed(_ button: UIView) { for (item, currentButton) in self.accessoryItemButtons { if currentButton === button { switch item { case .stickers: self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in - return (.media(.other), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + return (.media(mode: .other, expanded: false), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) }) case .keyboard: self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in @@ -1275,8 +1328,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } func frameForInputActionButton() -> CGRect? { - if !self.micButton.alpha.isZero { - return self.micButton.frame.insetBy(dx: 0.0, dy: 6.0) + if !self.actionButtons.micButton.alpha.isZero { + return self.actionButtons.frame.insetBy(dx: 0.0, dy: 6.0) } return nil } diff --git a/TelegramUI/ChatTitleView.swift b/TelegramUI/ChatTitleView.swift index 04bb12e6ed..1fb3e795a6 100644 --- a/TelegramUI/ChatTitleView.swift +++ b/TelegramUI/ChatTitleView.swift @@ -60,6 +60,12 @@ private final class ChatTitleNetworkStatusNode: ASDisplayNode { } } +private enum ChatTitleIcon { + case none + case lock + case mute +} + final class ChatTitleView: UIView, NavigationBarTitleView { private let account: Account @@ -69,11 +75,16 @@ final class ChatTitleView: UIView, NavigationBarTitleView { private let contentContainer: ASDisplayNode private let titleNode: ASTextNode + private let titleLeftIconNode: ASImageNode + private let titleRightIconNode: ASImageNode private let infoNode: ASTextNode private let typingNode: ASTextNode private var typingIndicator: TGModernConversationTitleActivityIndicator? private let button: HighlightTrackingButtonNode + private var titleLeftIcon: ChatTitleIcon = .none + private var titleRightIcon: ChatTitleIcon = .none + private var networkStatusNode: ChatTitleNetworkStatusNode? private var presenceManager: PeerPresenceStatusManager? @@ -208,6 +219,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { didSet { if let titleContent = self.titleContent { var string: NSAttributedString? + var titleLeftIcon: ChatTitleIcon = .none + var titleRightIcon: ChatTitleIcon = .none switch titleContent { case let .peer(peerView): if let peer = peerViewMainPeer(peerView) { @@ -217,6 +230,14 @@ final class ChatTitleView: UIView, NavigationBarTitleView { string = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) } } + if peerView.peerId.namespace == Namespaces.Peer.SecretChat { + titleLeftIcon = .lock + } + if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { + if case .muted = notificationSettings.muteState { + titleRightIcon = .mute + } + } case .group: string = NSAttributedString(string: "Feed", font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) } @@ -226,6 +247,28 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.setNeedsLayout() } + if titleLeftIcon != self.titleLeftIcon { + self.titleLeftIcon = titleLeftIcon + switch titleLeftIcon { + case .lock: + self.titleLeftIconNode.image = PresentationResourcesChat.chatTitleLockIcon(self.theme) + default: + self.titleLeftIconNode.image = nil + } + self.setNeedsLayout() + } + + if titleRightIcon != self.titleRightIcon { + self.titleRightIcon = titleRightIcon + switch titleRightIcon { + case .mute: + self.titleRightIconNode.image = PresentationResourcesChat.chatTitleMuteIcon(self.theme) + default: + self.titleRightIconNode.image = nil + } + self.setNeedsLayout() + } + self.updateStatus() } } @@ -348,6 +391,16 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.titleNode.truncationMode = .byTruncatingTail self.titleNode.isOpaque = false + self.titleLeftIconNode = ASImageNode() + self.titleLeftIconNode.isLayerBacked = true + self.titleLeftIconNode.displayWithoutProcessing = true + self.titleLeftIconNode.displaysAsynchronously = false + + self.titleRightIconNode = ASImageNode() + self.titleRightIconNode.isLayerBacked = true + self.titleRightIconNode.displayWithoutProcessing = true + self.titleRightIconNode.displaysAsynchronously = false + self.infoNode = ASTextNode() self.infoNode.displaysAsynchronously = false self.infoNode.maximumNumberOfLines = 1 @@ -409,18 +462,44 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.button.frame = CGRect(origin: CGPoint(), size: size) self.contentContainer.frame = CGRect(origin: CGPoint(), size: size) + var leftIconWidth: CGFloat = 0.0 + var rightIconWidth: CGFloat = 0.0 + + if let image = self.titleLeftIconNode.image { + if self.titleLeftIconNode.supernode == nil { + self.contentContainer.addSubnode(titleLeftIconNode) + } + leftIconWidth = image.size.width + 3.0 + } else if self.titleLeftIconNode.supernode != nil { + self.titleLeftIconNode.removeFromSupernode() + } + + if let image = self.titleRightIconNode.image { + if self.titleRightIconNode.supernode == nil { + self.contentContainer.addSubnode(titleRightIconNode) + } + rightIconWidth = image.size.width + 3.0 + } else if self.titleRightIconNode.supernode != nil { + self.titleRightIconNode.removeFromSupernode() + } + if size.height > 40.0 { - let titleSize = self.titleNode.measure(size) + let titleSize = self.titleNode.measure(CGSize(width: size.width - leftIconWidth - rightIconWidth, height: size.height)) let infoSize = self.infoNode.measure(size) let typingSize = self.typingNode.measure(size) let titleInfoSpacing: CGFloat = 0.0 + let titleFrame: CGRect + if infoSize.width.isZero && typingSize.width.isZero { - self.titleNode.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) + titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) + self.titleNode.frame = titleFrame } else { 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) + titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) + self.titleNode.frame = titleFrame + 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) self.typingNode.frame = CGRect(origin: CGPoint(x: floor((size.width - typingSize.width + 14.0) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: typingSize) @@ -428,17 +507,32 @@ final class ChatTitleView: UIView, NavigationBarTitleView { typingIndicator.frame = CGRect(x: self.typingNode.frame.origin.x - 24.0, y: self.typingNode.frame.origin.y, width: 24.0, height: 16.0) } } + + if let image = self.titleLeftIconNode.image { + self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX - image.size.width - 3.0 - UIScreenPixel, y: titleFrame.minY + 4.0), size: image.size) + } + if let image = self.titleRightIconNode.image { + self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 3.0, y: titleFrame.minY + 7.0), size: image.size) + } } else { - let titleSize = self.titleNode.measure(CGSize(width: floor(size.width / 2.0), height: size.height)) + let titleSize = self.titleNode.measure(CGSize(width: floor(size.width / 2.0 - leftIconWidth - rightIconWidth), height: size.height)) let infoSize = self.infoNode.measure(CGSize(width: floor(size.width / 2.0), height: size.height)) let typingSize = self.typingNode.measure(CGSize(width: floor(size.width / 2.0), height: size.height)) let titleInfoSpacing: CGFloat = 8.0 - let combinedWidth = titleSize.width + infoSize.width + titleInfoSpacing + let combinedWidth = titleSize.width + leftIconWidth + rightIconWidth + 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) - self.typingNode.frame = CGRect(origin: CGPoint(x: floor((size.width - combinedWidth) / 2.0 + titleSize.width + titleInfoSpacing), y: floor((size.height - typingSize.height) / 2.0)), size: typingSize) + let titleFrame = CGRect(origin: CGPoint(x: leftIconWidth + floor((size.width - combinedWidth) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) + self.titleNode.frame = titleFrame + self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - combinedWidth) / 2.0 + titleSize.width + leftIconWidth + rightIconWidth + titleInfoSpacing), y: floor((size.height - infoSize.height) / 2.0)), size: infoSize) + self.typingNode.frame = CGRect(origin: CGPoint(x: floor((size.width - combinedWidth) / 2.0 + titleSize.width + leftIconWidth + rightIconWidth + titleInfoSpacing), y: floor((size.height - typingSize.height) / 2.0)), size: typingSize) + + if let image = self.titleLeftIconNode.image { + self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.minY + 4.0), size: image.size) + } + if let image = self.titleRightIconNode.image { + self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 6.0), size: image.size) + } } if let networkStatusNode = self.networkStatusNode { diff --git a/TelegramUI/ChatUnblockInputPanelNode.swift b/TelegramUI/ChatUnblockInputPanelNode.swift index 1915c3463b..d97216742c 100644 --- a/TelegramUI/ChatUnblockInputPanelNode.swift +++ b/TelegramUI/ChatUnblockInputPanelNode.swift @@ -96,4 +96,8 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { return 47.0 } + + override func minimalHeight(interfaceState: ChatPresentationInterfaceState) -> CGFloat { + return 47.0 + } } diff --git a/TelegramUI/DeleteChatInputPanelNode.swift b/TelegramUI/DeleteChatInputPanelNode.swift index a61413bef7..4dd9e6a0e9 100644 --- a/TelegramUI/DeleteChatInputPanelNode.swift +++ b/TelegramUI/DeleteChatInputPanelNode.swift @@ -48,4 +48,8 @@ final class DeleteChatInputPanelNode: ChatInputPanelNode { return panelHeight } + + override func minimalHeight(interfaceState: ChatPresentationInterfaceState) -> CGFloat { + return 47.0 + } } diff --git a/TelegramUI/EmbedVideoNode.swift b/TelegramUI/EmbedVideoNode.swift index 2a2877dde8..2d0596e565 100644 --- a/TelegramUI/EmbedVideoNode.swift +++ b/TelegramUI/EmbedVideoNode.swift @@ -493,7 +493,7 @@ final class EmbedVideoNode: OverlayMediaItemNode { } else { status = .paused } - subscriber.putNext(MediaPlayerStatus(generationTimestamp: 0.0, duration: next.duration, timestamp: next.position, seekId: 0, status: status)) + subscriber.putNext(MediaPlayerStatus(generationTimestamp: 0.0, duration: next.duration, dimensions: CGSize(), timestamp: next.position, seekId: 0, status: status)) } }) return ActionDisposable { diff --git a/TelegramUI/ExtractVideoData.swift b/TelegramUI/ExtractVideoData.swift new file mode 100644 index 0000000000..591fdb5a0a --- /dev/null +++ b/TelegramUI/ExtractVideoData.swift @@ -0,0 +1,7 @@ +import Foundation +import SwiftSignalKit +import Postbox + +func extractVideoDimensions() { + +} diff --git a/TelegramUI/FileMediaResourceStatus.swift b/TelegramUI/FileMediaResourceStatus.swift index 14d2b50a74..6a33272785 100644 --- a/TelegramUI/FileMediaResourceStatus.swift +++ b/TelegramUI/FileMediaResourceStatus.swift @@ -29,7 +29,7 @@ func messageFileMediaPlaybackStatus(account: Account, file: TelegramMediaFile, m if let value = file.duration { duration = Double(value) } - let defaultStatus = MediaPlayerStatus(generationTimestamp: 0.0, duration: duration, timestamp: 0.0, seekId: 0, status: .paused) + let defaultStatus = MediaPlayerStatus(generationTimestamp: 0.0, duration: duration, dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: .paused) return internalMessageFileMediaPlaybackStatus(account: account, file: file, message: message) |> map { status in return status ?? defaultStatus } diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 956f49687a..7d1dcf5176 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -565,6 +565,8 @@ class GalleryController: ViewController { } self._hiddenMedia.set(.single((message.id, media))) + } else if self.isPresentedInPreviewingContext() { + centralItemNode.activateAsInitial() } } } @@ -592,7 +594,6 @@ class GalleryController: ViewController { if let centralItemNode = self.galleryNode.pager.centralItemNode(), let itemSize = centralItemNode.contentSize() { self.preferredContentSize = itemSize.aspectFitted(self.view.bounds.size) self.containerLayoutUpdated(ContainerViewLayout(size: self.preferredContentSize, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, standardInputHeight: 216.0, inputHeightIsInteractivellyChanging: false), transition: .immediate) - centralItemNode.activateAsInitial() } } } diff --git a/TelegramUI/GameController.swift b/TelegramUI/GameController.swift index cd2cb7a7c9..c6684e744c 100644 --- a/TelegramUI/GameController.swift +++ b/TelegramUI/GameController.swift @@ -38,8 +38,8 @@ final class GameController: ViewController { var botPeer: Peer? inner: for attribute in message.attributes { - if let attribute = attribute as? InlineBotMessageAttribute { - botPeer = message.peers[attribute.peerId] + if let attribute = attribute as? InlineBotMessageAttribute, let peerId = attribute.peerId { + botPeer = message.peers[peerId] break inner } } @@ -66,11 +66,13 @@ final class GameController: ViewController { } @objc func sharePressed() { - + self.controllerNode.shareWithoutScore() } override func loadDisplayNode() { - self.displayNode = GameControllerNode(presentationData: self.presentationData, url: self.url) + self.displayNode = GameControllerNode(account: self.account, presentationData: self.presentationData, url: self.url, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }, message: self.message) } override func viewDidAppear(_ animated: Bool) { diff --git a/TelegramUI/GameControllerNode.swift b/TelegramUI/GameControllerNode.swift index e1b384c3a4..5ad8b4eb4a 100644 --- a/TelegramUI/GameControllerNode.swift +++ b/TelegramUI/GameControllerNode.swift @@ -2,6 +2,9 @@ import Foundation import Display import AsyncDisplayKit import WebKit +import TelegramCore +import Postbox +import SwiftSignalKit private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler { private let f: (WKScriptMessage) -> () @@ -20,10 +23,16 @@ private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler { final class GameControllerNode: ViewControllerTracingNode { private var webView: WKWebView? + private let account: Account var presentationData: PresentationData + private let present: (ViewController, Any?) -> Void + private let message: Message - init(presentationData: PresentationData, url: String) { + init(account: Account, presentationData: PresentationData, url: String, present: @escaping (ViewController, Any?) -> Void, message: Message) { + self.account = account self.presentationData = presentationData + self.present = present + self.message = message super.init() @@ -77,6 +86,31 @@ final class GameControllerNode: ViewControllerTracingNode { }) } + private func shareData() -> (Peer, String)? { + var botPeer: Peer? + var gameName: String? + for media in self.message.media { + if let game = media as? TelegramMediaGame { + inner: for attribute in self.message.attributes { + if let attribute = attribute as? InlineBotMessageAttribute, let peerId = attribute.peerId { + botPeer = self.message.peers[peerId] + break inner + } + } + if botPeer == nil { + botPeer = self.message.author + } + + gameName = game.name + } + } + if let botPeer = botPeer, let gameName = gameName { + return (botPeer, gameName) + } + + return nil + } + private func handleScriptMessage(_ message: WKScriptMessage) { guard let body = message.body as? [String: Any] else { return @@ -87,7 +121,28 @@ final class GameControllerNode: ViewControllerTracingNode { } if eventName == "share_game" || eventName == "share_score" { - + if let (botPeer, gameName) = self.shareData(), let addressName = botPeer.addressName, !addressName.isEmpty, !gameName.isEmpty { + if eventName == "share_score" { + self.present(ShareController(account: self.account, subject: .fromExternal({ [weak self] peerIds, text in + if let strongSelf = self { + let signals = peerIds.map { forwardGameWithScore(account: strongSelf.account, messageId: strongSelf.message.id, to: $0) } + return .single(.preparing) |> then(combineLatest(signals) + |> mapToSignal { _ -> Signal in return .complete() }) |> then(.single(.done)) + } else { + return .single(.done) + } + }), saveToCameraRoll: false, showInChat: nil, externalShare: false, immediateExternalShare: false), nil) + } else { + self.shareWithoutScore() + } + } + } + } + + func shareWithoutScore() { + if let (botPeer, gameName) = self.shareData(), let addressName = botPeer.addressName, !addressName.isEmpty, !gameName.isEmpty { + let url = "https://t.me/\(addressName)?game=\(gameName)" + self.present(ShareController(account: self.account, subject: .url(url), saveToCameraRoll: false, showInChat: nil, externalShare: true), nil) } } } diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 63fd85b78d..c42486619a 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -1512,8 +1512,8 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl UIPasteboard.general.string = text })]) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in - if let resultItemNode = resultItemNode { - return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) + if let strongController = controller, let resultItemNode = resultItemNode { + return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds) } else { return nil } diff --git a/TelegramUI/HapticFeedback.swift b/TelegramUI/HapticFeedback.swift deleted file mode 100644 index 5c54daa9fb..0000000000 --- a/TelegramUI/HapticFeedback.swift +++ /dev/null @@ -1,107 +0,0 @@ -import Foundation -import UIKit - -@available(iOSApplicationExtension 10.0, *) -private final class HapticFeedbackImpl { - private lazy var impactGenerator = { UIImpactFeedbackGenerator(style: .medium) }() - private lazy var selectionGenerator = { UISelectionFeedbackGenerator() }() - private lazy var notificationGenerator = { UINotificationFeedbackGenerator() }() - - func prepareTap() { - self.selectionGenerator.prepare() - } - - func tap() { - self.selectionGenerator.selectionChanged() - } - - func prepareImpact() { - self.impactGenerator.prepare() - } - - func impact() { - self.impactGenerator.impactOccurred() - } - - func success() { - self.notificationGenerator.notificationOccurred(.success) - } - - func error() { - self.notificationGenerator.notificationOccurred(.error) - } - - @objc dynamic func f() { - } -} - -final class HapticFeedback { - private var impl: AnyObject? - - deinit { - let impl = self.impl - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { - if #available(iOSApplicationExtension 10.0, *) { - if let impl = impl as? HapticFeedbackImpl { - impl.f() - } - } - }) - } - - @available(iOSApplicationExtension 10.0, *) - private func withImpl(_ f: (HapticFeedbackImpl) -> Void) { - if self.impl == nil { - self.impl = HapticFeedbackImpl() - } - f(self.impl as! HapticFeedbackImpl) - } - - func prepareTap() { - if #available(iOSApplicationExtension 10.0, *) { - self.withImpl { impl in - impl.prepareTap() - } - } - } - - func tap() { - if #available(iOSApplicationExtension 10.0, *) { - self.withImpl { impl in - impl.tap() - } - } - } - - func prepareImpact() { - if #available(iOSApplicationExtension 10.0, *) { - self.withImpl { impl in - impl.prepareImpact() - } - } - } - - func impact() { - if #available(iOSApplicationExtension 10.0, *) { - self.withImpl { impl in - impl.impact() - } - } - } - - func success() { - if #available(iOSApplicationExtension 10.0, *) { - self.withImpl { impl in - impl.success() - } - } - } - - func error() { - if #available(iOSApplicationExtension 10.0, *) { - self.withImpl { impl in - impl.error() - } - } - } -} diff --git a/TelegramUI/InstantPageController.swift b/TelegramUI/InstantPageController.swift index ba04fa3247..3b26dc60e8 100644 --- a/TelegramUI/InstantPageController.swift +++ b/TelegramUI/InstantPageController.swift @@ -72,7 +72,7 @@ final class InstantPageController: ViewController { } override public func loadDisplayNode() { - self.displayNode = InstantPageControllerNode(account: self.account, settings: self.settings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, statusBar: self.statusBar, getNavigationController: { [weak self] in + self.displayNode = InstantPageControllerNode(account: self.account, settings: self.settings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, timeFormat: self.presentationData.timeFormat, statusBar: self.statusBar, getNavigationController: { [weak self] in return self?.navigationController as? NavigationController }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index c527560e23..039f2dfc30 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -11,6 +11,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private var settings: InstantPagePresentationSettings? private var presentationTheme: PresentationTheme private var strings: PresentationStrings + private var timeFormat: PresentationTimeFormat private var theme: InstantPageTheme? private let getNavigationController: () -> NavigationController? private let present: (ViewController, Any?) -> Void @@ -47,9 +48,10 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private let resolveUrlDisposable = MetaDisposable() private let loadWebpageDisposable = MetaDisposable() - init(account: Account, settings: InstantPagePresentationSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, statusBar: StatusBar, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) { + init(account: Account, settings: InstantPagePresentationSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, timeFormat: PresentationTimeFormat, statusBar: StatusBar, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) { self.account = account self.presentationTheme = presentationTheme + self.timeFormat = timeFormat self.strings = strings self.settings = settings self.theme = settings.flatMap { return instantPageThemeForSettingsAndTime(settings: $0, time: Date()) } @@ -276,7 +278,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { return } - let currentLayout = instantPageLayoutForWebPage(webPage, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme) + let currentLayout = instantPageLayoutForWebPage(webPage, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, timeFormat: self.timeFormat) for (_, tileNode) in self.visibleTiles { tileNode.removeFromSupernode() @@ -660,7 +662,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in if let strongSelf = self { - return (strongSelf.scrollNode, coveringRect.insetBy(dx: -3.0, dy: -3.0)) + return (strongSelf.scrollNode, coveringRect.insetBy(dx: -3.0, dy: -3.0), strongSelf, strongSelf.bounds) } else { return nil } diff --git a/TelegramUI/InstantPageLayout.swift b/TelegramUI/InstantPageLayout.swift index ad781a490d..9dd10210f5 100644 --- a/TelegramUI/InstantPageLayout.swift +++ b/TelegramUI/InstantPageLayout.swift @@ -39,10 +39,10 @@ private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantP } } -func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToWidthAndHeight: Bool, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, theme: InstantPageTheme) -> InstantPageLayout { +func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToWidthAndHeight: Bool, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, timeFormat: PresentationTimeFormat) -> InstantPageLayout { switch block { case let .cover(block): - return layoutInstantPageBlock(webpage: webpage, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToWidthAndHeight: fillToWidthAndHeight, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) + return layoutInstantPageBlock(webpage: webpage, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToWidthAndHeight: fillToWidthAndHeight, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme, strings: strings, timeFormat: timeFormat) case let .title(text): let styleStack = InstantPageTextStyleStack() setupStyleStack(styleStack, theme: theme, category: .header, link: false) @@ -61,28 +61,36 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo var text: RichText? if case .empty = author { if date != 0 { - let dateStringPlain = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(date)), dateStyle: .long, timeStyle: .none) + let dateFormatter = DateFormatter() + dateFormatter.locale = localeWithStrings(strings) + dateFormatter.dateStyle = .long + dateFormatter.timeStyle = .none + let dateStringPlain = dateFormatter.string(from: Date(timeIntervalSince1970: Double(date))) text = RichText.plain(dateStringPlain) } } else { - let dateStringPlain = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(date)), dateStyle: .long, timeStyle: .none) + let dateFormatter = DateFormatter() + dateFormatter.locale = localeWithStrings(strings) + dateFormatter.dateStyle = .long + dateFormatter.timeStyle = .none + let dateStringPlain = dateFormatter.string(from: Date(timeIntervalSince1970: Double(date))) let dateText = RichText.plain(dateStringPlain) if date != 0 { - let formatString = NSLocalizedString("InstantPage.AuthorAndDateTitle", comment: "") + let formatString = strings.InstantPage_AuthorAndDateTitle("%1$@", "%2$@").0 let authorRange = formatString.range(of: "%1$@")! let dateRange = formatString.range(of: "%2$@")! if authorRange.lowerBound < dateRange.lowerBound { - let byPart = formatString.substring(to: authorRange.lowerBound) - let middlePart = formatString.substring(with: authorRange.upperBound ..< dateRange.lowerBound) - let endPart = formatString.substring(from: dateRange.upperBound) + let byPart = String(formatString[formatString.startIndex ..< authorRange.lowerBound]) + let middlePart = String(formatString[authorRange.upperBound ..< dateRange.lowerBound]) + let endPart = String(formatString[dateRange.upperBound...]) text = .concat([.plain(byPart), author, .plain(middlePart), dateText, .plain(endPart)]) } else { - let beforePart = formatString.substring(to: dateRange.lowerBound) - let middlePart = formatString.substring(with: dateRange.upperBound ..< authorRange.lowerBound) - let endPart = formatString.substring(from: authorRange.upperBound) + let beforePart = String(formatString[formatString.startIndex ..< dateRange.lowerBound]) + let middlePart = String(formatString[dateRange.upperBound ..< authorRange.lowerBound]) + let endPart = String(formatString[authorRange.upperBound...]) text = .concat([.plain(beforePart), dateText, .plain(middlePart), author, .plain(endPart)]) } @@ -351,7 +359,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo nextItemOrigin.x = 0.0 nextItemOrigin.y += itemSize + spacing } - let subLayout = layoutInstantPageBlock(webpage: webpage, block: subItem, boundingWidth: itemSize, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToWidthAndHeight: true, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) + let subLayout = layoutInstantPageBlock(webpage: webpage, block: subItem, boundingWidth: itemSize, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToWidthAndHeight: true, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme, strings: strings, timeFormat: timeFormat) items.append(contentsOf: subLayout.flattenedItemsWithOrigin(nextItemOrigin)) nextItemOrigin.x += itemSize + spacing } @@ -432,7 +440,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo var previousBlock: InstantPageBlock? for subBlock in blocks { - let subLayout = layoutInstantPageBlock(webpage: webpage, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) + let subLayout = layoutInstantPageBlock(webpage: webpage, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme, strings: strings, timeFormat: timeFormat) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock) let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + lineInset, y: contentSize.height + spacing)) @@ -477,7 +485,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo let filledSize = imageSize.fitted(CGSize(width: boundingWidth, height: 1200.0)) contentSize.height = max(contentSize.height, filledSize.height) - itemMedias.append(InstantPageMedia(index: mediaIndex, media: image, caption: "")) + itemMedias.append(InstantPageMedia(index: mediaIndex, media: image, caption: caption.plainText)) } break default: @@ -486,6 +494,21 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo } items.append(InstantPageSlideshowItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height)), medias: itemMedias)) + + if case .empty = caption { + } else { + contentSize.height += 14.0 + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + + let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) + captionItem.frame = captionItem.frame.offsetBy(dx: floor(boundingWidth - captionItem.frame.size.width) / 2.0, dy: contentSize.height) + captionItem.alignment = .center + + contentSize.height += captionItem.frame.size.height + items.append(captionItem) + } return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) case let .webEmbed(url, html, dimensions, caption, stretchToWidth, allowScrolling, coverId): @@ -575,7 +598,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, block: InstantPageBlo } } -func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme) -> InstantPageLayout { +func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, timeFormat: PresentationTimeFormat) -> InstantPageLayout { var maybeLoadedContent: TelegramMediaWebpageLoadedContent? if case let .Loaded(content) = webPage.content { maybeLoadedContent = content @@ -599,7 +622,7 @@ func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: var previousBlock: InstantPageBlock? for block in pageBlocks { - let blockLayout = layoutInstantPageBlock(webpage: webPage, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme) + let blockLayout = layoutInstantPageBlock(webpage: webPage, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, theme: theme, strings: strings, timeFormat: timeFormat) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block) let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) items.append(contentsOf: blockItems) diff --git a/TelegramUI/InstantPageTextItem.swift b/TelegramUI/InstantPageTextItem.swift index 6160f678b6..693549709d 100644 --- a/TelegramUI/InstantPageTextItem.swift +++ b/TelegramUI/InstantPageTextItem.swift @@ -211,8 +211,8 @@ final class InstantPageTextItem: InstantPageItem { func linkSelectionRects(at point: CGPoint) -> [CGRect] { if let (index, dict) = self.attributesAtPoint(point) { - if let _ = dict[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] { - if let rects = self.attributeRects(name: NSAttributedStringKey(rawValue: TextNode.UrlAttribute), at: index) { + if let _ = dict[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] { + if let rects = self.attributeRects(name: NSAttributedStringKey(rawValue: TelegramTextAttributes.Url), at: index) { return rects } } @@ -223,7 +223,7 @@ final class InstantPageTextItem: InstantPageItem { func urlAttribute(at point: CGPoint) -> InstantPageUrlItem? { if let (_, dict) = self.attributesAtPoint(point) { - if let url = dict[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] as? InstantPageUrlItem { + if let url = dict[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? InstantPageUrlItem { return url } } @@ -285,7 +285,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt case let .plain(string): var attributes = styleStack.textAttributes() if let url = url { - attributes[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] = url + attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] = url } return NSAttributedString(string: string, attributes: attributes) case let .bold(text): diff --git a/TelegramUI/ItemListMultilineTextItem.swift b/TelegramUI/ItemListMultilineTextItem.swift index 81ebd24784..4421b23b96 100644 --- a/TelegramUI/ItemListMultilineTextItem.swift +++ b/TelegramUI/ItemListMultilineTextItem.swift @@ -327,11 +327,11 @@ class ItemListMultilineTextItemNode: ListViewItemNode { private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? { let textNodeFrame = self.textNode.frame if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { - if let url = attributes[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] as? String { + if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { return .url(url) - } else if let peerName = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute)] as? String { + } else if let peerName = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { return .mention(peerName) - } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute)] as? TelegramHashtag { + } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { return .hashtag(hashtag.peerName, hashtag.hashtag) } else { return nil @@ -351,11 +351,11 @@ class ItemListMultilineTextItemNode: ListViewItemNode { let textNodeFrame = self.textNode.frame if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { let possibleNames: [String] = [ - TextNode.UrlAttribute, - TextNode.TelegramPeerMentionAttribute, - TextNode.TelegramPeerTextMentionAttribute, - TextNode.TelegramBotCommandAttribute, - TextNode.TelegramHashtagAttribute + TelegramTextAttributes.Url, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag ] for name in possibleNames { if let _ = attributes[NSAttributedStringKey(rawValue: name)] { diff --git a/TelegramUI/ItemListSecretChatKeyItem.swift b/TelegramUI/ItemListSecretChatKeyItem.swift new file mode 100644 index 0000000000..547762bfba --- /dev/null +++ b/TelegramUI/ItemListSecretChatKeyItem.swift @@ -0,0 +1,338 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore + +class ItemListSecretChatKeyItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let icon: UIImage? + let title: String + let fingerprint: SecretChatKeyFingerprint + let sectionId: ItemListSectionId + let style: ItemListStyle + let disclosureStyle: ItemListDisclosureStyle + let action: (() -> Void)? + + init(theme: PresentationTheme, icon: UIImage? = nil, title: String, fingerprint: SecretChatKeyFingerprint, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?) { + self.theme = theme + self.icon = icon + self.title = title + self.fingerprint = fingerprint + self.sectionId = sectionId + self.style = style + self.disclosureStyle = disclosureStyle + self.action = action + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = ItemListSecretChatKeyItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply() }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? ItemListSecretChatKeyItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } + } + + var selectable: Bool = true + + func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action?() + } +} + +private let titleFont = Font.regular(17.0) + +class ItemListSecretChatKeyItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + let iconNode: ASImageNode + let titleNode: TextNode + let keyNode: ASImageNode + let arrowNode: ASImageNode + + private var item: ItemListSecretChatKeyItem? + + override var canBeSelected: Bool { + if let item = self.item, let _ = item.action { + return true + } else { + return false + } + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displaysAsynchronously = false + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + + self.keyNode = ASImageNode() + self.keyNode.isLayerBacked = true + self.keyNode.displayWithoutProcessing = true + self.keyNode.displaysAsynchronously = false + + self.arrowNode = ASImageNode() + self.arrowNode.displayWithoutProcessing = true + self.arrowNode.displaysAsynchronously = false + self.arrowNode.isLayerBacked = true + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.titleNode) + self.addSubnode(self.keyNode) + self.addSubnode(self.arrowNode) + } + + func asyncLayout() -> (_ item: ItemListSecretChatKeyItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + + let currentItem = self.item + + return { item, params, neighbors in + let rightInset: CGFloat + switch item.disclosureStyle { + case .none: + rightInset = 16.0 + params.rightInset + case .arrow: + rightInset = 34.0 + params.rightInset + } + + var updateArrowImage: UIImage? + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.theme) + } + + var updateIcon = false + if currentItem?.icon != item.icon { + updateIcon = true + } + + var updateKeyImage: UIImage? + if currentItem?.fingerprint != item.fingerprint { + updateKeyImage = secretChatKeyImage(item.fingerprint, size: CGSize(width: 24.0, height: 24.0)) + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + var leftInset: CGFloat = params.leftInset + + switch item.style { + case .plain: + leftInset += 35.0 + itemBackgroundColor = item.theme.list.plainBackgroundColor + itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + leftInset += 16.0 + itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: 44.0) + insets = itemListNeighborsGroupedInsets(neighbors) + } + + if let _ = item.icon { + leftInset += 43.0 + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + if let icon = item.icon { + if strongSelf.iconNode.supernode == nil { + strongSelf.addSubnode(strongSelf.iconNode) + } + if updateIcon { + strongSelf.iconNode.image = icon + } + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - icon.size.width) / 2.0), y: floor((layout.contentSize.height - icon.size.height) / 2.0)), size: icon.size) + } else if strongSelf.iconNode.supernode != nil { + strongSelf.iconNode.image = nil + strongSelf.iconNode.removeFromSupernode() + } + + if let updateArrowImage = updateArrowImage { + strongSelf.arrowNode.image = updateArrowImage + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + + let _ = titleApply() + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + default: + bottomStripeInset = 0.0 + } + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + if let updateKeyImage = updateKeyImage { + strongSelf.keyNode.image = updateKeyImage + } + if let image = strongSelf.keyNode.image { + strongSelf.keyNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - image.size.width, y: 10.0), size: image.size) + } + + if let arrowImage = strongSelf.arrowNode.image { + strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 15.0 - arrowImage.size.width, y: 15.0), size: arrowImage.size) + } + + switch item.disclosureStyle { + case .none: + strongSelf.arrowNode.isHidden = true + case .arrow: + strongSelf.arrowNode.isHidden = false + } + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel)) + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} + diff --git a/TelegramUI/ItemListStickerPackItem.swift b/TelegramUI/ItemListStickerPackItem.swift index 41165b1109..f7ba074b7e 100644 --- a/TelegramUI/ItemListStickerPackItem.swift +++ b/TelegramUI/ItemListStickerPackItem.swift @@ -317,10 +317,11 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } var imageApply: (() -> Void)? + var imageSize: CGSize = CGSize(width: 34.0, height: 34.0) if let file = file, let dimensions = file.dimensions { let imageBoundingSize = CGSize(width: 34.0, height: 34.0) - let fileImageSize = dimensions.aspectFitted(imageBoundingSize) - imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: fileImageSize, boundingSize: imageBoundingSize, intrinsicInsets: UIEdgeInsets())) + imageSize = dimensions.aspectFitted(imageBoundingSize) + imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) } var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? @@ -480,7 +481,8 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: (strongSelf.unreadNode.isHidden ? 0.0 : 10.0) + leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size)) transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 32.0), size: statusLayout.size)) - transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: 11.0), size: CGSize(width: 34.0, height: 34.0))) + let boundingSize = CGSize(width: 34.0, height: 34.0) + transition.updateFrame(node: strongSelf.imageNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0 + floor((boundingSize.width - imageSize.width) / 2.0), y: 11.0 + floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize)) if let updatedImageSignal = updatedImageSignal { strongSelf.imageNode.setSignal(updatedImageSignal) @@ -569,7 +571,9 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size)) - transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0, y: self.imageNode.frame.minY), size: CGSize(width: 34.0, height: 34.0))) + let boundingSize = CGSize(width: 34.0, height: 34.0) + + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 15.0 + floor((boundingSize.width - self.imageNode.frame.size.width) / 2.0), y: self.imageNode.frame.minY), size: self.imageNode.frame.size)) } override func revealOptionsInteractivelyOpened() { @@ -600,7 +604,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } override func isReorderable(at point: CGPoint) -> Bool { - if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point) { + if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions { return true } return false diff --git a/TelegramUI/ItemListTextItem.swift b/TelegramUI/ItemListTextItem.swift index fbb32cf8c9..a6b28dc953 100644 --- a/TelegramUI/ItemListTextItem.swift +++ b/TelegramUI/ItemListTextItem.swift @@ -104,7 +104,7 @@ class ItemListTextItemNode: ListViewItemNode { attributedText = NSAttributedString(string: text, font: titleFont, textColor: item.theme.list.freeTextColor) case let .markdown(text): attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in - return (TextNode.UrlAttribute, contents) + return (TelegramTextAttributes.Url, contents) })) } let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -145,7 +145,7 @@ class ItemListTextItemNode: ListViewItemNode { let titleFrame = self.titleNode.frame if let item = self.item, titleFrame.contains(location) { if let (_, attributes) = self.titleNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { - if let url = attributes[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] as? String { + if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { item.linkAction?(.tap(url)) } } diff --git a/TelegramUI/ItemListTextWithLabelItem.swift b/TelegramUI/ItemListTextWithLabelItem.swift index 9bde8160f7..594b1c2a1e 100644 --- a/TelegramUI/ItemListTextWithLabelItem.swift +++ b/TelegramUI/ItemListTextWithLabelItem.swift @@ -296,11 +296,11 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? { let textNodeFrame = self.textNode.frame if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { - if let url = attributes[NSAttributedStringKey(rawValue: TextNode.UrlAttribute)] as? String { + if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { return .url(url) - } else if let peerName = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute)] as? String { + } else if let peerName = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { return .mention(peerName) - } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute)] as? TelegramHashtag { + } else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { return .hashtag(hashtag.peerName, hashtag.hashtag) } else { return nil @@ -328,11 +328,11 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { let textNodeFrame = self.textNode.frame if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { let possibleNames: [String] = [ - TextNode.UrlAttribute, - TextNode.TelegramPeerMentionAttribute, - TextNode.TelegramPeerTextMentionAttribute, - TextNode.TelegramBotCommandAttribute, - TextNode.TelegramHashtagAttribute + TelegramTextAttributes.Url, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag ] for name in possibleNames { if let _ = attributes[NSAttributedStringKey(rawValue: name)] { diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index 3b02f2b172..e19b31f4ad 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -185,7 +185,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { let plainUrlString = NSAttributedString(string: content.displayUrl, font: descriptionFont, textColor: item.theme.list.itemAccentColor) let urlString = NSMutableAttributedString() urlString.append(plainUrlString) - urlString.addAttribute(NSAttributedStringKey(rawValue: TextNode.UrlAttribute), value: content.displayUrl, range: NSMakeRange(0, urlString.length)) + urlString.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Url), value: content.displayUrl, range: NSMakeRange(0, urlString.length)) mutableDescriptionText.append(urlString) descriptionText = mutableDescriptionText @@ -227,7 +227,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { if item.theme.list.itemAccentColor.isEqual(item.theme.list.itemPrimaryTextColor) { urlAttributedString.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length)) } - urlAttributedString.addAttribute(NSAttributedStringKey(rawValue: TextNode.UrlAttribute), value: urlString, range: NSMakeRange(0, urlAttributedString.length)) + urlAttributedString.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Url), value: urlString, range: NSMakeRange(0, urlAttributedString.length)) mutableDescriptionText.append(urlAttributedString) descriptionText = mutableDescriptionText @@ -450,7 +450,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { let textNodeFrame = self.descriptionNode.frame if let (_, attributes) = self.descriptionNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { let possibleNames: [String] = [ - TextNode.UrlAttribute, + TelegramTextAttributes.Url, ] for name in possibleNames { if let value = attributes[NSAttributedStringKey(rawValue: name)] as? String { @@ -496,7 +496,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { let textNodeFrame = self.descriptionNode.frame if let (index, attributes) = self.descriptionNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { let possibleNames: [String] = [ - TextNode.UrlAttribute + TelegramTextAttributes.Url ] for name in possibleNames { if let _ = attributes[NSAttributedStringKey(rawValue: name)] { diff --git a/TelegramUI/Locale.swift b/TelegramUI/Locale.swift new file mode 100644 index 0000000000..99d6432709 --- /dev/null +++ b/TelegramUI/Locale.swift @@ -0,0 +1,15 @@ +import Foundation + +private let systemLocaleRegionSuffix: String = { + let identifier = Locale.current.identifier + if let range = identifier.range(of: "_") { + return String(identifier[range.lowerBound...]) + } else { + return "" + } +}() + +func localeWithStrings(_ strings: PresentationStrings) -> Locale { + let code = strings.languageCode + systemLocaleRegionSuffix + return Locale(identifier: code) +} diff --git a/TelegramUI/ManagedAudioSession.swift b/TelegramUI/ManagedAudioSession.swift index 87e3cff207..ea2771dae4 100644 --- a/TelegramUI/ManagedAudioSession.swift +++ b/TelegramUI/ManagedAudioSession.swift @@ -340,7 +340,9 @@ public final class ManagedAudioSession { var deactivate = false if interruption { - deactivate = true + if self.holders[activeIndex].audioSessionType != .voiceCall { + deactivate = true + } } else { if activeIndex != self.holders.count - 1 { if self.holders[activeIndex].audioSessionType == .voiceCall { diff --git a/TelegramUI/MediaManager.swift b/TelegramUI/MediaManager.swift index 1b0f4dc3d6..ac725b61e4 100644 --- a/TelegramUI/MediaManager.swift +++ b/TelegramUI/MediaManager.swift @@ -308,7 +308,7 @@ public final class MediaManager: NSObject { var baseNowPlayingInfo: [String: Any]? self.globalControlsDisposable.set((self.globalMediaPlayerState |> deliverOnMainQueue).start(next: { stateAndType in - if let (state, _) = stateAndType, let displayData = state.item.displayData { + if let (state, type) = stateAndType, type == .music, let displayData = state.item.displayData { if previousDisplayData != displayData { previousDisplayData = displayData diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index c210234a5b..b39d8fc8cf 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -66,7 +66,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { } return false }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none) + }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none) let listNode = ChatHistoryListNode(account: account, chatLocation: .peer(updatedPlaylistPeerId), tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false)) listNode.preloadPages = true diff --git a/TelegramUI/MediaPlayer.swift b/TelegramUI/MediaPlayer.swift index 773dc00370..a57836b5d4 100644 --- a/TelegramUI/MediaPlayer.swift +++ b/TelegramUI/MediaPlayer.swift @@ -228,10 +228,10 @@ private final class MediaPlayerContext { audioStatus = audioTrackFrameBuffer.status(at: currentTimestamp) duration = max(duration, CMTimeGetSeconds(audioTrackFrameBuffer.duration)) } - let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, timestamp: min(max(timestamp, 0.0), duration), seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play)) + let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, dimensions: CGSize(), timestamp: min(max(timestamp, 0.0), duration), seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play)) self.playerStatus.set(status) } else { - let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: 0.0, timestamp: 0.0, seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play)) + let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: 0.0, dimensions: CGSize(), timestamp: 0.0, seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play)) self.playerStatus.set(status) } @@ -705,7 +705,7 @@ private final class MediaPlayerContext { if case .seeking(_, timestamp, _, _, _) = self.state { reportTimestamp = timestamp } - let status = MediaPlayerStatus(generationTimestamp: statusTimestamp, duration: duration, timestamp: min(max(reportTimestamp, 0.0), duration), seekId: self.seekId, status: playbackStatus) + let status = MediaPlayerStatus(generationTimestamp: statusTimestamp, duration: duration, dimensions: CGSize(), timestamp: min(max(reportTimestamp, 0.0), duration), seekId: self.seekId, status: playbackStatus) self.playerStatus.set(status) } @@ -766,6 +766,7 @@ enum MediaPlayerPlaybackStatus: Equatable { struct MediaPlayerStatus: Equatable { let generationTimestamp: Double let duration: Double + let dimensions: CGSize let timestamp: Double let seekId: Int let status: MediaPlayerPlaybackStatus @@ -777,6 +778,9 @@ struct MediaPlayerStatus: Equatable { if !lhs.duration.isEqual(to: rhs.duration) { return false } + if !lhs.dimensions.equalTo(rhs.dimensions) { + return false + } if !lhs.timestamp.isEqual(to: rhs.timestamp) { return false } @@ -794,7 +798,7 @@ final class MediaPlayer { private let queue = Queue() private var contextRef: Unmanaged? - private let statusValue = ValuePromise(MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused), ignoreRepeated: true) + private let statusValue = ValuePromise(MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: .paused), ignoreRepeated: true) var status: Signal { return self.statusValue.get() diff --git a/TelegramUI/MultiplexedVideoNode.swift b/TelegramUI/MultiplexedVideoNode.swift index 4e65aa7d0a..42a92a4c4a 100644 --- a/TelegramUI/MultiplexedVideoNode.swift +++ b/TelegramUI/MultiplexedVideoNode.swift @@ -142,12 +142,10 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { } private var validSize: CGSize? - override func layoutSubviews() { - super.layoutSubviews() - - if self.validSize == nil || !self.validSize!.equalTo(self.bounds.size) { - self.validSize = self.bounds.size - self.updateVisibleItems() + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + if self.validSize == nil || !self.validSize!.equalTo(size) { + self.validSize = size + self.updateVisibleItems(transition: transition) } } @@ -249,7 +247,7 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { } } - private func updateVisibleItems() { + private func updateVisibleItems(transition: ContainedViewLayoutTransition = .immediate) { let drawableSize = self.bounds.size if !drawableSize.width.isZero { var displayItems: [VisibleVideoItem] = [] diff --git a/TelegramUI/NativeVideoContent.swift b/TelegramUI/NativeVideoContent.swift index e20ad2a6ee..ac5a384027 100644 --- a/TelegramUI/NativeVideoContent.swift +++ b/TelegramUI/NativeVideoContent.swift @@ -84,6 +84,11 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private let fetchDisposable = MetaDisposable() + private var dimensions: CGSize? + private let dimensionsPromise = ValuePromise(CGSize()) + + private var validLayout: CGSize? + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, file: TelegramMediaFile, streamVideo: Bool, enableSound: Bool) { self.postbox = postbox self.file = file @@ -104,17 +109,34 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.playerNode = MediaPlayerNode(backgroundThread: false) self.player.attachPlayerNode(self.playerNode) + self.dimensions = file.dimensions + super.init() actionAtEndImpl = { [weak self] in self?.performActionAtEnd() } - self.imageNode.setSignal(mediaGridMessageVideo(postbox: postbox, video: file)) + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, video: file) |> map { [weak self] getSize, getData in + Queue.mainQueue().async { + if let strongSelf = self, strongSelf.dimensions == nil { + if let dimensions = getSize() { + strongSelf.dimensions = dimensions + strongSelf.dimensionsPromise.set(dimensions) + if let size = strongSelf.validLayout { + strongSelf.updateLayout(size: size, transition: .immediate) + } + } + } + } + return getData + }) self.addSubnode(self.imageNode) self.addSubnode(self.playerNode) - self._status.set(self.player.status) + self._status.set(combineLatest(self.dimensionsPromise.get(), self.player.status) |> map { dimensions, status in + return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: dimensions, timestamp: status.timestamp, seekId: status.seekId, status: status.status) + }) if let size = file.size { self._bufferingStatus.set(postbox.mediaBox.resourceRangesStatus(file.resource) |> map { ranges in @@ -141,7 +163,9 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - if let dimensions = self.file.dimensions { + self.validLayout = size + + if let dimensions = self.dimensions { let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0)) let makeLayout = self.imageNode.asyncLayout() let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) diff --git a/TelegramUI/OngoingCallContext.swift b/TelegramUI/OngoingCallContext.swift index 0e7e97d523..0a4f654940 100644 --- a/TelegramUI/OngoingCallContext.swift +++ b/TelegramUI/OngoingCallContext.swift @@ -18,6 +18,32 @@ private let setupLogs: Bool = { return true }() +enum OngoingCallContextState { + case initializing + case connected + case failed +} + +private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue { + private let queue: Queue + + init(queue: Queue) { + self.queue = queue + + super.init() + } + + func dispatch(_ f: @escaping () -> Void) { + self.queue.async { + f() + } + } + + func isCurrent() -> Bool { + return self.queue.isCurrent() + } +} + final class OngoingCallContext { let internalId: CallSessionInternalId @@ -27,6 +53,20 @@ final class OngoingCallContext { private var contextRef: Unmanaged? private let contextState = Promise(nil) + var state: Signal { + return self.contextState.get() |> map { + $0.flatMap { + switch $0 { + case .initializing: + return .initializing + case .connected: + return .connected + case .failed: + return .failed + } + } + } + } private let audioSessionDisposable = MetaDisposable() @@ -36,8 +76,9 @@ final class OngoingCallContext { self.internalId = internalId self.callSessionManager = callSessionManager + let queue = self.queue self.queue.async { - let context = OngoingCallThreadLocalContext() + let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue)) self.contextRef = Unmanaged.passRetained(context) context.stateChanged = { [weak self] state in self?.contextState.set(.single(state)) diff --git a/TelegramUI/OngoingCallThreadLocalContext.h b/TelegramUI/OngoingCallThreadLocalContext.h index cf7f1a6311..021d2bf60e 100644 --- a/TelegramUI/OngoingCallThreadLocalContext.h +++ b/TelegramUI/OngoingCallThreadLocalContext.h @@ -21,13 +21,20 @@ typedef NS_ENUM(int32_t, OngoingCallState) { OngoingCallStateFailed }; +@protocol OngoingCallThreadLocalContextQueue + +- (void)dispatch:(void (^ _Nonnull)())f; +- (bool)isCurrent; + +@end + @interface OngoingCallThreadLocalContext : NSObject + (void)setupLoggingFunction:(void (* _Nullable)(NSString * _Nullable))loggingFunction; @property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallState); -- (instancetype _Nonnull)init; +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue; - (void)startWithKey:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescription * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer; - (void)stop; diff --git a/TelegramUI/OngoingCallThreadLocalContext.mm b/TelegramUI/OngoingCallThreadLocalContext.mm index 062061a74e..d56a4286de 100644 --- a/TelegramUI/OngoingCallThreadLocalContext.mm +++ b/TelegramUI/OngoingCallThreadLocalContext.mm @@ -54,7 +54,73 @@ static void TGCallRandomBytes(uint8_t *buffer, size_t length) { @end +static MTAtomic *callContexts() { + static MTAtomic *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[MTAtomic alloc] initWithValue:[[NSMutableDictionary alloc] init]]; + }); + return instance; +} + +@interface OngoingCallThreadLocalContextReference : NSObject + +@property (nonatomic, weak) OngoingCallThreadLocalContext *context; +@property (nonatomic, strong, readonly) id queue; + +@end + +@implementation OngoingCallThreadLocalContextReference + +- (instancetype)initWithContext:(OngoingCallThreadLocalContext *)context queue:(id)queue { + self = [super init]; + if (self != nil) { + self.context = context; + _queue = queue; + } + return self; +} + +@end + +static int32_t nextId = 1; + +static int32_t addContext(OngoingCallThreadLocalContext *context, id queue) { + int32_t contextId = OSAtomicIncrement32(&nextId); + [callContexts() with:^id(NSMutableDictionary *dict) { + dict[@(contextId)] = [[OngoingCallThreadLocalContextReference alloc] initWithContext:context queue:queue]; + return nil; + }]; + return contextId; +} + +static void removeContext(int32_t contextId) { + [callContexts() with:^id(NSMutableDictionary *dict) { + [dict removeObjectForKey:@(contextId)]; + return nil; + }]; +} + +static void withContext(int32_t contextId, void (^f)(OngoingCallThreadLocalContext *)) { + __block OngoingCallThreadLocalContextReference *reference = nil; + [callContexts() with:^id(NSMutableDictionary *dict) { + reference = dict[@(contextId)]; + return nil; + }]; + if (reference != nil) { + [reference.queue dispatch:^{ + __strong OngoingCallThreadLocalContext *context = reference.context; + if (context != nil) { + f(context); + } + }]; + } +} + @interface OngoingCallThreadLocalContext () { + id _queue; + int32_t _contextId; + NSTimeInterval _callReceiveTimeout; NSTimeInterval _callRingTimeout; NSTimeInterval _callConnectTimeout; @@ -72,8 +138,10 @@ static void TGCallRandomBytes(uint8_t *buffer, size_t length) { @end static void controllerStateCallback(tgvoip::VoIPController *controller, int state) { - OngoingCallThreadLocalContext *context = (__bridge OngoingCallThreadLocalContext *)controller->implData; - [context controllerStateChanged:state]; + int32_t contextId = (int32_t)((intptr_t)controller->implData); + withContext(contextId, ^(OngoingCallThreadLocalContext *context) { + [context controllerStateChanged:state]; + }); } @implementation OngoingCallThreadLocalContext @@ -82,9 +150,13 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat TGVoipLoggingFunction = loggingFunction; } -- (instancetype)init { +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue { self = [super init]; if (self != nil) { + _queue = queue; + assert([queue isCurrent]); + _contextId = addContext(self, queue); + _callReceiveTimeout = 20.0; _callRingTimeout = 90.0; _callConnectTimeout = 30.0; @@ -93,7 +165,7 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat _allowP2P = true; _controller = new tgvoip::VoIPController(); - _controller->implData = (__bridge void *)self; + _controller->implData = (void *)((intptr_t)_contextId); /*releasable*/ //_controller->SetStateCallback(&controllerStateCallback); @@ -118,6 +190,14 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat return self; } +- (void)dealloc { + assert([_queue isCurrent]); + removeContext(_contextId); + if (_controller != NULL) { + [self stop]; + } +} + - (void)startWithKey:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescription * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer { std::vector endpoints; NSArray *connections = [@[primaryConnection] arrayByAddingObjectsFromArray:alternativeConnections]; diff --git a/TelegramUI/OpenUrl.swift b/TelegramUI/OpenUrl.swift index 9c325f2696..0c935ca753 100644 --- a/TelegramUI/OpenUrl.swift +++ b/TelegramUI/OpenUrl.swift @@ -49,217 +49,229 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre } } - if parsedUrl.scheme == "tg", let query = parsedUrl.query { - var convertedUrl: String? - if parsedUrl.host == "resolve" { - if let components = URLComponents(string: "/?" + query) { - var domain: String? - var start: String? - var startGroup: String? - var game: String? - var post: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "domain" { - domain = value - } else if queryItem.name == "start" { - start = value - } else if queryItem.name == "startgroup" { - startGroup = value - } else if queryItem.name == "game" { - game = value - } else if queryItem.name == "post" { - post = value + let continueHandling: () -> Void = { + if parsedUrl.scheme == "tg", let query = parsedUrl.query { + var convertedUrl: String? + if parsedUrl.host == "resolve" { + if let components = URLComponents(string: "/?" + query) { + var domain: String? + var start: String? + var startGroup: String? + var game: String? + var post: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "domain" { + domain = value + } else if queryItem.name == "start" { + start = value + } else if queryItem.name == "startgroup" { + startGroup = value + } else if queryItem.name == "game" { + game = value + } else if queryItem.name == "post" { + post = value + } } } } - } - - if let domain = domain { - var result = "https://t.me/\(domain)" - if let post = post, let postValue = Int(post) { - result += "/\(postValue)" + + if let domain = domain { + var result = "https://t.me/\(domain)" + if let post = post, let postValue = Int(post) { + result += "/\(postValue)" + } + if let start = start { + result += "?start=\(start)" + } else if let startGroup = startGroup { + result += "?startgroup=\(startGroup)" + } else if let game = game { + result += "?game=\(game)" + } + convertedUrl = result } - if let start = start { - result += "?start=\(start)" - } else if let startGroup = startGroup { - result += "?startgroup=\(startGroup)" - } else if let game = game { - result += "?game=\(game)" - } - convertedUrl = result } - } - } else if parsedUrl.host == "join" { - if let components = URLComponents(string: "/?" + query) { - var invite: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "invite" { - invite = value + } else if parsedUrl.host == "join" { + if let components = URLComponents(string: "/?" + query) { + var invite: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "invite" { + invite = value + } } } } + if let invite = invite { + convertedUrl = "https://t.me/joinchat/\(invite)" + } } - if let invite = invite { - convertedUrl = "https://t.me/joinchat/\(invite)" - } - } - } else if parsedUrl.host == "addstickers" { - if let components = URLComponents(string: "/?" + query) { - var set: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "set" { - set = value + } else if parsedUrl.host == "addstickers" { + if let components = URLComponents(string: "/?" + query) { + var set: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "set" { + set = value + } } } } + if let set = set { + convertedUrl = "https://t.me/addstickers/\(set)" + } } - if let set = set { - convertedUrl = "https://t.me/addstickers/\(set)" - } - } - } else if parsedUrl.host == "msg_url" { - if let components = URLComponents(string: "/?" + query) { - var shareUrl: String? - var shareText: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "url" { - shareUrl = value - } else if queryItem.name == "text" { - shareText = value + } else if parsedUrl.host == "msg_url" { + if let components = URLComponents(string: "/?" + query) { + var shareUrl: String? + var shareText: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "url" { + shareUrl = value + } else if queryItem.name == "text" { + shareText = value + } } } } - } - if let shareUrl = shareUrl { - let controller = PeerSelectionController(account: account) - controller.peerSelected = { [weak controller] peerId in - if let strongController = controller { - strongController.dismiss() - - let textInputState: ChatTextInputState - if let shareText = shareText, !shareText.isEmpty { - let urlString = NSMutableAttributedString(string: "\(shareUrl)\n") - let textString = NSAttributedString(string: "\(shareText)") - let selectionRange: Range = urlString.length ..< (urlString.length + textString.length) - urlString.append(textString) - textInputState = ChatTextInputState(inputText: urlString, selectionRange: selectionRange) - } else { - textInputState = ChatTextInputState(inputText: NSAttributedString(string: "\(shareUrl)")) - } - - let _ = (account.postbox.modify({ modifier -> Void in - modifier.updatePeerChatInterfaceState(peerId, update: { currentState in - if let currentState = currentState as? ChatInterfaceState { - return currentState.withUpdatedComposeInputState(textInputState) - } else { - return ChatInterfaceState().withUpdatedComposeInputState(textInputState) - } + if let shareUrl = shareUrl { + let controller = PeerSelectionController(account: account) + controller.peerSelected = { [weak controller] peerId in + if let strongController = controller { + strongController.dismiss() + + let textInputState: ChatTextInputState + if let shareText = shareText, !shareText.isEmpty { + let urlString = NSMutableAttributedString(string: "\(shareUrl)\n") + let textString = NSAttributedString(string: "\(shareText)") + let selectionRange: Range = urlString.length ..< (urlString.length + textString.length) + urlString.append(textString) + textInputState = ChatTextInputState(inputText: urlString, selectionRange: selectionRange) + } else { + textInputState = ChatTextInputState(inputText: NSAttributedString(string: "\(shareUrl)")) + } + + let _ = (account.postbox.modify({ modifier -> Void in + modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + if let currentState = currentState as? ChatInterfaceState { + return currentState.withUpdatedComposeInputState(textInputState) + } else { + return ChatInterfaceState().withUpdatedComposeInputState(textInputState) + } + }) }) - }) - |> deliverOnMainQueue).start(completed: { - navigationController?.pushViewController(ChatController(account: account, chatLocation: .peer(peerId), messageId: nil)) - }) + |> deliverOnMainQueue).start(completed: { + navigationController?.pushViewController(ChatController(account: account, chatLocation: .peer(peerId), messageId: nil)) + }) + } + } + if let navigationController = navigationController { + (navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) } } - if let navigationController = navigationController { - (navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) - } } - } - } else if parsedUrl.host == "socks" { - if let components = URLComponents(string: "/?" + query) { - var server: String? - var port: String? - var user: String? - var pass: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "server" || queryItem.name == "proxy" { - server = value - } else if queryItem.name == "port" { - port = value - } else if queryItem.name == "user" { - user = value - } else if queryItem.name == "pass" { - pass = value + } else if parsedUrl.host == "socks" { + if let components = URLComponents(string: "/?" + query) { + var server: String? + var port: String? + var user: String? + var pass: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "server" || queryItem.name == "proxy" { + server = value + } else if queryItem.name == "port" { + port = value + } else if queryItem.name == "user" { + user = value + } else if queryItem.name == "pass" { + pass = value + } } } } - } - - if let server = server, !server.isEmpty, let port = port, let _ = Int32(port) { - var result = "https://t.me/proxy?proxy=\(server)&port=\(port)" - if let user = user { - result += "&user=\(user)" - if let pass = pass { - result += "&pass=\(pass)" + + if let server = server, !server.isEmpty, let port = port, let _ = Int32(port) { + var result = "https://t.me/proxy?proxy=\(server)&port=\(port)" + if let user = user { + result += "&user=\(user)" + if let pass = pass { + result += "&pass=\(pass)" + } } + convertedUrl = result } - convertedUrl = result } } + + if let convertedUrl = convertedUrl { + let _ = (resolveUrl(account: account, url: convertedUrl) + |> deliverOnMainQueue).start(next: { resolved in + if case let .externalUrl(value) = resolved { + applicationContext.applicationBindings.openUrl(value) + } else { + openResolvedUrl(resolved, account: account, navigationController: navigationController, openPeer: { peerId, navigation in + switch navigation { + case .info: + let _ = (account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { peer in + if let infoController = peerInfoController(account: account, peer: peer) { + navigationController?.pushViewController(infoController) + } + }) + case .chat: + if let navigationController = navigationController { + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) + } + case .withBotStartPayload: + if let navigationController = navigationController { + navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) + } + } + }, present: { c, a in + if let navigationController = navigationController { + (navigationController.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a) + } + }) + } + }) + } + return } - if let convertedUrl = convertedUrl { - let _ = (resolveUrl(account: account, url: convertedUrl) - |> deliverOnMainQueue).start(next: { resolved in - if case let .externalUrl(value) = resolved { - applicationContext.applicationBindings.openUrl(value) + if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { + if #available(iOSApplicationExtension 9.0, *) { + if let window = navigationController?.view.window { + let controller = SFSafariViewController(url: parsedUrl) + if #available(iOSApplicationExtension 10.0, *) { + controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.backgroundColor + controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor + } + window.rootViewController?.present(controller, animated: true) } else { - openResolvedUrl(resolved, account: account, navigationController: navigationController, openPeer: { peerId, navigation in - switch navigation { - case .info: - let _ = (account.postbox.loadedPeerWithId(peerId) - |> deliverOnMainQueue).start(next: { peer in - if let infoController = peerInfoController(account: account, peer: peer) { - navigationController?.pushViewController(infoController) - } - }) - case .chat: - if let navigationController = navigationController { - navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) - } - case .withBotStartPayload: - if let navigationController = navigationController { - navigateToChatController(navigationController: navigationController, account: account, chatLocation: .peer(peerId)) - } - } - }, present: { c, a in - if let navigationController = navigationController { - (navigationController.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a) - } - }) + applicationContext.applicationBindings.openUrl(parsedUrl.absoluteString) } - }) - } - return - } - - if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { - if #available(iOSApplicationExtension 9.0, *) { - if let window = navigationController?.view.window { - let controller = SFSafariViewController(url: parsedUrl) - if #available(iOSApplicationExtension 10.0, *) { - controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.backgroundColor - controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor - } - window.rootViewController?.present(controller, animated: true) } else { - applicationContext.applicationBindings.openUrl(parsedUrl.absoluteString) + applicationContext.applicationBindings.openUrl(url) } } else { applicationContext.applicationBindings.openUrl(url) } + } + + if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { + applicationContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in + if !success { + continueHandling() + } + })) } else { - applicationContext.applicationBindings.openUrl(url) + continueHandling() } } diff --git a/TelegramUI/OverlayInstantVideoDecoration.swift b/TelegramUI/OverlayInstantVideoDecoration.swift index c4e3ebafb8..883fceef6d 100644 --- a/TelegramUI/OverlayInstantVideoDecoration.swift +++ b/TelegramUI/OverlayInstantVideoDecoration.swift @@ -14,6 +14,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { private let shadowNode: ASImageNode private let foregroundContainerNode: ASDisplayNode + private let progressNode: InstantVideoRadialStatusNode private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? private var contentNodeSnapshot: UIView? @@ -33,10 +34,9 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { self.contentContainerNode.clipsToBounds = true self.foregroundContainerNode = ASDisplayNode() - //self.foregroundContainerNode.addSubnode(self.controlsNode) + self.progressNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.8)) + self.foregroundContainerNode.addSubnode(self.progressNode) self.foregroundNode = self.foregroundContainerNode - - //self.controlsNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(controlsNodeTapGesture(_:)))) } func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) { @@ -51,6 +51,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { } if let contentNode = contentNode { + self.progressNode.status = contentNode.status if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) if let validLayoutSize = self.validLayoutSize { @@ -82,11 +83,13 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration { let shadowInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0) transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: -shadowInsets.left, y: -shadowInsets.top), size: CGSize(width: size.width + shadowInsets.left + shadowInsets.right, height: size.height + shadowInsets.top + shadowInsets.bottom))) - transition.updateFrame(node: self.foregroundContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + let foregroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) + transition.updateFrame(node: self.foregroundContainerNode, frame: foregroundFrame) + transition.updateFrame(node: self.progressNode, frame: foregroundFrame.insetBy(dx: 0.0, dy: 0.0)) transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: size)) if let contentNode = self.contentNode { - transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -0.5, dy: -0.5)) contentNode.updateLayout(size: size, transition: transition) } diff --git a/TelegramUI/OverlayPlayerControllerNode.swift b/TelegramUI/OverlayPlayerControllerNode.swift index 39a6fbd2db..3c83208991 100644 --- a/TelegramUI/OverlayPlayerControllerNode.swift +++ b/TelegramUI/OverlayPlayerControllerNode.swift @@ -56,7 +56,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec } }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in - }, canSetupReply: { + }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none) diff --git a/TelegramUI/OverlayPlayerControlsNode.swift b/TelegramUI/OverlayPlayerControlsNode.swift index 4c82e4022f..dba16e98d8 100644 --- a/TelegramUI/OverlayPlayerControlsNode.swift +++ b/TelegramUI/OverlayPlayerControlsNode.swift @@ -180,7 +180,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { if let value = value { return value.status } else { - return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) + return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: .paused) } } self.scrubberNode.status = mappedStatus diff --git a/TelegramUI/OverlayVideoDecoration.swift b/TelegramUI/OverlayVideoDecoration.swift index ad2feb0863..7eb0156bd2 100644 --- a/TelegramUI/OverlayVideoDecoration.swift +++ b/TelegramUI/OverlayVideoDecoration.swift @@ -136,7 +136,7 @@ final class OverlayVideoDecoration: UniversalVideoDecoration { if let value = value { return value } else { - return MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) + return MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: 0.0, dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: .paused) } } } diff --git a/TelegramUI/PasscodeOptionsController.swift b/TelegramUI/PasscodeOptionsController.swift index 6ef7aadf0b..e0253c6310 100644 --- a/TelegramUI/PasscodeOptionsController.swift +++ b/TelegramUI/PasscodeOptionsController.swift @@ -60,7 +60,7 @@ private enum PasscodeOptionsEntry: ItemListNodeEntry { case .touchId: return 4 case .simplePasscode: - return 4 + return 5 } } diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index f1f6608fa2..c659660b5e 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -212,7 +212,7 @@ public class PeerMediaCollectionController: TelegramController { }, openSearch: { [weak self] in self?.activateSearch() }, setupReply: { _ in - }, canSetupReply: { + }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none) diff --git a/TelegramUI/PeerMediaCollectionControllerNode.swift b/TelegramUI/PeerMediaCollectionControllerNode.swift index eacd95e646..3fea0dfc52 100644 --- a/TelegramUI/PeerMediaCollectionControllerNode.swift +++ b/TelegramUI/PeerMediaCollectionControllerNode.swift @@ -120,7 +120,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { self.historyEmptyNode = PeerMediaCollectionEmptyNode(mode: self.mediaCollectionInterfaceState.mode, theme: self.presentationData.theme, strings: self.presentationData.strings) self.historyEmptyNode.isHidden = true - self.chatPresentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, fontSize: self.presentationData.fontSize, accountPeerId: account.peerId, mode: .standard, chatLocation: .peer(self.peerId)) + self.chatPresentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, fontSize: self.presentationData.fontSize, accountPeerId: account.peerId, mode: .standard(previewing: false), chatLocation: .peer(self.peerId)) super.init() diff --git a/TelegramUI/PeerMessagesMediaPlaylist.swift b/TelegramUI/PeerMessagesMediaPlaylist.swift index 0860abc40b..02695959c5 100644 --- a/TelegramUI/PeerMessagesMediaPlaylist.swift +++ b/TelegramUI/PeerMessagesMediaPlaylist.swift @@ -88,7 +88,12 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { if isVoice { return SharedMediaPlaybackDisplayData.voice(author: self.message.author, peer: self.message.peers[self.message.id.peerId]) } else { - return SharedMediaPlaybackDisplayData.music(title: title, performer: performer, albumArt: SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: false))) + var updatedTitle = title + let updatedPerformer = performer + if (title ?? "").isEmpty && (performer ?? "").isEmpty { + updatedTitle = file.fileName ?? "" + } + return SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: updatedTitle ?? "", performer: updatedPerformer ?? "", isThumbnail: false))) } case let .Video(_, _, flags): if flags.contains(.instantRoundVideo) { diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 25b0209000..a576946278 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -246,7 +246,9 @@ private func chatMessageVideoDatas(postbox: Postbox, file: TelegramMediaFile, th } } } - } |> filter({ $0.0 != nil || $0.1 != nil }) + } |> filter({ + return $0.0 != nil || $0.1 != nil + }) return signal } else { @@ -1129,10 +1131,30 @@ func gifPaneVideoThumbnail(account: Account, video: TelegramMediaFile) -> Signal } func mediaGridMessageVideo(postbox: Postbox, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return internalMediaGridMessageVideo(postbox: postbox, video: video) |> map { + return $0.1 + } +} + +func internalMediaGridMessageVideo(postbox: Postbox, video: TelegramMediaFile) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { let signal = chatMessageVideoDatas(postbox: postbox, file: video) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in - return { arguments in + return ({ + var fullSizeImage: CGImage? + if let fullSizeData = fullSizeData { + if fullSizeComplete { + let options = NSMutableDictionary() + if let imageSource = CGImageSourceCreateWithData(fullSizeData.0 as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + } + } + if let fullSizeImage = fullSizeImage { + return CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height)) + } + return nil + }, { arguments in assertNotOnMainThread() let context = DrawingContext(size: arguments.drawingSize, clear: true) @@ -1238,7 +1260,7 @@ func mediaGridMessageVideo(postbox: Postbox, video: TelegramMediaFile) -> Signal addCorners(context, arguments: arguments) return context - } + }) } } diff --git a/TelegramUI/PresentationCall.swift b/TelegramUI/PresentationCall.swift index 603bd06d54..42b22529ff 100644 --- a/TelegramUI/PresentationCall.swift +++ b/TelegramUI/PresentationCall.swift @@ -2,15 +2,16 @@ import Foundation import Postbox import TelegramCore import SwiftSignalKit +import AVFoundation public enum PresentationCallState: Equatable { case waiting case ringing case requesting(Bool) - case connecting + case connecting(Data?) case active(Double, Data) case terminating - case terminated + case terminated(CallSessionTerminationReason?) public static func ==(lhs: PresentationCallState, rhs: PresentationCallState) -> Bool { switch lhs { @@ -50,8 +51,8 @@ public enum PresentationCallState: Equatable { } else { return false } - case .terminated: - if case .terminated = rhs { + case let .terminated(lhsReason): + if case let .terminated(rhsReason) = rhs, lhsReason == rhsReason { return true } else { return false @@ -60,6 +61,117 @@ public enum PresentationCallState: Equatable { } } +private final class PresentationCallToneRenderer { + let queue: Queue + + let tone: PresentationCallTone + + private let toneRenderer: MediaPlayerAudioRenderer + private var toneRendererAudioSession: MediaPlayerAudioSessionCustomControl? + private var toneRendererAudioSessionActivated = false + + init(tone: PresentationCallTone) { + let queue = Queue.mainQueue() + self.queue = queue + + self.tone = tone + let data = presentationCallToneData(tone) + + var controlImpl: ((MediaPlayerAudioSessionCustomControl) -> Disposable)? + + self.toneRenderer = MediaPlayerAudioRenderer(audioSession: .custom({ control in + return controlImpl?(control) ?? EmptyDisposable + }), playAndRecord: false, forceAudioToSpeaker: false, updatedRate: {}, audioPaused: {}) + + controlImpl = { [weak self] control in + queue.async { + if let strongSelf = self { + strongSelf.toneRendererAudioSession = control + if strongSelf.toneRendererAudioSessionActivated { + control.activate() + } + } + } + return ActionDisposable { + } + } + + let toneDataOffset = Atomic(value: 0) + self.toneRenderer.beginRequestingFrames(queue: DispatchQueue.global(), takeFrame: { + guard let toneData = data else { + return .finished + } + + let frameSize = 44100 + + var takeOffset: Int? + let _ = toneDataOffset.modify { current in + takeOffset = current + return current + frameSize + } + + if let takeOffset = takeOffset { + var blockBuffer: CMBlockBuffer? + + let bytes = malloc(frameSize)! + toneData.withUnsafeBytes { (dataBytes: UnsafePointer) -> Void in + var takenCount = 0 + while takenCount < frameSize { + let dataOffset = (takeOffset + takenCount) % toneData.count + let dataCount = min(frameSize, toneData.count - dataOffset) + memcpy(bytes, dataBytes.advanced(by: dataOffset), dataCount) + takenCount += dataCount + } + } + let status = CMBlockBufferCreateWithMemoryBlock(nil, bytes, frameSize, nil, nil, 0, frameSize, 0, &blockBuffer) + if status != noErr { + return .finished + } + + let sampleCount = frameSize / 2 + + let pts = CMTime(value: Int64(takeOffset / 2), timescale: 44100) + var timingInfo = CMSampleTimingInfo(duration: CMTime(value: Int64(sampleCount), timescale: 44100), presentationTimeStamp: pts, decodeTimeStamp: pts) + var sampleBuffer: CMSampleBuffer? + var sampleSize = frameSize + guard CMSampleBufferCreate(nil, blockBuffer, true, nil, nil, nil, 1, 1, &timingInfo, 1, &sampleSize, &sampleBuffer) == noErr else { + return .finished + } + + if let sampleBuffer = sampleBuffer { + return .frame(MediaTrackFrame(type: .audio, sampleBuffer: sampleBuffer, resetDecoder: false, decoded: true)) + } else { + return .finished + } + } else { + return .finished + } + }) + self.toneRenderer.start() + self.toneRenderer.setRate(1.0) + } + + deinit { + assert(self.queue.isCurrent()) + self.toneRenderer.stop() + } + + func setAudioSessionActive(_ value: Bool) { + if self.toneRendererAudioSessionActivated != value { + self.toneRendererAudioSessionActivated = value + if let control = self.toneRendererAudioSession { + if value { + self.toneRenderer.setRate(1.0) + control.activate() + } else { + self.toneRenderer.setRate(0.0) + control.deactivate() + } + } + } + } +} + public final class PresentationCall { private let audioSession: ManagedAudioSession private let callSessionManager: CallSessionManager @@ -71,7 +183,10 @@ public final class PresentationCall { let peer: Peer? private var sessionState: CallSession? + private var callContextState: OngoingCallContextState? private var ongoingGontext: OngoingCallContext + private var ongoingGontextStateDisposable: Disposable? + private var reportedIncomingCall = false private var sessionStateDisposable: Disposable? @@ -104,6 +219,16 @@ public final class PresentationCall { private var audioSessionControl: ManagedAudioSessionControl? private var audioSessionDisposable: Disposable? + private let audioSessionShouldBeActive = ValuePromise(false, ignoreRepeated: true) + private var audioSessionShouldBeActiveDisposable: Disposable? + private let audioSessionActive = Promise(false) + private var audioSessionActiveDisposable: Disposable? + private var isAudioSessionActive = false + + private var toneRenderer: PresentationCallToneRenderer? + + private var droppedCall = false + private var dropCallKitCallTimer: SwiftSignalKit.Timer? init(audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?) { self.audioSession = audioSession @@ -118,17 +243,28 @@ public final class PresentationCall { self.ongoingGontext = OngoingCallContext(callSessionManager: self.callSessionManager, internalId: self.internalId) self.sessionStateDisposable = (callSessionManager.callState(internalId: internalId) - |> deliverOnMainQueue).start(next: { [weak self] sessionState in - if let strongSelf = self { - strongSelf.updateSessionState(sessionState: sessionState, audioSessionControl: strongSelf.audioSessionControl) + |> deliverOnMainQueue).start(next: { [weak self] sessionState in + if let strongSelf = self { + strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, audioSessionControl: strongSelf.audioSessionControl) + } + }) + + self.ongoingGontextStateDisposable = (self.ongoingGontext.state + |> deliverOnMainQueue).start(next: { [weak self] contextState in + if let strongSelf = self { + if let sessionState = strongSelf.sessionState { + strongSelf.updateSessionState(sessionState: sessionState, callContextState: contextState, audioSessionControl: strongSelf.audioSessionControl) + } else { + strongSelf.callContextState = contextState } - }) + } + }) self.audioSessionDisposable = audioSession.push(audioSessionType: .voiceCall, manualActivate: { [weak self] control in Queue.mainQueue().async { if let strongSelf = self { if let sessionState = strongSelf.sessionState { - strongSelf.updateSessionState(sessionState: sessionState, audioSessionControl: control) + strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, audioSessionControl: control) } else { strongSelf.audioSessionControl = control } @@ -138,8 +274,9 @@ public final class PresentationCall { return Signal { subscriber in Queue.mainQueue().async { if let strongSelf = self { + strongSelf.updateIsAudioSessionActive(false) if let sessionState = strongSelf.sessionState { - strongSelf.updateSessionState(sessionState: sessionState, audioSessionControl: nil) + strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, audioSessionControl: nil) } else { strongSelf.audioSessionControl = nil } @@ -149,20 +286,71 @@ public final class PresentationCall { return EmptyDisposable } }) + + self.audioSessionShouldBeActiveDisposable = (self.audioSessionShouldBeActive.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + if value { + if let audioSessionControl = strongSelf.audioSessionControl { + let audioSessionActive: Signal + if let callKitIntegration = strongSelf.callKitIntegration { + audioSessionActive = callKitIntegration.audioSessionActive |> filter { $0 } |> timeout(2.0, queue: Queue.mainQueue(), alternate: Signal { subscriber in + if let strongSelf = self, let audioSessionControl = strongSelf.audioSessionControl { + audioSessionControl.activate({ _ in }) + } + subscriber.putNext(true) + subscriber.putCompletion() + return EmptyDisposable + }) + } else { + audioSessionControl.activate({ _ in }) + audioSessionActive = .single(true) + } + strongSelf.audioSessionActive.set(audioSessionActive) + } else { + strongSelf.audioSessionActive.set(.single(false)) + } + } else { + strongSelf.audioSessionActive.set(.single(false)) + } + } + }) + + self.audioSessionActiveDisposable = (self.audioSessionActive.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + strongSelf.updateIsAudioSessionActive(value) + } + }) } deinit { + self.audioSessionShouldBeActiveDisposable?.dispose() + self.audioSessionActiveDisposable?.dispose() self.sessionStateDisposable?.dispose() + self.ongoingGontextStateDisposable?.dispose() self.audioSessionDisposable?.dispose() + + if let dropCallKitCallTimer = self.dropCallKitCallTimer { + dropCallKitCallTimer.invalidate() + if !self.droppedCall { + self.callKitIntegration?.dropCall(uuid: self.internalId) + } + } } - private func updateSessionState(sessionState: CallSession, audioSessionControl: ManagedAudioSessionControl?) { + private func updateSessionState(sessionState: CallSession, callContextState: OngoingCallContextState?, audioSessionControl: ManagedAudioSessionControl?) { let previous = self.sessionState let previousControl = self.audioSessionControl self.sessionState = sessionState + self.callContextState = callContextState self.audioSessionControl = audioSessionControl - let presentationState: PresentationCallState + if previousControl != nil && audioSessionControl == nil { + print("updateSessionState \(sessionState.state) \(audioSessionControl != nil)") + } + + let presentationState: PresentationCallState? var wasActive = false var wasTerminated = false @@ -186,59 +374,71 @@ public final class PresentationCall { case .ringing: presentationState = .ringing if let _ = audioSessionControl, previous == nil || previousControl == nil { - self.callKitIntegration?.reportIncomingCall(uuid: self.internalId, handle: "\(self.peerId.id)", displayTitle: self.peer?.displayTitle ?? "Unknown", completion: { [weak self] error in - if error != nil { - Queue.mainQueue().async { - if let strongSelf = self { - strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp) + if !self.reportedIncomingCall { + self.reportedIncomingCall = true + self.callKitIntegration?.reportIncomingCall(uuid: self.internalId, handle: "\(self.peerId.id)", displayTitle: self.peer?.displayTitle ?? "Unknown", completion: { [weak self] error in + if let error = error { + Logger.shared.log("PresentationCall", "reportIncomingCall error \(error)") + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp) + } } } - } - }) + }) + } } case .accepting: - presentationState = .connecting + presentationState = .connecting(nil) case .dropping: presentationState = .terminating - case .terminated: - presentationState = .terminated + case let .terminated(reason, _): + presentationState = .terminated(reason) case let .requesting(ringing): presentationState = .requesting(ringing) case let .active(_, keyVisualHash, _, _): - let timestamp: Double - if let activeTimestamp = self.activeTimestamp { - timestamp = activeTimestamp + if let callContextState = callContextState { + switch callContextState { + case .initializing: + presentationState = .connecting(keyVisualHash) + case .failed: + presentationState = nil + self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect) + case .connected: + let timestamp: Double + if let activeTimestamp = self.activeTimestamp { + timestamp = activeTimestamp + } else { + timestamp = CFAbsoluteTimeGetCurrent() + self.activeTimestamp = timestamp + } + presentationState = .active(timestamp, keyVisualHash) + } } else { - timestamp = CFAbsoluteTimeGetCurrent() - self.activeTimestamp = timestamp + presentationState = .connecting(keyVisualHash) } - presentationState = .active(timestamp, keyVisualHash) } switch sessionState.state { + case .requesting: + if let _ = audioSessionControl { + self.audioSessionShouldBeActive.set(true) + } case let .active(key, _, connections, maxLayer): - if let audioSessionControl = audioSessionControl, !wasActive || previousControl == nil { - let audioSessionActive: Signal - if let callKitIntegration = self.callKitIntegration { - audioSessionActive = callKitIntegration.audioSessionActive |> filter { $0 } |> timeout(2.0, queue: Queue.mainQueue(), alternate: Signal { [weak self] subscriber in - if let strongSelf = self, let audioSessionControl = strongSelf.audioSessionControl { - audioSessionControl.activate({ _ in }) - } - subscriber.putNext(true) - subscriber.putCompletion() - return EmptyDisposable - }) - } else { - audioSessionControl.activate({ _ in }) - audioSessionActive = .single(true) - } - - self.ongoingGontext.start(key: key, isOutgoing: sessionState.isOutgoing, connections: connections, maxLayer: maxLayer, audioSessionActive: audioSessionActive) + self.audioSessionShouldBeActive.set(true) + if let _ = audioSessionControl, !wasActive || previousControl == nil { + self.ongoingGontext.start(key: key, isOutgoing: sessionState.isOutgoing, connections: connections, maxLayer: maxLayer, audioSessionActive: self.audioSessionActive.get()) if sessionState.isOutgoing { self.callKitIntegration?.reportOutgoingCallConnected(uuid: sessionState.id, at: Date()) } } + case .terminated: + self.audioSessionShouldBeActive.set(true) + if wasActive { + self.ongoingGontext.stop() + } default: + self.audioSessionShouldBeActive.set(false) if wasActive { self.ongoingGontext.stop() } @@ -249,9 +449,70 @@ public final class PresentationCall { self.canBeRemovedPromise.set(.single(true) |> delay(2.0, queue: Queue.mainQueue())) } self.hungUpPromise.set(true) - self.callKitIntegration?.dropCall(uuid: self.internalId) + if sessionState.isOutgoing { + if !self.droppedCall && self.dropCallKitCallTimer == nil { + let dropCallKitCallTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.dropCallKitCallTimer = nil + if !strongSelf.droppedCall { + strongSelf.droppedCall = true + strongSelf.callKitIntegration?.dropCall(uuid: strongSelf.internalId) + } + } + }, queue: Queue.mainQueue()) + self.dropCallKitCallTimer = dropCallKitCallTimer + dropCallKitCallTimer.start() + } + } else { + self.callKitIntegration?.dropCall(uuid: self.internalId) + } + } + if let presentationState = presentationState { + self.statePromise.set(presentationState) + self.updateTone(presentationState) + } + } + + private func updateTone(_ state: PresentationCallState) { + var tone: PresentationCallTone? + switch state { + case .connecting: + tone = .connecting + case .requesting(true): + tone = .ringing + case let .terminated(reason): + if let reason = reason { + switch reason { + case let .ended(type): + switch type { + case .busy: + tone = .busy + case .hungUp, .missed: + tone = .ended + } + case .error: + tone = .failed + } + } + default: + break + } + if tone != self.toneRenderer?.tone { + if let tone = tone { + let toneRenderer = PresentationCallToneRenderer(tone: tone) + self.toneRenderer = toneRenderer + toneRenderer.setAudioSessionActive(self.isAudioSessionActive) + } else { + self.toneRenderer = nil + } + } + } + + private func updateIsAudioSessionActive(_ value: Bool) { + if self.isAudioSessionActive != value { + self.isAudioSessionActive = value + self.toneRenderer?.setAudioSessionActive(value) } - self.statePromise.set(presentationState) } func answer() { diff --git a/TelegramUI/PresentationCallToneData.swift b/TelegramUI/PresentationCallToneData.swift new file mode 100644 index 0000000000..7baf4785a6 --- /dev/null +++ b/TelegramUI/PresentationCallToneData.swift @@ -0,0 +1,89 @@ +import Foundation +import AVFoundation + +private func loadToneData(name: String) -> Data? { + let outputSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatLinearPCM as NSNumber, + AVSampleRateKey: 44100.0 as NSNumber, + AVLinearPCMBitDepthKey: 16 as NSNumber, + AVLinearPCMIsNonInterleaved: false as NSNumber, + AVLinearPCMIsFloatKey: false as NSNumber, + AVLinearPCMIsBigEndianKey: false as NSNumber, + AVNumberOfChannelsKey: 2 as NSNumber + ] + + let nsName: NSString = name as NSString + let baseName: String + let nameExtension: String + let pathExtension = nsName.pathExtension + if pathExtension.isEmpty { + baseName = name + nameExtension = "caf" + } else { + baseName = nsName.substring(with: NSRange(location: 0, length: (name.count - pathExtension.count - 1))) + nameExtension = pathExtension + } + + guard let url = Bundle.main.url(forResource: baseName, withExtension: nameExtension) else { + return nil + } + + let asset = AVURLAsset(url: url) + + guard let assetReader = try? AVAssetReader(asset: asset) else { + return nil + } + + let readerOutput = AVAssetReaderAudioMixOutput(audioTracks: asset.tracks, audioSettings: outputSettings) + + if !assetReader.canAdd(readerOutput) { + return nil + } + + assetReader.add(readerOutput) + + if !assetReader.startReading() { + return nil + } + + var data = Data() + + while assetReader.status == .reading { + if let nextBuffer = readerOutput.copyNextSampleBuffer() { + var abl = AudioBufferList() + var blockBuffer: CMBlockBuffer? = nil + CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(nextBuffer, nil, &abl, MemoryLayout.size, nil, nil, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer) + let size = Int(CMSampleBufferGetTotalSampleSize(nextBuffer)) + if size != 0, let mData = abl.mBuffers.mData { + data.append(Data(bytes: mData, count: size)) + } + } else { + break + } + } + + return data +} + +enum PresentationCallTone { + case ringing + case connecting + case busy + case failed + case ended +} + +func presentationCallToneData(_ tone: PresentationCallTone) -> Data? { + switch tone { + case .ringing: + return loadToneData(name: "voip_ringback.caf") + case .connecting: + return loadToneData(name: "voip_connecting.mp3") + case .busy: + return loadToneData(name: "voip_busy.caf") + case .failed: + return loadToneData(name: "voip_fail.caf") + case .ended: + return loadToneData(name: "voip_end.caf") + } +} diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift index fed9a66174..1ccd4f0d84 100644 --- a/TelegramUI/PresentationResourceKey.swift +++ b/TelegramUI/PresentationResourceKey.swift @@ -57,6 +57,9 @@ enum PresentationResourceKey: Int32 { case chatListVerifiedIcon case chatListSecretIcon + case chatTitleLockIcon + case chatTitleMuteIcon + case chatPrincipalThemeEssentialGraphics case chatBubbleVerticalLineIncomingImage case chatBubbleVerticalLineOutgoingImage @@ -125,10 +128,10 @@ enum PresentationResourceKey: Int32 { case chatInputPanelApplyButtonImage case chatInputPanelVoiceButtonImage case chatInputPanelVideoButtonImage + case chatInputPanelExpandButtonImage case chatInputPanelVoiceActiveButtonImage case chatInputPanelVideoActiveButtonImage case chatInputPanelAttachmentButtonImage - case chatInputPanelExpandButtonImage case chatInputPanelMediaRecordingDotImage case chatInputPanelMediaRecordingCancelArrowImage case chatInputTextFieldStickersImage diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift index 77b43bd017..67ccd7e921 100644 --- a/TelegramUI/PresentationResourcesChat.swift +++ b/TelegramUI/PresentationResourcesChat.swift @@ -36,6 +36,49 @@ private func generateInputPanelButtonBackgroundImage(fillColor: UIColor, strokeC } struct PresentationResourcesChat { + /* + + + + Created with Sketch. + + + + + + + + + */ + + static func chatTitleLockIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatTitleLockIcon.rawValue, { theme in + return generateImage(CGSize(width: 9.0, height: 13.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.translateBy(x: 0.0, y: 1.0) + + context.setFillColor(theme.rootController.navigationBar.primaryTextColor.cgColor) + context.setStrokeColor(theme.rootController.navigationBar.primaryTextColor.cgColor) + context.setLineWidth(1.32) + + let _ = try? drawSvgPath(context, path: "M4.5,0.600000024 C5.88071187,0.600000024 7,1.88484952 7,3.46979169 L7,7.39687502 C7,8.9818172 5.88071187,10.2666667 4.5,10.2666667 C3.11928813,10.2666667 2,8.9818172 2,7.39687502 L2,3.46979169 C2,1.88484952 3.11928813,0.600000024 4.5,0.600000024 S ") + let _ = try? drawSvgPath(context, path: "M1.32,5.65999985 L7.68,5.65999985 C8.40901587,5.65999985 9,6.25098398 9,6.97999985 L9,10.6733332 C9,11.4023491 8.40901587,11.9933332 7.68,11.9933332 L1.32,11.9933332 C0.59098413,11.9933332 1.11022302e-16,11.4023491 0,10.6733332 L2.22044605e-16,6.97999985 C1.11022302e-16,6.25098398 0.59098413,5.65999985 1.32,5.65999985 Z ") + }) + }) + } + + static func chatTitleMuteIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatTitleMuteIcon.rawValue, { theme in + return generateImage(CGSize(width: 9.0, height: 9.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.rootController.navigationBar.controlColor.cgColor) + + let _ = try? drawSvgPath(context, path: "M2.97607626,2.27306995 L5.1424026,0.18411241 C5.25492443,0.0756092198 5.40753677,0.0146527621 5.56666667,0.0146527621 C5.89803752,0.0146527621 6.16666667,0.273688014 6.16666667,0.593224191 L6.16666667,5.47790407 L8.86069303,8.18395735 C9.05193038,8.37604845 9.04547086,8.68126082 8.84626528,8.86566828 C8.6470597,9.05007573 8.33054317,9.0438469 8.13930581,8.85175581 L0.139306972,0.816042647 C-0.0519303838,0.623951552 -0.0454708626,0.318739175 0.153734717,0.134331724 C0.352940296,-0.0500757275 0.669456833,-0.0438469035 0.860694189,0.148244192 L2.97607626,2.27306995 Z M0.933196438,2.75856564 L6.16666667,8.01539958 L6.16666667,8.40677707 C6.16666667,8.56022375 6.10345256,8.70738566 5.99093074,8.81588885 C5.75661616,9.04183505 5.37671717,9.04183505 5.1424026,8.81588885 L2.59763107,6.36200202 C2.53511895,6.30172247 2.45033431,6.26785777 2.36192881,6.26785777 L1.16666667,6.26785777 C0.614381917,6.26785777 0.166666667,5.83613235 0.166666667,5.30357206 L0.166666667,3.6964292 C0.166666667,3.24138962 0.493527341,2.85996592 0.933196438,2.75856564 Z ") + }) + }) + } + static func principalGraphics(_ theme: PresentationTheme) -> PrincipalThemeEssentialGraphics { return theme.object(PresentationResourceKey.chatPrincipalThemeEssentialGraphics.rawValue, { theme in return PrincipalThemeEssentialGraphics(theme.chat) diff --git a/TelegramUI/ReplyAccessoryPanelNode.swift b/TelegramUI/ReplyAccessoryPanelNode.swift index 9cec07cfa0..dce901bf5a 100644 --- a/TelegramUI/ReplyAccessoryPanelNode.swift +++ b/TelegramUI/ReplyAccessoryPanelNode.swift @@ -72,6 +72,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { var updatedMedia: Media? var imageDimensions: CGSize? + var isRoundImage = false if let message = message, !message.containsSecretMedia { for media in message.media { if let image = media as? TelegramMediaImage { @@ -82,7 +83,8 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { break } else if let file = media as? TelegramMediaFile { updatedMedia = file - if !file.isInstantVideo, let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { + isRoundImage = file.isInstantVideo + if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { imageDimensions = representation.dimensions } break @@ -94,7 +96,11 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { var applyImage: (() -> Void)? if let imageDimensions = imageDimensions { let boundingSize = CGSize(width: 35.0, height: 35.0) - applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) + var radius: CGFloat = 2.0 + if isRoundImage { + radius = floor(boundingSize.width / 2.0) + } + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) } var mediaUpdated = false diff --git a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift index 37c3c945a7..7d486d506d 100644 --- a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift +++ b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift @@ -61,4 +61,8 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { return panelHeight } + + override func minimalHeight(interfaceState: ChatPresentationInterfaceState) -> CGFloat { + return 47.0 + } } diff --git a/TelegramUI/SecretChatKeyController.swift b/TelegramUI/SecretChatKeyController.swift new file mode 100644 index 0000000000..042093d151 --- /dev/null +++ b/TelegramUI/SecretChatKeyController.swift @@ -0,0 +1,46 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramCore +import Postbox + +final class SecretChatKeyController: ViewController { + private var controllerNode: SecretChatKeyControllerNode { + return self.displayNode as! SecretChatKeyControllerNode + } + + private let account: Account + private let fingerprint: SecretChatKeyFingerprint + private let peer: Peer + + private var presentationData: PresentationData + + init(account: Account, fingerprint: SecretChatKeyFingerprint, peer: Peer) { + self.account = account + self.fingerprint = fingerprint + self.peer = peer + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + self.title = self.presentationData.strings.EncryptionKey_Title + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = SecretChatKeyControllerNode(account: self.account, presentationData: self.presentationData, fingerprint: self.fingerprint, peer: self.peer, getNavigationController: { [weak self] in + return self?.navigationController as? NavigationController + }) + self.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } +} diff --git a/TelegramUI/SecretChatKeyControllerNode.swift b/TelegramUI/SecretChatKeyControllerNode.swift new file mode 100644 index 0000000000..4dc1a64026 --- /dev/null +++ b/TelegramUI/SecretChatKeyControllerNode.swift @@ -0,0 +1,160 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramCore +import Postbox + +private func processHexString(_ string: String) -> String { + var result = "" + var i = 0 + for c in string { + if i % 2 == 0 && i != 0 { + result.append(" ") + } + if i % 8 == 0 && i != 0 { + result.append(" ") + } + result.append(c) + i += 1 + } + return result +} + +final class SecretChatKeyControllerNode: ViewControllerTracingNode { + private let account: Account + private var presentationData: PresentationData + private let fingerprint: SecretChatKeyFingerprint + private let peer: Peer + private let getNavigationController: () -> NavigationController? + + private let scrollNode: ASScrollNode + private let imageNode: ASImageNode + private let keyTextNode: TextNode + private let infoNode: TextNode + + private var validImageSize: CGSize? + + init(account: Account, presentationData: PresentationData, fingerprint: SecretChatKeyFingerprint, peer: Peer, getNavigationController: @escaping () -> NavigationController?) { + self.account = account + self.presentationData = presentationData + self.fingerprint = fingerprint + self.peer = peer + self.getNavigationController = getNavigationController + + self.scrollNode = ASScrollNode() + + self.imageNode = ASImageNode() + self.imageNode.isLayerBacked = true + self.imageNode.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + + self.keyTextNode = TextNode() + self.keyTextNode.isLayerBacked = true + self.keyTextNode.displaysAsynchronously = false + + self.infoNode = TextNode() + self.infoNode.displaysAsynchronously = false + + super.init() + + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.scrollNode) + self.scrollNode.addSubnode(self.imageNode) + self.scrollNode.addSubnode(self.keyTextNode) + self.scrollNode.addSubnode(self.infoNode) + } + + override func didLoad() { + super.didLoad() + + self.infoNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.infoTap(_:)))) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + var insets = layout.insets(options: [.input]) + insets.top += navigationBarHeight + + self.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top)) + + let sideInset: CGFloat = 10.0 + + var imageSize = CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.width - sideInset * 2.0) + if imageSize.height > layout.size.height - insets.top - sideInset * 2.0 - 100.0 { + let side = layout.size.height - insets.top - sideInset * 2.0 - 100.0 + imageSize = CGSize(width: side, height: side) + } + if imageSize.height > 512.0 { + imageSize = CGSize(width: 512.0, height: 512.0) + } + if self.validImageSize != imageSize { + self.validImageSize = imageSize + self.imageNode.image = secretChatKeyImage(self.fingerprint, size: imageSize) + } + + let makeKeyTextLayout = TextNode.asyncLayout(self.keyTextNode) + let makeInfoLayout = TextNode.asyncLayout(self.infoNode) + + let keySignatureData = self.fingerprint.sha1.data() + let additionalSignature = self.fingerprint.sha256.data() + + var data = Data() + data.append(keySignatureData) + data.append(additionalSignature) + + let s1: String = (data.subdata(in: 0 ..< 8) as NSData).stringByEncodingInHex() + let s2: String = (data.subdata(in: 8 ..< 16) as NSData).stringByEncodingInHex() + + let s3: String = (additionalSignature.subdata(in: 0 ..< 8) as NSData).stringByEncodingInHex() + let s4: String = (additionalSignature.subdata(in : 8 ..< 16) as NSData).stringByEncodingInHex() + + let text: String = "\(processHexString(s1))\n\(processHexString(s2))\n\(processHexString(s3))\n\(processHexString(s4))" + + let (keyTextLayout, keyTextApply) = makeKeyTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: Font.semiboldMonospace(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: layout.size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (infoRaw, infoRanges) = self.presentationData.strings.EncryptionKey_Description(self.peer.compactDisplayTitle, self.peer.compactDisplayTitle) + let infoText = NSMutableAttributedString(string: infoRaw, attributes: [.font: Font.regular(14.0), .foregroundColor: self.presentationData.theme.list.itemPrimaryTextColor]) + + for (_, range) in infoRanges { + infoText.addAttributes([.font: Font.semibold(14.0)], range: range) + } + + let linkRange = (infoRaw as NSString).range(of: "telegram.org") + if linkRange.location != NSNotFound { + infoText.addAttributes([.foregroundColor: self.presentationData.theme.list.itemAccentColor, NSAttributedStringKey(rawValue: TelegramTextAttributes.Url): "https://telegram.org/faq#secret-chats"], range: linkRange) + } + + let (infoLayout, infoApply) = makeInfoLayout(TextNodeLayoutArguments(attributedString: infoText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: layout.size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let _ = keyTextApply() + let _ = infoApply() + + let imageSpacing: CGFloat = 12.0 + let textSpacing: CGFloat = 10.0 + let contentHeight = imageSize.height + imageSpacing + keyTextLayout.size.height + textSpacing + infoLayout.size.height + + let contentOrigin = sideInset + max(0, floor((layout.size.height - insets.top - sideInset * 2.0 - contentHeight) / 2.0)) + + self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: contentHeight + sideInset * 2.0) + + let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize) + transition.updateFrame(node: self.imageNode, frame: imageFrame) + + let keyTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - keyTextLayout.size.width) / 2.0), y: imageFrame.maxY + imageSpacing), size: keyTextLayout.size) + transition.updateFrame(node: self.keyTextNode, frame: keyTextFrame) + + let infoFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - infoLayout.size.width) / 2.0), y: keyTextFrame.maxY + textSpacing), size: infoLayout.size) + transition.updateFrame(node: self.infoNode, frame: infoFrame) + } + + @objc func infoTap(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let point = recognizer.location(in: recognizer.view) + if let attributes = self.infoNode.attributesAtPoint(point)?.1 { + if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Url)] as? String { + openExternalUrl(account: self.account, url: url, presentationData: self.presentationData, applicationContext: self.account.telegramApplicationContext, navigationController: self.getNavigationController()) + } + } + } + } +} diff --git a/TelegramUI/SecretChatKeyVisualization.swift b/TelegramUI/SecretChatKeyVisualization.swift new file mode 100644 index 0000000000..3c59f84d8d --- /dev/null +++ b/TelegramUI/SecretChatKeyVisualization.swift @@ -0,0 +1,9 @@ +import Foundation +import TelegramCore +import TelegramUIPrivateModule + +func secretChatKeyImage(_ fingerprint: SecretChatKeyFingerprint, size: CGSize) -> UIImage? { + let keySignatureData = fingerprint.sha1.data() + let additionalSignature = fingerprint.sha256.data() + return SecretChatKeyVisualization(keySignatureData, additionalSignature, size) +} diff --git a/TelegramUI/SharedMediaPlayer.swift b/TelegramUI/SharedMediaPlayer.swift index f5910165b0..a82b12c89a 100644 --- a/TelegramUI/SharedMediaPlayer.swift +++ b/TelegramUI/SharedMediaPlayer.swift @@ -265,7 +265,7 @@ private enum SharedMediaPlaybackItem: Equatable { if let status = status { return status } else { - return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) + return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: .paused) } } } diff --git a/TelegramUI/StickerPackPreviewController.swift b/TelegramUI/StickerPackPreviewController.swift index 63cbc7be7b..c355430169 100644 --- a/TelegramUI/StickerPackPreviewController.swift +++ b/TelegramUI/StickerPackPreviewController.swift @@ -21,7 +21,20 @@ final class StickerPackPreviewController: ViewController { private let stickerPackInstalledDisposable = MetaDisposable() private let stickerPackInstalled = Promise() - var sendSticker: ((TelegramMediaFile) -> Void)? + var sendSticker: ((TelegramMediaFile) -> Void)? { + didSet { + if self.isNodeLoaded { + if let sendSticker = self.sendSticker { + self.controllerNode.sendSticker = { [weak self] file in + sendSticker(file) + self?.dismiss() + } + } else { + self.controllerNode.sendSticker = nil + } + } + } + } init(account: Account, stickerPack: StickerPackReference) { self.account = account @@ -51,15 +64,13 @@ final class StickerPackPreviewController: ViewController { self.controllerNode.cancel = { [weak self] in self?.dismiss() } - self.controllerNode.presentPreview = { [weak self] controller, arguments in - self?.present(controller, in: .window(.root), with: arguments) + self.controllerNode.presentInGlobalOverlay = { [weak self] controller, arguments in + self?.presentInGlobalOverlay(controller, with: arguments) } - self.controllerNode.sendSticker = { [weak self] file in - if let sendSticker = self?.sendSticker { + if let sendSticker = self.sendSticker { + self.controllerNode.sendSticker = { [weak self] file in sendSticker(file) - return true - } else { - return false + self?.dismiss() } } self.displayNodeDidLoad() diff --git a/TelegramUI/StickerPackPreviewControllerNode.swift b/TelegramUI/StickerPackPreviewControllerNode.swift index d0057cc190..1f9e123916 100644 --- a/TelegramUI/StickerPackPreviewControllerNode.swift +++ b/TelegramUI/StickerPackPreviewControllerNode.swift @@ -28,10 +28,10 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol private var interaction: StickerPackPreviewInteraction! - var presentPreview: ((ViewController, Any?) -> Void)? + var presentInGlobalOverlay: ((ViewController, Any?) -> Void)? var dismiss: (() -> Void)? var cancel: (() -> Void)? - var sendSticker: ((TelegramMediaFile) -> Bool)? + var sendSticker: ((TelegramMediaFile) -> Void)? let ready = Promise() private var didSetReady = false @@ -41,8 +41,6 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol private var didSetItems = false - private var previewController: StickerPreviewController? - private var hapticFeedback: HapticFeedback? init(account: Account) { @@ -112,9 +110,9 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol self.interaction = StickerPackPreviewInteraction(sendSticker: { [weak self] item in if let strongSelf = self, let sendSticker = strongSelf.sendSticker { - if sendSticker(item.file) { + /*if sendSticker(item.file) { strongSelf.cancel?() - } + }*/ } }) @@ -146,15 +144,6 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) } - - let longTapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.previewGesture(_:))) - longTapRecognizer.tapActionAtPoint = { [weak self] location in - if let strongSelf = self, let _ = strongSelf.contentGridNode.itemNodeAtPoint(location) as? StickerPackPreviewGridItemNode { - return .waitForHold(timeout: 0.2, acceptTap: true) - } - return .fail - } - self.contentGridNode.view.addGestureRecognizer(longTapRecognizer) } override func didLoad() { @@ -163,6 +152,71 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol if #available(iOSApplicationExtension 11.0, *) { self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never } + + /* + let longTapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.previewGesture(_:))) + longTapRecognizer.tapActionAtPoint = { [weak self] location in + if let strongSelf = self, let _ = strongSelf.contentGridNode.itemNodeAtPoint(location) as? StickerPackPreviewGridItemNode { + return .waitForHold(timeout: 0.2, acceptTap: true) + } + return .fail + } + self.contentGridNode.view.addGestureRecognizer(longTapRecognizer) + */ + + self.contentGridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? in + if let strongSelf = self { + if let itemNode = strongSelf.contentGridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem { + return strongSelf.account.postbox.modify { modifier -> Bool in + return getIsStickerSaved(modifier: modifier, fileId: item.file.fileId) + } + |> deliverOnMainQueue + |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in + if let strongSelf = self { + var menuItems: [PeekControllerMenuItem] = [] + if strongSelf.sendSticker != nil { + menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { + if let strongSelf = self { + strongSelf.sendSticker?(item.file) + } + })) + } + menuItems.append(PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { + if let strongSelf = self { + if isStarred { + let _ = removeSavedSticker(postbox: strongSelf.account.postbox, mediaId: item.file.fileId).start() + } else { + let _ = addSavedSticker(postbox: strongSelf.account.postbox, network: strongSelf.account.network, file: item.file).start() + } + } + })) + menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: {})) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.account, item: item, menu: menuItems)) + } else { + return nil + } + } + } + } + return nil + }, present: { [weak self] content, sourceNode in + if let strongSelf = self { + let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: { + return sourceNode + }) + strongSelf.presentInGlobalOverlay?(controller, nil) + return controller + } + return nil + }, updateContent: { [weak self] content in + if let strongSelf = self { + var item: StickerPackItem? + if let content = content as? StickerPreviewPeekContent { + item = content.item + } + strongSelf.updatePreviewingItem(item: item, animated: true) + } + }, activateBySingleTap: true)) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -467,25 +521,6 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } } - @objc func previewGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - switch recognizer.state { - case .began: - if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture { - if let itemNode = self.contentGridNode.itemNodeAtPoint(location) as? StickerPackPreviewGridItemNode { - self.updatePreviewingItem(item: itemNode.stickerPackItem, animated: true) - } - } - case .ended, .cancelled: - self.updatePreviewingItem(item: nil, animated: true) - case .changed: - if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture, let itemNode = self.contentGridNode.itemNodeAtPoint(location) as? StickerPackPreviewGridItemNode { - self.updatePreviewingItem(item: itemNode.stickerPackItem, animated: true) - } - default: - break - } - } - private func updatePreviewingItem(item: StickerPackItem?, animated: Bool) { if self.interaction.previewedItem != item { self.interaction.previewedItem = item @@ -495,35 +530,6 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol itemNode.updatePreviewing(animated: animated) } } - - if let item = item { - if let previewController = self.previewController { - self.hapticFeedback?.tap() - self.hapticFeedback?.prepareTap() - previewController.updateItem(item) - } else { - self.hapticFeedback = HapticFeedback() - self.hapticFeedback?.prepareTap() - let previewController = StickerPreviewController(account: self.account, item: item) - self.previewController = previewController - self.presentPreview?(previewController, StickerPreviewControllerPresentationArguments(transitionNode: { [weak self] item in - if let strongSelf = self { - var result: ASDisplayNode? - strongSelf.contentGridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? StickerPackPreviewGridItemNode, itemNode.stickerPackItem == item { - result = itemNode.transitionNode() - } - } - return result - } - return nil - })) - } - } else if let previewController = self.previewController { - self.hapticFeedback = nil - previewController.dismiss() - self.previewController = nil - } } } } diff --git a/TelegramUI/StickerPreviewPeekContent.swift b/TelegramUI/StickerPreviewPeekContent.swift index 14629e6c66..7846414e94 100644 --- a/TelegramUI/StickerPreviewPeekContent.swift +++ b/TelegramUI/StickerPreviewPeekContent.swift @@ -31,6 +31,14 @@ final class StickerPreviewPeekContent: PeekControllerContent { func node() -> PeekControllerContentNode & ASDisplayNode { return StickerPreviewPeekContentNode(account: self.account, item: self.item) } + + func isEqual(to: PeekControllerContent) -> Bool { + if let to = to as? StickerPreviewPeekContent { + return self.item == to.item + } else { + return false + } + } } private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerContentNode { diff --git a/TelegramUI/StickerResources.swift b/TelegramUI/StickerResources.swift index 1b281ba7a8..77dec1ad94 100644 --- a/TelegramUI/StickerResources.swift +++ b/TelegramUI/StickerResources.swift @@ -145,6 +145,11 @@ func chatMessageSticker(account: Account, file: TelegramMediaFile, small: Bool, return { arguments in let context = DrawingContext(size: arguments.drawingSize, clear: true) + /*let drawingRect = arguments.drawingRect + let fittedSize = arguments.imageSize + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize)*/ + let fittedRect = arguments.drawingRect + var fullSizeImage: (UIImage, UIImage)? if let fullSizeData = fullSizeData, fullSizeComplete { if let image = imageFromAJpeg(data: fullSizeData) { @@ -172,7 +177,7 @@ func chatMessageSticker(account: Account, file: TelegramMediaFile, small: Bool, c.setBlendMode(.copy) if let blurredThumbnailImage = blurredThumbnailImage { c.interpolationQuality = .low - c.draw(blurredThumbnailImage.cgImage!, in: arguments.drawingRect) + c.draw(blurredThumbnailImage.cgImage!, in: fittedRect) } if let fullSizeImage = fullSizeImage, let cgImage = fullSizeImage.0.cgImage, let cgImageAlpha = fullSizeImage.1.cgImage { @@ -181,7 +186,7 @@ func chatMessageSticker(account: Account, file: TelegramMediaFile, small: Bool, let mask = CGImage(maskWidth: cgImageAlpha.width, height: cgImageAlpha.height, bitsPerComponent: cgImageAlpha.bitsPerComponent, bitsPerPixel: cgImageAlpha.bitsPerPixel, bytesPerRow: cgImageAlpha.bytesPerRow, provider: cgImageAlpha.dataProvider!, decode: nil, shouldInterpolate: true) - c.draw(cgImage.masking(mask!)!, in: arguments.drawingRect) + c.draw(cgImage.masking(mask!)!, in: fittedRect) } } diff --git a/TelegramUI/StringWithAppliedEntities.swift b/TelegramUI/StringWithAppliedEntities.swift index 3451ff93eb..781e2308e4 100644 --- a/TelegramUI/StringWithAppliedEntities.swift +++ b/TelegramUI/StringWithAppliedEntities.swift @@ -65,25 +65,25 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba if nsString == nil { nsString = text as NSString } - string.addAttribute(NSAttributedStringKey(rawValue: TextNode.UrlAttribute), value: nsString!.substring(with: range), range: range) + string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Url), value: nsString!.substring(with: range), range: range) case .Email: string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) if nsString == nil { nsString = text as NSString } - string.addAttribute(NSAttributedStringKey(rawValue: TextNode.UrlAttribute), value: "mailto:\(nsString!.substring(with: range))", range: range) + string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Url), value: "mailto:\(nsString!.substring(with: range))", range: range) case .PhoneNumber: string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) if nsString == nil { nsString = text as NSString } - string.addAttribute(NSAttributedStringKey(rawValue: TextNode.UrlAttribute), value: "tel:\(nsString!.substring(with: range))", range: range) + string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Url), value: "tel:\(nsString!.substring(with: range))", range: range) case let .TextUrl(url): string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) if nsString == nil { nsString = text as NSString } - string.addAttribute(NSAttributedStringKey(rawValue: TextNode.UrlAttribute), value: url, range: range) + string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Url), value: url, range: range) case .Bold: string.addAttribute(NSAttributedStringKey.font, value: boldFont, range: range) case .Italic: @@ -99,7 +99,7 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba if nsString == nil { nsString = text as NSString } - string.addAttribute(NSAttributedStringKey(rawValue: TextNode.TelegramPeerTextMentionAttribute), value: nsString!.substring(with: range), range: range) + string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention), value: nsString!.substring(with: range), range: range) case let .TextMention(peerId): string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) if linkColor.isEqual(baseColor) { @@ -109,7 +109,7 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba string.addAttribute(NSAttributedStringKey.font, value: linkFont, range: range) } let mention = nsString!.substring(with: range) - string.addAttribute(NSAttributedStringKey(rawValue: TextNode.TelegramPeerMentionAttribute), value: TelegramPeerMention(peerId: peerId, mention: mention), range: range) + string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: peerId, mention: mention), range: range) case .Hashtag: if nsString == nil { nsString = text as NSString @@ -127,7 +127,7 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba if linkColor.isEqual(baseColor) { string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: combinedRange) } - string.addAttribute(NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute), value: TelegramHashtag(peerName: peerName, hashtag: hashtag), range: combinedRange) + string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: peerName, hashtag: hashtag), range: combinedRange) } } } @@ -136,7 +136,7 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba if linkColor.isEqual(baseColor) { string.addAttribute(NSAttributedStringKey.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue as NSNumber, range: range) } - string.addAttribute(NSAttributedStringKey(rawValue: TextNode.TelegramHashtagAttribute), value: TelegramHashtag(peerName: nil, hashtag: hashtag), range: range) + string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: nil, hashtag: hashtag), range: range) } case .BotCommand: string.addAttribute(NSAttributedStringKey.foregroundColor, value: linkColor, range: range) @@ -146,7 +146,7 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba if nsString == nil { nsString = text as NSString } - string.addAttribute(NSAttributedStringKey(rawValue: TextNode.TelegramBotCommandAttribute), value: nsString!.substring(with: range), range: range) + string.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.BotCommand), value: nsString!.substring(with: range), range: range) case .Code, .Pre: string.addAttribute(NSAttributedStringKey.font, value: fixedFont, range: range) default: diff --git a/TelegramUI/SystemVideoContent.swift b/TelegramUI/SystemVideoContent.swift index 245dbc17fc..953ab0118f 100644 --- a/TelegramUI/SystemVideoContent.swift +++ b/TelegramUI/SystemVideoContent.swift @@ -39,7 +39,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent private let playbackCompletedListeners = Bag<() -> Void>() private var initializedStatus = false - private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) + private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: .paused) private var isBuffering = false private let _status = ValuePromise() var status: Signal { @@ -145,7 +145,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent } else { status = isPlaying ? .playing : .paused } - self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: 0, status: status) + self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: status) self._status.set(self.statusValue) } else if keyPath == "playbackBufferEmpty" { let isPlaying = !self.player.rate.isZero @@ -156,7 +156,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent } else { status = isPlaying ? .playing : .paused } - self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: 0, status: status) + self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: status) self._status.set(self.statusValue) } else if keyPath == "playbackLikelyToKeepUp" || keyPath == "playbackBufferFull" { let isPlaying = !self.player.rate.isZero @@ -167,7 +167,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent } else { status = isPlaying ? .playing : .paused } - self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: 0, status: status) + self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: status) self._status.set(self.statusValue) } } @@ -186,7 +186,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent func play() { assert(Queue.mainQueue().isCurrent()) if !self.initializedStatus { - self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true))) + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: .buffering(initial: true, whilePlaying: true))) } if !self.hasAudioSession { self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play, activate: { [weak self] _ in @@ -205,7 +205,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent func pause() { assert(Queue.mainQueue().isCurrent()) if !self.initializedStatus { - self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: 0, status: .paused)) + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: .paused)) } self.player.pause() } diff --git a/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift b/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift index 7c2292e9c1..a696733aaa 100644 --- a/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift +++ b/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift @@ -1,5 +1,6 @@ import Foundation import UIKit.UIGestureRecognizerSubclass +import Display private class TapLongTapOrDoubleTapGestureRecognizerTimerTarget: NSObject { weak var target: TapLongTapOrDoubleTapGestureRecognizer? diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index 76e449424e..7398494f00 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -4,9 +4,18 @@ import UIKit import Postbox import TelegramCore +public final class TelegramApplicationOpenUrlCompletion { + public let completion: (Bool) -> Void + + public init(completion: @escaping (Bool) -> Void) { + self.completion = completion + } +} + public final class TelegramApplicationBindings { public let isMainApp: Bool public let openUrl: (String) -> Void + public let openUniversalUrl: (String, TelegramApplicationOpenUrlCompletion) -> Void public let canOpenUrl: (String) -> Bool public let getTopWindow: () -> UIWindow? public let displayNotification: (String) -> Void @@ -15,9 +24,10 @@ public final class TelegramApplicationBindings { public let clearMessageNotifications: ([MessageId]) -> Void public let pushIdleTimerExtension: () -> Disposable - public init(isMainApp: Bool, openUrl: @escaping (String) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable) { + public init(isMainApp: Bool, openUrl: @escaping (String) -> Void, openUniversalUrl: @escaping (String, TelegramApplicationOpenUrlCompletion) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal, applicationIsActive: Signal, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable) { self.isMainApp = isMainApp self.openUrl = openUrl + self.openUniversalUrl = openUniversalUrl self.canOpenUrl = canOpenUrl self.getTopWindow = getTopWindow self.displayNotification = displayNotification @@ -61,6 +71,11 @@ public final class TelegramApplicationContext { public var navigateToCurrentCall: (() -> Void)? public var hasOngoingCall: Signal? + private var immediateHasOngoingCallValue = Atomic(value: false) + public var immediateHasOngoingCall: Bool { + return self.immediateHasOngoingCallValue.with { $0 } + } + private var hasOngoingCallDisposable: Disposable? public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, currentPresentationData: PresentationData, presentationData: Signal, currentMediaDownloadSettings: AutomaticMediaDownloadSettings, automaticMediaDownloadSettings: Signal, currentInAppNotificationSettings: InAppNotificationSettings, postbox: Postbox, network: Network, accountPeerId: PeerId?, viewTracker: AccountViewTracker?, stateManager: AccountStateManager?) { self.mediaManager = MediaManager(postbox: postbox, inForeground: applicationBindings.applicationInForeground) @@ -123,6 +138,11 @@ public final class TelegramApplicationContext { let _ = strongSelf.currentAutomaticMediaDownloadSettings.swap(next) } })) + + let immediateHasOngoingCallValue = self.immediateHasOngoingCallValue + self.hasOngoingCallDisposable = self.hasOngoingCall?.start(next: { value in + let _ = immediateHasOngoingCallValue.swap(value) + }) } deinit { diff --git a/TelegramUI/TelegramController.swift b/TelegramUI/TelegramController.swift index f5fe8113f7..45aa4608c0 100644 --- a/TelegramUI/TelegramController.swift +++ b/TelegramUI/TelegramController.swift @@ -298,7 +298,7 @@ public class TelegramController: ViewController { mediaAccessoryPanel.updateLayout(size: panelFrame.size, transition: transition) mediaAccessoryPanel.containerNode.headerNode.playbackItem = item mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.account.telegramApplicationContext.mediaManager.globalMediaPlayerState |> map { state in - return state?.0.status ?? MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) + return state?.0.status ?? MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: .paused) } } else { if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel { @@ -349,7 +349,7 @@ public class TelegramController: ViewController { mediaAccessoryPanel.updateLayout(size: panelFrame.size, transition: .immediate) mediaAccessoryPanel.containerNode.headerNode.playbackItem = item mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.account.telegramApplicationContext.mediaManager.globalMediaPlayerState |> map { state in - return state?.0.status ?? MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) + return state?.0.status ?? MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: .paused) } mediaAccessoryPanel.animateIn(transition: transition) } diff --git a/TelegramUI/TextNode.swift b/TelegramUI/TextNode.swift index 5dbf554d3f..5d66f04a5f 100644 --- a/TelegramUI/TextNode.swift +++ b/TelegramUI/TextNode.swift @@ -3,182 +3,6 @@ import AsyncDisplayKit import Display import Postbox -private let defaultFont = UIFont.systemFont(ofSize: 15.0) - -private final class TextNodeLine { - let line: CTLine - let frame: CGRect - let range: NSRange - - init(line: CTLine, frame: CGRect, range: NSRange) { - self.line = line - self.frame = frame - self.range = range - } -} - -enum TextNodeCutoutPosition { - case TopLeft - case TopRight -} - -struct TextNodeCutout: Equatable { - let position: TextNodeCutoutPosition - let size: CGSize - - static func ==(lhs: TextNodeCutout, rhs: TextNodeCutout) -> Bool { - return lhs.position == rhs.position && lhs.size == rhs.size - } -} - -final class TextNodeLayoutArguments { - let attributedString: NSAttributedString? - let backgroundColor: UIColor? - let maximumNumberOfLines: Int - let truncationType: CTLineTruncationType - let constrainedSize: CGSize - let alignment: NSTextAlignment - let lineSpacing: CGFloat - let cutout: TextNodeCutout? - let insets: UIEdgeInsets - - init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets()) { - self.attributedString = attributedString - self.backgroundColor = backgroundColor - self.maximumNumberOfLines = maximumNumberOfLines - self.truncationType = truncationType - self.constrainedSize = constrainedSize - self.alignment = alignment - self.lineSpacing = lineSpacing - self.cutout = cutout - self.insets = insets - } -} - -final class TextNodeLayout: NSObject { - fileprivate let attributedString: NSAttributedString? - fileprivate let maximumNumberOfLines: Int - fileprivate let truncationType: CTLineTruncationType - fileprivate let backgroundColor: UIColor? - fileprivate let constrainedSize: CGSize - fileprivate let alignment: NSTextAlignment - fileprivate let lineSpacing: CGFloat - fileprivate let cutout: TextNodeCutout? - fileprivate let insets: UIEdgeInsets - let size: CGSize - fileprivate let firstLineOffset: CGFloat - fileprivate let lines: [TextNodeLine] - - fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, firstLineOffset: CGFloat, lines: [TextNodeLine], backgroundColor: UIColor?) { - self.attributedString = attributedString - self.maximumNumberOfLines = maximumNumberOfLines - self.truncationType = truncationType - self.constrainedSize = constrainedSize - self.alignment = alignment - self.lineSpacing = lineSpacing - self.cutout = cutout - self.insets = insets - self.size = size - self.firstLineOffset = firstLineOffset - self.lines = lines - self.backgroundColor = backgroundColor - } - - var numberOfLines: Int { - return self.lines.count - } - - var trailingLineWidth: CGFloat { - if let lastLine = self.lines.last { - return lastLine.frame.width - } else { - return 0.0 - } - } - - func attributesAtPoint(_ point: CGPoint) -> (Int, [NSAttributedStringKey: Any])? { - if let attributedString = self.attributedString { - let transformedPoint = CGPoint(x: point.x - self.insets.left, y: point.y - self.insets.top) - for line in self.lines { - let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) - if lineFrame.contains(transformedPoint) { - var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) - if index == attributedString.length { - index -= 1 - } else if index != 0 { - var glyphStart: CGFloat = 0.0 - CTLineGetOffsetForStringIndex(line.line, index, &glyphStart) - if transformedPoint.x < glyphStart { - index -= 1 - } - } - if index >= 0 && index < attributedString.length { - return (index, attributedString.attributes(at: index, effectiveRange: nil)) - } - break - } - } - for line in self.lines { - let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) - if lineFrame.offsetBy(dx: 0.0, dy: -lineFrame.size.height).insetBy(dx: -3.0, dy: -3.0).contains(transformedPoint) { - var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) - if index == attributedString.length { - index -= 1 - } else if index != 0 { - var glyphStart: CGFloat = 0.0 - CTLineGetOffsetForStringIndex(line.line, index, &glyphStart) - if transformedPoint.x < glyphStart { - index -= 1 - } - } - if index >= 0 && index < attributedString.length { - return (index, attributedString.attributes(at: index, effectiveRange: nil)) - } - break - } - } - } - return nil - } - - func linesRects() -> [CGRect] { - var rects: [CGRect] = [] - for line in self.lines { - rects.append(line.frame) - } - return rects - } - - func lineAndAttributeRects(name: String, at index: Int) -> [(CGRect, CGRect)]? { - if let attributedString = self.attributedString { - var range = NSRange() - let _ = attributedString.attribute(NSAttributedStringKey(rawValue: name), at: index, effectiveRange: &range) - if range.length != 0 { - var rects: [(CGRect, CGRect)] = [] - for line in self.lines { - let lineRange = NSIntersectionRange(range, line.range) - if lineRange.length != 0 { - var leftOffset: CGFloat = 0.0 - if lineRange.location != line.range.location { - leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) - } - var rightOffset: CGFloat = line.frame.width - if lineRange.location + lineRange.length != line.range.length { - rightOffset = ceil(CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, nil)) - } - let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) - rects.append((lineFrame, CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height)))) - } - } - if !rects.isEmpty { - return rects - } - } - } - return nil - } -} - final class TelegramHashtag { let peerName: String? let hashtag: String @@ -199,306 +23,10 @@ final class TelegramPeerMention { } } -final class TextNode: ASDisplayNode { - static let UrlAttribute = "UrlAttributeT" - static let TelegramPeerMentionAttribute = "TelegramPeerMention" - static let TelegramPeerTextMentionAttribute = "TelegramPeerTextMention" - static let TelegramBotCommandAttribute = "TelegramBotCommand" - static let TelegramHashtagAttribute = "TelegramHashtag" - - private(set) var cachedLayout: TextNodeLayout? - - override init() { - super.init() - - self.backgroundColor = UIColor.clear - self.isOpaque = false - self.clipsToBounds = false - } - - func attributesAtPoint(_ point: CGPoint) -> (Int, [NSAttributedStringKey: Any])? { - if let cachedLayout = self.cachedLayout { - return cachedLayout.attributesAtPoint(point) - } else { - return nil - } - } - - func attributeRects(name: String, at index: Int) -> [CGRect]? { - if let cachedLayout = self.cachedLayout { - return cachedLayout.lineAndAttributeRects(name: name, at: index)?.map { $0.1 } - } else { - return nil - } - } - - func lineAndAttributeRects(name: String, at index: Int) -> [(CGRect, CGRect)]? { - if let cachedLayout = self.cachedLayout { - return cachedLayout.lineAndAttributeRects(name: name, at: index) - } else { - return nil - } - } - - private class func calculateLayout(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets) -> TextNodeLayout { - if let attributedString = attributedString { - let stringLength = attributedString.length - - let font: CTFont - if stringLength != 0 { - if let stringFont = attributedString.attribute(NSAttributedStringKey.font, at: 0, effectiveRange: nil) { - font = stringFont as! CTFont - } else { - font = defaultFont - } - } else { - font = defaultFont - } - - let fontAscent = CTFontGetAscent(font) - let fontDescent = CTFontGetDescent(font) - let fontLineHeight = floor(fontAscent + fontDescent) - let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor) - - var lines: [TextNodeLine] = [] - - var maybeTypesetter: CTTypesetter? - maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) - if maybeTypesetter == nil { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor) - } - - let typesetter = maybeTypesetter! - - var lastLineCharacterIndex: CFIndex = 0 - var layoutSize = CGSize() - - var cutoutEnabled = false - var cutoutMinY: CGFloat = 0.0 - var cutoutMaxY: CGFloat = 0.0 - var cutoutWidth: CGFloat = 0.0 - var cutoutOffset: CGFloat = 0.0 - if let cutout = cutout { - cutoutMinY = -fontLineSpacing - cutoutMaxY = cutout.size.height + fontLineSpacing - cutoutWidth = cutout.size.width - if case .TopLeft = cutout.position { - cutoutOffset = cutoutWidth - } - cutoutEnabled = true - } - - let firstLineOffset = floorToScreenPixels(fontLineSpacing * 2.0) - - var first = true - while true { - var lineConstrainedWidth = constrainedSize.width - //var lineOriginY = floorToScreenPixels(layoutSize.height + fontLineHeight - fontLineSpacing * 2.0) - var lineOriginY = floorToScreenPixels(layoutSize.height + fontAscent) - if !first { - lineOriginY += fontLineSpacing - } - var lineCutoutOffset: CGFloat = 0.0 - var lineAdditionalWidth: CGFloat = 0.0 - - if cutoutEnabled { - if lineOriginY - fontLineHeight < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY { - lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth) - lineCutoutOffset = cutoutOffset - lineAdditionalWidth = cutoutWidth - } - } - - let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth)) - - var isLastLine = false - if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 { - isLastLine = true - } else if layoutSize.height + (fontLineSpacing + fontLineHeight) * 2.0 > constrainedSize.height { - isLastLine = true - } - - if isLastLine { - if first { - first = false - } else { - layoutSize.height += fontLineSpacing - } - - let lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) - - if lineRange.length == 0 { - break - } - - let coreTextLine: CTLine - - let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0) - - if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(constrainedSize.width) { - coreTextLine = originalLine - } else { - var truncationTokenAttributes: [NSAttributedStringKey : AnyObject] = [:] - truncationTokenAttributes[NSAttributedStringKey.font] = font - truncationTokenAttributes[NSAttributedStringKey(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber - let tokenString = "\u{2026}" - let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes) - let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) - - coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(constrainedSize.width), truncationType, truncationToken) ?? truncationToken - } - - let lineWidth = min(constrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))) - let lineFrame = CGRect(x: lineCutoutOffset, y: lineOriginY, width: lineWidth, height: fontLineHeight) - layoutSize.height += fontLineHeight + fontLineSpacing - layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) - - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length))) - - break - } else { - if lineCharacterCount > 0 { - if first { - first = false - } else { - layoutSize.height += fontLineSpacing - } - - let lineRange = CFRangeMake(lastLineCharacterIndex, lineCharacterCount) - let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 100.0) - lastLineCharacterIndex += lineCharacterCount - - let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) - let lineFrame = CGRect(x: lineCutoutOffset, y: lineOriginY, width: lineWidth, height: fontLineHeight) - layoutSize.height += fontLineHeight - layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) - - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length))) - } else { - if !lines.isEmpty { - layoutSize.height += fontLineSpacing - } - break - } - } - } - - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), firstLineOffset: firstLineOffset, lines: lines, backgroundColor: backgroundColor) - } else { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor) - } - } - - override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return self.cachedLayout - } - - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { - if isCancelled() { - return - } - - let context = UIGraphicsGetCurrentContext()! - - context.setAllowsAntialiasing(true) - - context.setAllowsFontSmoothing(false) - context.setShouldSmoothFonts(false) - - context.setAllowsFontSubpixelPositioning(false) - context.setShouldSubpixelPositionFonts(false) - - context.setAllowsFontSubpixelQuantization(true) - context.setShouldSubpixelQuantizeFonts(true) - - if let layout = parameters as? TextNodeLayout { - if !isRasterizing || layout.backgroundColor != nil { - context.setBlendMode(.copy) - context.setFillColor((layout.backgroundColor ?? UIColor.clear).cgColor) - context.fill(bounds) - } - - let textMatrix = context.textMatrix - let textPosition = context.textPosition - //CGContextSaveGState(context) - - context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0) - - //let clipRect = CGContextGetClipBoundingBox(context) - - let alignment = layout.alignment - let offset = CGPoint(x: layout.insets.left, y: layout.insets.top) - - for i in 0 ..< layout.lines.count { - let line = layout.lines[i] - let lineOffset: CGFloat - if alignment == .center { - lineOffset = floor((bounds.size.width - line.frame.size.width) / 2.0) - } else { - lineOffset = 0.0 - } - context.textPosition = CGPoint(x: line.frame.origin.x + lineOffset + offset.x, y: line.frame.origin.y + offset.y) - CTLineDraw(line.line, context) - } - - //CGContextRestoreGState(context) - context.textMatrix = textMatrix - context.textPosition = CGPoint(x: textPosition.x, y: textPosition.y) - } - - context.setBlendMode(.normal) - } - - class func asyncLayout(_ maybeNode: TextNode?) -> (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) { - let existingLayout: TextNodeLayout? = maybeNode?.cachedLayout - - return { arguments in - let layout: TextNodeLayout - - var updated = false - if let existingLayout = existingLayout, existingLayout.constrainedSize == arguments.constrainedSize && existingLayout.maximumNumberOfLines == arguments.maximumNumberOfLines && existingLayout.truncationType == arguments.truncationType && existingLayout.cutout == arguments.cutout && existingLayout.alignment == arguments.alignment && existingLayout.lineSpacing.isEqual(to: arguments.lineSpacing) { - let stringMatch: Bool - - var colorMatch: Bool = true - if let backgroundColor = arguments.backgroundColor, let previousBackgroundColor = existingLayout.backgroundColor { - if !backgroundColor.isEqual(previousBackgroundColor) { - colorMatch = false - } - } else if (arguments.backgroundColor != nil) != (existingLayout.backgroundColor != nil) { - colorMatch = false - } - - if !colorMatch { - stringMatch = false - } else if let existingString = existingLayout.attributedString, let string = arguments.attributedString { - stringMatch = existingString.isEqual(to: string) - } else if existingLayout.attributedString == nil && arguments.attributedString == nil { - stringMatch = true - } else { - stringMatch = false - } - - if stringMatch { - layout = existingLayout - } else { - layout = TextNode.calculateLayout(attributedString: arguments.attributedString, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets) - updated = true - } - } else { - layout = TextNode.calculateLayout(attributedString: arguments.attributedString, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets) - updated = true - } - - let node = maybeNode ?? TextNode() - - return (layout, { - node.cachedLayout = layout - if updated { - node.setNeedsDisplay() - } - - return node - }) - } - } +struct TelegramTextAttributes { + static let Url = "UrlAttributeT" + static let PeerMention = "TelegramPeerMention" + static let PeerTextMention = "TelegramPeerTextMention" + static let BotCommand = "TelegramBotCommand" + static let Hashtag = "TelegramHashtag" } diff --git a/TelegramUI/ThemeSettingsChatPreviewItem.swift b/TelegramUI/ThemeSettingsChatPreviewItem.swift index 7dc5e0abb9..1abda897b8 100644 --- a/TelegramUI/ThemeSettingsChatPreviewItem.swift +++ b/TelegramUI/ThemeSettingsChatPreviewItem.swift @@ -89,7 +89,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { self.controllerInteraction = ChatControllerInteraction(openMessage: { _ in return false }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in - }, canSetupReply: { + }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: .none) diff --git a/TelegramUI/UniversalVideoCalleryItem.swift b/TelegramUI/UniversalVideoCalleryItem.swift index 41c2e4c295..db9b0f1e3e 100644 --- a/TelegramUI/UniversalVideoCalleryItem.swift +++ b/TelegramUI/UniversalVideoCalleryItem.swift @@ -194,7 +194,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let value = value, !value.duration.isZero { return value } else { - return MediaPlayerStatus(generationTimestamp: 0.0, duration: max(Double(item.content.duration), 0.01), timestamp: 0.0, seekId: 0, status: .paused) + return MediaPlayerStatus(generationTimestamp: 0.0, duration: max(Double(item.content.duration), 0.01), dimensions: CGSize(), timestamp: 0.0, seekId: 0, status: .paused) } }) @@ -205,6 +205,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var initialBuffering = false var isPaused = true if let value = value { + if let zoomableContent = strongSelf.zoomableContent, !value.dimensions.width.isZero && !value.dimensions.height.isZero { + let videoSize = CGSize(width: value.dimensions.width * 2.0, height: value.dimensions.height * 2.0) + if !zoomableContent.0.equalTo(videoSize) { + strongSelf.zoomableContent = (videoSize, zoomableContent.1) + strongSelf.videoNode?.updateLayout(size: videoSize, transition: .immediate) + } + } switch value.status { case .playing: isPaused = false diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index 08e9a9f092..128c8908f6 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -24,8 +24,9 @@ private final class UserInfoControllerArguments { let call: () -> Void let openCallMenu: (String) -> Void let displayAboutContextMenu: (String) -> Void + let openEncryptionKey: (SecretChatKeyFingerprint) -> Void - init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, addContact: @escaping () -> Void, shareContact: @escaping () -> Void, startSecretChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayCopyContextMenu: @escaping (UserInfoEntryTag, String) -> Void, call: @escaping () -> Void, openCallMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void) { + init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, addContact: @escaping () -> Void, shareContact: @escaping () -> Void, startSecretChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayCopyContextMenu: @escaping (UserInfoEntryTag, String) -> Void, call: @escaping () -> Void, openCallMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, openEncryptionKey: @escaping (SecretChatKeyFingerprint) -> Void) { self.account = account self.avatarAndNameInfoContext = avatarAndNameInfoContext self.updateEditingName = updateEditingName @@ -45,6 +46,7 @@ private final class UserInfoControllerArguments { self.call = call self.openCallMenu = openCallMenu self.displayAboutContextMenu = displayAboutContextMenu + self.openEncryptionKey = openEncryptionKey } } @@ -313,7 +315,8 @@ private enum UserInfoEntry: ItemListNodeEntry { arguments.openGroupsInCommon() }) case let .secretEncryptionKey(theme, text, fingerprint): - return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .plain, action: { + return ItemListSecretChatKeyItem(theme: theme, title: text, fingerprint: fingerprint, sectionId: self.section, style: .plain, action: { + arguments.openEncryptionKey(fingerprint) }) case let .block(theme, text, action): return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { @@ -475,6 +478,19 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat return entries } +private func getUserPeer(postbox: Postbox, peerId: PeerId) -> Signal { + return postbox.modify { modifier -> Peer? in + guard let peer = modifier.getPeer(peerId) else { + return nil + } + if let peer = peer as? TelegramSecretChat { + return modifier.getPeer(peer.regularPeerId) + } else { + return peer + } + } +} + public func userInfoController(account: Account, peerId: PeerId) -> ViewController { let statePromise = ValuePromise(UserInfoState(), ignoreRepeated: true) let stateValue = Atomic(value: UserInfoState()) @@ -515,23 +531,30 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll let cachedAvatarEntries = Atomic?>(value: nil) let requestCallImpl: () -> Void = { - let callResult = account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: false) - if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { - if currentPeerId == peerId { - account.telegramApplicationContext.navigateToCurrentCall?() - } else { - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - let _ = (account.postbox.modify { modifier -> (Peer?, Peer?) in - return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId)) - } |> deliverOnMainQueue).start(next: { peer, current in - if let peer = peer, let current = current { - presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { - let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true) - })]), nil) - } - }) + let _ = (getUserPeer(postbox: account.postbox, peerId: peerId) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer else { + return } - } + + let callResult = account.telegramApplicationContext.callManager?.requestCall(peerId: peer.id, endCurrentIfAny: false) + if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { + if currentPeerId == peer.id { + account.telegramApplicationContext.navigateToCurrentCall?() + } else { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let _ = (account.postbox.modify { modifier -> (Peer?, Peer?) in + return (modifier.getPeer(peer.id), modifier.getPeer(currentPeerId)) + } |> deliverOnMainQueue).start(next: { peer, current in + if let peer = peer, let current = current { + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peer.id, endCurrentIfAny: true) + })]), nil) + } + }) + } + } + }) } let arguments = UserInfoControllerArguments(account: account, avatarAndNameInfoContext: avatarAndNameInfoContext, updateEditingName: { editingName in @@ -561,10 +584,9 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }, openChat: { openChatImpl?() }, addContact: { - let _ = (account.postbox.modify { modifier -> TelegramUser? in - return modifier.getPeer(peerId) as? TelegramUser - }).start(next: { user in - if let user = user, let phone = user.phone, !phone.isEmpty { + let _ = (getUserPeer(postbox: account.postbox, peerId: peerId) + |> deliverOnMainQueue).start(next: { peer in + if let user = peer as? TelegramUser, let phone = user.phone, !phone.isEmpty { let _ = addContactPeerInteractively(account: account, peerId: user.id, phone: phone).start() } }) @@ -672,9 +694,8 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }, call: { requestCallImpl() }, openCallMenu: { number in - let _ = (account.postbox.modify { modifier -> Peer? in - return modifier.getPeer(peerId) - } |> deliverOnMainQueue).start(next: { peer in + let _ = (getUserPeer(postbox: account.postbox, peerId: peerId) + |> deliverOnMainQueue).start(next: { peer in if let peer = peer as? TelegramUser, let peerPhoneNumber = peer.phone, formatPhoneNumber(number) == formatPhoneNumber(peerPhoneNumber) { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationTheme: presentationData.theme) @@ -701,6 +722,19 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }) }, displayAboutContextMenu: { text in displayAboutContextMenuImpl?(text) + }, openEncryptionKey: { fingerprint in + let _ = (account.postbox.modify { modifier -> Peer? in + if let peer = modifier.getPeer(peerId) as? TelegramSecretChat { + if let userPeer = modifier.getPeer(peer.regularPeerId) { + return userPeer + } + } + return nil + } |> deliverOnMainQueue).start(next: { peer in + if let peer = peer { + pushControllerImpl?(SecretChatKeyController(account: account, fingerprint: fingerprint, peer: peer)) + } + }) }) let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) @@ -805,9 +839,8 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll } } shareContactImpl = { [weak controller] in - let _ = (account.postbox.modify { modifier -> Peer? in - return modifier.getPeer(peerId) - } |> deliverOnMainQueue).start(next: { peer in + let _ = (getUserPeer(postbox: account.postbox, peerId: peerId) + |> deliverOnMainQueue).start(next: { peer in if let peer = peer as? TelegramUser, let phone = peer.phone { let selectionController = PeerSelectionController(account: account) selectionController.peerSelected = { [weak selectionController] peerId in @@ -910,8 +943,8 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll UIPasteboard.general.string = text })]) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in - if let resultItemNode = resultItemNode { - return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) + if let strongController = controller, let resultItemNode = resultItemNode { + return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds) } else { return nil } @@ -941,8 +974,8 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll UIPasteboard.general.string = value })]) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in - if let resultItemNode = resultItemNode { - return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) + if let strongController = controller, let resultItemNode = resultItemNode { + return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds) } else { return nil } diff --git a/TelegramUI/WebEmbedVideoContent.swift b/TelegramUI/WebEmbedVideoContent.swift index e1f0d57343..62f480c5fd 100644 --- a/TelegramUI/WebEmbedVideoContent.swift +++ b/TelegramUI/WebEmbedVideoContent.swift @@ -175,7 +175,7 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte self._ready.set(.single(Void())) } - self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true))) + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true))) let stateSignal = self.playerView.stateSignal()! self.statusDisposable = (Signal { subscriber in @@ -189,7 +189,7 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte } else { status = .paused } - subscriber.putNext(MediaPlayerStatus(generationTimestamp: 0.0, duration: next.duration, timestamp: max(0.0, next.position), seekId: 0, status: status)) + subscriber.putNext(MediaPlayerStatus(generationTimestamp: 0.0, duration: next.duration, dimensions: CGSize(), timestamp: max(0.0, next.position), seekId: 0, status: status)) } }) return ActionDisposable { @@ -203,7 +203,7 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte } } strongSelf.initializedStatus = true - strongSelf._status.set(MediaPlayerStatus(generationTimestamp: value.generationTimestamp, duration: value.duration, timestamp: value.timestamp, seekId: strongSelf.seekId, status: value.status)) + strongSelf._status.set(MediaPlayerStatus(generationTimestamp: value.generationTimestamp, duration: value.duration, dimensions: CGSize(), timestamp: value.timestamp, seekId: strongSelf.seekId, status: value.status)) } }) @@ -226,7 +226,7 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte func play() { assert(Queue.mainQueue().isCurrent()) if !self.initializedStatus { - self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true))) + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true))) } else { self.playerView.playVideo() } @@ -235,7 +235,7 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte func pause() { assert(Queue.mainQueue().isCurrent()) if !self.initializedStatus { - self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), timestamp: 0.0, seekId: self.seekId, status: .paused)) + self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, seekId: self.seekId, status: .paused)) } self.playerView.pauseVideo() }