diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index ce02c93690..dc0f764664 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -92,6 +92,12 @@ 09DD88FA21BFD70B000766BC /* ThemedTextAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09DD88F921BFD70B000766BC /* ThemedTextAlertController.swift */; }; 09F799FA21C3542D00820234 /* LegacyWebSearchGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F799F921C3542D00820234 /* LegacyWebSearchGallery.swift */; }; 09F799FC21C3FF3000820234 /* WebSearchGalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F799FB21C3FF3000820234 /* WebSearchGalleryController.swift */; }; + 09F79A0121C8116C00820234 /* WebSearchBadgeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F79A0021C8116C00820234 /* WebSearchBadgeNode.swift */; }; + 09F79A0321C8225600820234 /* WebSearchVideoGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F79A0221C8225600820234 /* WebSearchVideoGalleryItem.swift */; }; + 09F79A0721C829BC00820234 /* GalleryNavigationCheckNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F79A0621C829BC00820234 /* GalleryNavigationCheckNode.swift */; }; + 09F79A0921C829C700820234 /* GalleryNavigationRecipientNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F79A0821C829C700820234 /* GalleryNavigationRecipientNode.swift */; }; + 09F79A0B21C832F400820234 /* WebSearchGalleryFooterContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F79A0A21C832F400820234 /* WebSearchGalleryFooterContentNode.swift */; }; + 09F79A0D21C88E8900820234 /* LegacyWebSearchEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F79A0C21C88E8900820234 /* LegacyWebSearchEditor.swift */; }; 09FE756D2153F5F900A3120F /* CallRouteActionSheetItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */; }; 9F06830921A404AB001D8EDB /* NotificationExceptionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06830821A404AB001D8EDB /* NotificationExceptionControllerNode.swift */; }; 9F06830B21A404C4001D8EDB /* NotificationExcetionSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06830A21A404C4001D8EDB /* NotificationExcetionSettingsController.swift */; }; @@ -1186,6 +1192,12 @@ 09DD88F921BFD70B000766BC /* ThemedTextAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemedTextAlertController.swift; sourceTree = ""; }; 09F799F921C3542D00820234 /* LegacyWebSearchGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyWebSearchGallery.swift; sourceTree = ""; }; 09F799FB21C3FF3000820234 /* WebSearchGalleryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSearchGalleryController.swift; sourceTree = ""; }; + 09F79A0021C8116C00820234 /* WebSearchBadgeNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSearchBadgeNode.swift; sourceTree = ""; }; + 09F79A0221C8225600820234 /* WebSearchVideoGalleryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSearchVideoGalleryItem.swift; sourceTree = ""; }; + 09F79A0621C829BC00820234 /* GalleryNavigationCheckNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryNavigationCheckNode.swift; sourceTree = ""; }; + 09F79A0821C829C700820234 /* GalleryNavigationRecipientNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryNavigationRecipientNode.swift; sourceTree = ""; }; + 09F79A0A21C832F400820234 /* WebSearchGalleryFooterContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSearchGalleryFooterContentNode.swift; sourceTree = ""; }; + 09F79A0C21C88E8900820234 /* LegacyWebSearchEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyWebSearchEditor.swift; sourceTree = ""; }; 09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallRouteActionSheetItem.swift; sourceTree = ""; }; 9F06830821A404AB001D8EDB /* NotificationExceptionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationExceptionControllerNode.swift; sourceTree = ""; }; 9F06830A21A404C4001D8EDB /* NotificationExcetionSettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationExcetionSettingsController.swift; sourceTree = ""; }; @@ -2373,6 +2385,10 @@ 09DD88F221BF907C000766BC /* WebSearchRecentQueryItem.swift */, 09DD88F421BF9730000766BC /* WebSearchRecentQueries.swift */, 09F799FB21C3FF3000820234 /* WebSearchGalleryController.swift */, + 09F79A0021C8116C00820234 /* WebSearchBadgeNode.swift */, + 09F79A0621C829BC00820234 /* GalleryNavigationCheckNode.swift */, + 09F79A0821C829C700820234 /* GalleryNavigationRecipientNode.swift */, + 09F79A0A21C832F400820234 /* WebSearchGalleryFooterContentNode.swift */, ); name = "Web Search"; sourceTree = ""; @@ -3185,6 +3201,7 @@ D0380DAC204ED434000414AB /* LegacyLiveUploadInterface.swift */, D097C26B20DD1EA5007BB4B8 /* OverlayStatusController.swift */, 09F799F921C3542D00820234 /* LegacyWebSearchGallery.swift */, + 09F79A0C21C88E8900820234 /* LegacyWebSearchEditor.swift */, ); name = "Legacy Components"; sourceTree = ""; @@ -4516,6 +4533,7 @@ D0104F291F471DA6004E4881 /* InstantImageGalleryItem.swift */, D0104F2B1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift */, D0A8BBA01F61EE83000F03FD /* UniversalVideoGalleryItem.swift */, + 09F79A0221C8225600820234 /* WebSearchVideoGalleryItem.swift */, ); name = Items; sourceTree = ""; @@ -5034,6 +5052,7 @@ D0B2F76820528E3D00D3BFB9 /* UserInfoEditingPhoneActionItem.swift in Sources */, D0EC6CB71EB9F58800EBF1C3 /* RMIntroPageView.m in Sources */, D0EC6CB81EB9F58800EBF1C3 /* RMIntroViewController.m in Sources */, + 09F79A0321C8225600820234 /* WebSearchVideoGalleryItem.swift in Sources */, D0EC6CB91EB9F58800EBF1C3 /* RMLoginViewController.m in Sources */, D0E9BA631F055AD200F079A4 /* BotPaymentCardInputItemNode.swift in Sources */, D01848E821A03BDA00B6DEBD /* ChatSearchState.swift in Sources */, @@ -5221,6 +5240,7 @@ D0EC6D131EB9F58800EBF1C3 /* MediaTrackDecodableFrame.swift in Sources */, D0EC6D141EB9F58800EBF1C3 /* MediaTrackFrame.swift in Sources */, D0B69C3920EBB397003632C7 /* ChatMessageInteractiveInstantVideoNode.swift in Sources */, + 09F79A0D21C88E8900820234 /* LegacyWebSearchEditor.swift in Sources */, D0EC6D151EB9F58800EBF1C3 /* MediaTrackFrameBuffer.swift in Sources */, D0EC6D161EB9F58800EBF1C3 /* MediaTrackFrameDecoder.swift in Sources */, D056CD701FF147B000880D28 /* IconButtonNode.swift in Sources */, @@ -5301,6 +5321,7 @@ D087BFB31F748752003FD209 /* ShareControllerRecentPeersGridItem.swift in Sources */, D0EC6D341EB9F58800EBF1C3 /* AvatarNode.swift in Sources */, D08D7E8420A0F6020005D80C /* ExperimentalUISettings.swift in Sources */, + 09F79A0921C829C700820234 /* GalleryNavigationRecipientNode.swift in Sources */, D0EC6D351EB9F58800EBF1C3 /* SearchBarNode.swift in Sources */, D0EC6D361EB9F58800EBF1C3 /* SearchBarPlaceholderNode.swift in Sources */, D0E8B8B9204477B600605593 /* SecretChatKeyVisualization.swift in Sources */, @@ -5638,6 +5659,7 @@ D0F0AAE61EC21B68005EE2A5 /* CallControllerButton.swift in Sources */, D0EC6DDE1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingOverlayButton.swift in Sources */, D0E9BAC91F05738600F079A4 /* STPAPIClient+ApplePay.m in Sources */, + 09F79A0721C829BC00820234 /* GalleryNavigationCheckNode.swift in Sources */, D0EC6DDF1EB9F58900EBF1C3 /* ChatTextInputAudioRecordingTimeNode.swift in Sources */, D0BFAE5B20AB35D200793CF2 /* IconSwitchNode.swift in Sources */, D0EC6DE01EB9F58900EBF1C3 /* ChatTextInputAudioRecordingCancelIndicator.swift in Sources */, @@ -5842,6 +5864,7 @@ D09250061FE5371D003F693F /* GlobalExperimentalSettings.swift in Sources */, D0A24D281F92C27100584D24 /* DefaultDarkAccentPresentationTheme.swift in Sources */, D025A4231F79344500563950 /* FetchManager.swift in Sources */, + 09F79A0121C8116C00820234 /* WebSearchBadgeNode.swift in Sources */, D0CB27CF20C17A4A001ACF93 /* TermsOfServiceController.swift in Sources */, D00BDA1F1EE5B69200C64C5E /* ChannelAdminController.swift in Sources */, D0EC6E501EB9F58900EBF1C3 /* ChannelAdminsController.swift in Sources */, @@ -5897,6 +5920,7 @@ D0EC6E6E1EB9F58900EBF1C3 /* ArchivedStickerPacksController.swift in Sources */, D0DE5805205B202500C356A8 /* ScreenCaptureDetection.swift in Sources */, D0EC6E711EB9F58900EBF1C3 /* ThemeGalleryController.swift in Sources */, + 09F79A0B21C832F400820234 /* WebSearchGalleryFooterContentNode.swift in Sources */, D0C0B5B11EE1C421000F4D2C /* ChatDateSelectionSheet.swift in Sources */, D0EC6E721EB9F58900EBF1C3 /* ThemeGalleryItem.swift in Sources */, D00781052084DFB100369A39 /* UrlEscaping.swift in Sources */, diff --git a/TelegramUI/AvatarNode.swift b/TelegramUI/AvatarNode.swift index 02eb964124..4459b765c0 100644 --- a/TelegramUI/AvatarNode.swift +++ b/TelegramUI/AvatarNode.swift @@ -201,6 +201,7 @@ public final class AvatarNode: ASDisplayNode { } public func setPeer(account: Account, peer: Peer, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, synchronousLoad: Bool = false) { + var synchronousLoad = synchronousLoad var representation: TelegramMediaImageRepresentation? var icon = AvatarNodeIcon.none if let overrideImage = overrideImage { @@ -209,6 +210,7 @@ public final class AvatarNode: ASDisplayNode { representation = nil case let .image(image): representation = image + synchronousLoad = false case .savedMessagesIcon: representation = nil icon = .savedMessagesIcon diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index 1299be8325..1f3b12cae8 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -628,9 +628,9 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr })) }) }, changeProfilePhoto: { - let _ = (account.postbox.transaction { transaction -> Peer? in - return transaction.getPeer(peerId) - } |> deliverOnMainQueue).start(next: { peer in + let _ = (account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in + return (transaction.getPeer(peerId), currentSearchBotsConfiguration(transaction: transaction)) + } |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) @@ -650,28 +650,39 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr hasPhotos = true } - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: hasPhotos, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false)! + let completedImpl: (UIImage) -> Void = { image in + if let data = UIImageJPEGRepresentation(image, 0.6) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) + updateState { + $0.withUpdatedUpdatingAvatar(.image(representation, true)) + } + updateAvatarDisposable.set((updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: peerId, photo: uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: resource)) |> deliverOnMainQueue).start(next: { result in + switch result { + case .complete: + updateState { + $0.withUpdatedUpdatingAvatar(nil) + } + case .progress: + break + } + })) + } + } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos, hasViewButton: false, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)! let _ = currentAvatarMixin.swap(mixin) + mixin.requestSearchController = { assetsController in + let controller = WebSearchController(account: account, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(completion: { result in + assetsController?.dismiss() + completedImpl(result) + })) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } mixin.didFinishWithImage = { image in if let image = image { - if let data = UIImageJPEGRepresentation(image, 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) - account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) - updateState { - $0.withUpdatedUpdatingAvatar(.image(representation, true)) - } - updateAvatarDisposable.set((updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: peerId, photo: uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: resource)) |> deliverOnMainQueue).start(next: { result in - switch result { - case .complete: - updateState { - $0.withUpdatedUpdatingAvatar(nil) - } - case .progress: - break - } - })) - } + completedImpl(image) } } mixin.didFinishWithDelete = { diff --git a/TelegramUI/ChatBotInfoItem.swift b/TelegramUI/ChatBotInfoItem.swift index 26b0eb98e8..c59158f101 100644 --- a/TelegramUI/ChatBotInfoItem.swift +++ b/TelegramUI/ChatBotInfoItem.swift @@ -132,7 +132,6 @@ final class ChatBotInfoItemNode: ListViewItemNode { var updatedBackgroundImage: UIImage? if currentTheme != item.presentationData.theme { - //let principalGraphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) updatedBackgroundImage = PresentationResourcesChat.chatInfoItemBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty) } @@ -181,9 +180,9 @@ final class ChatBotInfoItemNode: ListViewItemNode { override func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) { if height.isLessThanOrEqualTo(0.0) { - transition.updateBounds(node: self.offsetContainer, bounds: CGRect(origin: CGPoint(), size: self.offsetContainer.bounds.size)) + transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(), size: self.offsetContainer.bounds.size)) } else { - transition.updateBounds(node: self.offsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: floor(height) / 2.0), size: self.offsetContainer.bounds.size)) + transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: -floorToScreenPixels(height / 2.0)), size: self.offsetContainer.bounds.size)) } } @@ -199,12 +198,18 @@ final class ChatBotInfoItemNode: ListViewItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + let result = super.point(inside: point, with: event) + let extra = self.offsetContainer.frame.contains(point) + return result || extra + } + func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { var rects: [CGRect]? if let point = point { 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 (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.offsetContainer.frame.minX - textNodeFrame.minX, y: point.y - self.offsetContainer.frame.minY - textNodeFrame.minY)) { let possibleNames: [String] = [ TelegramTextAttributes.URL, TelegramTextAttributes.PeerMention, @@ -228,7 +233,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { } else { linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.theme.chat.bubble.incomingLinkHighlightColor) self.linkHighlightingNode = linkHighlightingNode - self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) + self.offsetContainer.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) } linkHighlightingNode.frame = self.textNode.frame linkHighlightingNode.updateRects(rects) @@ -243,7 +248,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction { 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 (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.offsetContainer.frame.minX - textNodeFrame.minX, y: point.y - self.offsetContainer.frame.minY - textNodeFrame.minY)) { if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true if let attributeText = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index c3b0e59f83..19b18e1549 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -3894,9 +3894,17 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal configureLegacyAssetPicker(controller, account: strongSelf.account, peer: peer, presentWebSearch: { [weak self] in if let strongSelf = self { - let controller = WebSearchController(account: strongSelf.account, chatLocation: .peer(peer.id), configuration: searchBotsConfiguration, sendSelected: { (resuls, collection, editingContext) in - - }) + let controller = WebSearchController(account: strongSelf.account, peer: peer, configuration: searchBotsConfiguration, mode: .media(completion: { [weak self] selectionState, editingState in + legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in + if let strongSelf = self { + strongSelf.enqueueChatContextResult(nil, result) + } + }, enqueueMediaMessages: { [weak self] signals in + if let strongSelf = self { + strongSelf.enqueueMediaMessages(signals: signals) + } + }) + })) strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } }) @@ -3920,7 +3928,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal } private func presentWebSearch(editingMessage: Bool) { - guard let _ = self.presentationInterfaceState.renderedPeer?.peer else { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } @@ -3933,48 +3941,17 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal } |> deliverOnMainQueue).start(next: { [weak self] configuration in if let strongSelf = self { - let controller = WebSearchController(account: strongSelf.account, chatLocation: strongSelf.chatLocation, configuration: configuration, sendSelected: { [weak self] ids, collection, editingContext in - if let strongSelf = self { - var results: [ChatContextResult] = [] - for id in ids { - var result: ChatContextResult? - for r in collection.results { - if r.id == id { - result = r - results.append(r) - break - } - } + let controller = WebSearchController(account: strongSelf.account, peer: peer, configuration: configuration, mode: .media(completion: { [weak self] selectionState, editingState in + legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in + if let strongSelf = self { + strongSelf.enqueueChatContextResult(nil, result) } - - if !results.isEmpty { - var signals: [Any] = [] - for result in results { - let editableItem = LegacyWebSearchItem(result: result, dimensions: CGSize(), thumbnailImage: .complete(), originalImage: .complete()) - if editingContext.adjustments(for: editableItem) != nil { - if let imageSignal = editingContext.imageSignal(for: editableItem) { - let signal = imageSignal.map { image -> Any in - if let image = image as? UIImage { - let dict: [AnyHashable: Any] = [ - "type": "editedPhoto", - "image": image - ] - return legacyAssetPickerItemGenerator()(dict, nil, nil, nil) - } else { - return SSignal.complete() - } - } - signals.append(signal) - } - } else { - strongSelf.enqueueChatContextResult(collection, result, includeViaBot: false) - } - } - + }, enqueueMediaMessages: { [weak self] signals in + if let strongSelf = self { strongSelf.enqueueMediaMessages(signals: signals) } - } - }) + }) + })) strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } }) @@ -4250,11 +4227,11 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal })) } - private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, includeViaBot: Bool = true) { + private func enqueueChatContextResult(_ results: ChatContextResultCollection?, _ result: ChatContextResult) { guard case let .peer(peerId) = self.chatLocation else { return } - if let message = outgoingMessageWithChatContextResult(to: peerId, results: results, result: result, includeViaBot: includeViaBot), canSendMessagesToChat(self.presentationInterfaceState) { + if let message = outgoingMessageWithChatContextResult(to: peerId, results: results, result: result), canSendMessagesToChat(self.presentationInterfaceState) { let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index ff60e74ae5..ec61b0dccd 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -352,12 +352,12 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !self.disableDate) - node.updateSelectionState(animated: false) - node.updateHighlightedState(animated: false) - node.contentSize = layout.contentSize node.insets = layout.insets + node.updateSelectionState(animated: false) + node.updateHighlightedState(animated: false) + Queue.mainQueue().async { completion(node, { return (nil, { _ in apply(.None, synchronousLoads) }) diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index d2ee0fd89c..53beb2bde9 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -226,7 +226,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { mediaAndFlags = (file, []) } } else if let image = mainMedia as? TelegramMediaImage { - if let type = webpage.type, ["photo", "video", "embed", "gif", "telegram_album"].contains(type) { + if let type = webpage.type, ["photo", "video", "embed", "gif", "document", "telegram_album"].contains(type) { var flags = ChatMessageAttachedContentNodeMediaFlags() if webpage.instantPage != nil, let largest = largestImageRepresentation(image.representations) { if largest.dimensions.width >= 256.0 { diff --git a/TelegramUI/ChatTextInputMediaRecordingButton.swift b/TelegramUI/ChatTextInputMediaRecordingButton.swift index e4979528b0..c7e6b8dbc8 100644 --- a/TelegramUI/ChatTextInputMediaRecordingButton.swift +++ b/TelegramUI/ChatTextInputMediaRecordingButton.swift @@ -240,8 +240,6 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto self.disablesInteractiveTransitionGestureRecognizer = true - let inputPanelTheme = theme.chat.inputPanel - self.pallete = legacyInputMicPalette(from: theme) self.insertSubview(self.innerIconView, at: 0) @@ -348,7 +346,7 @@ final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButto func micButtonInteractionCancelled(_ velocity: CGPoint) { //print("\(CFAbsoluteTimeGetCurrent()) cancelled") self.modeTimeoutTimer?.invalidate() - self.stopRecording() + self.endRecording(false) } func micButtonInteractionCompleted(_ velocity: CGPoint) { diff --git a/TelegramUI/ChatTitleView.swift b/TelegramUI/ChatTitleView.swift index 537a843e3f..8d62a4d257 100644 --- a/TelegramUI/ChatTitleView.swift +++ b/TelegramUI/ChatTitleView.swift @@ -100,7 +100,21 @@ final class ChatTitleView: UIView, NavigationBarTitleView { var inputActivities: (PeerId, [(Peer, PeerInputActivity)])? { didSet { - if let (peerId, inputActivities) = self.inputActivities, !inputActivities.isEmpty { + var inputActivitiesAllowed = true + if let titleContent = self.titleContent { + switch titleContent { + case let .peer(peerView, _): + if let peer = peerViewMainPeer(peerView) { + if peer.id == self.account.peerId { + inputActivitiesAllowed = false + } + } + default: + break + } + } + + if let (peerId, inputActivities) = self.inputActivities, !inputActivities.isEmpty, inputActivitiesAllowed { self.typingNode.isHidden = false self.infoNode.isHidden = true var stringValue = "" diff --git a/TelegramUI/CheckNode.swift b/TelegramUI/CheckNode.swift index 5134c31fb3..61386f83c9 100644 --- a/TelegramUI/CheckNode.swift +++ b/TelegramUI/CheckNode.swift @@ -6,18 +6,22 @@ import LegacyComponents enum CheckNodeStyle { case plain case overlay + case navigation } final class CheckNode: ASDisplayNode { - private let strokeColor: UIColor - private let fillColor: UIColor - private let foregroundColor: UIColor + private var strokeColor: UIColor + private var fillColor: UIColor + private var foregroundColor: UIColor private let checkStyle: CheckNodeStyle private var checkView: TGCheckButtonView? private(set) var isChecked: Bool = false + private weak var target: AnyObject? + private var action: Selector? + init(strokeColor: UIColor, fillColor: UIColor, foregroundColor: UIColor, style: CheckNodeStyle) { self.strokeColor = strokeColor self.fillColor = fillColor @@ -31,19 +35,29 @@ final class CheckNode: ASDisplayNode { super.didLoad() let style: TGCheckButtonStyle + let checkSize: CGSize switch self.checkStyle { case .plain: style = TGCheckButtonStyleDefault + checkSize = CGSize(width: 32.0, height: 32.0) case .overlay: style = TGCheckButtonStyleMedia + checkSize = CGSize(width: 32.0, height: 32.0) + case .navigation: + style = TGCheckButtonStyleGallery + checkSize = CGSize(width: 39.0, height: 39.0) } let checkView = TGCheckButtonView(style: style, pallete: TGCheckButtonPallete(defaultBackgroundColor: self.fillColor, accentBackgroundColor: self.fillColor, defaultBorderColor: self.strokeColor, mediaBorderColor: self.strokeColor, chatBorderColor: self.strokeColor, check: self.foregroundColor, blueColor: self.fillColor, barBackgroundColor: self.fillColor))! checkView.setSelected(true, animated: false) checkView.layoutSubviews() checkView.setSelected(self.isChecked, animated: false) + if let target = self.target, let action = self.action { + checkView.addTarget(target, action: action, for: .touchUpInside) + } self.checkView = checkView self.view.addSubview(checkView) - checkView.frame = self.bounds + + checkView.frame = CGRect(origin: CGPoint(), size: checkSize) } func setIsChecked(_ isChecked: Bool, animated: Bool) { @@ -52,4 +66,12 @@ final class CheckNode: ASDisplayNode { self.checkView?.setSelected(isChecked, animated: animated) } } + + func addTarget(target: AnyObject?, action: Selector) { + self.target = target + self.action = action + if self.isNodeLoaded { + self.checkView?.addTarget(target, action: action, for: .touchUpInside) + } + } } diff --git a/TelegramUI/ContactsControllerNode.swift b/TelegramUI/ContactsControllerNode.swift index 187586d523..455b420100 100644 --- a/TelegramUI/ContactsControllerNode.swift +++ b/TelegramUI/ContactsControllerNode.swift @@ -43,18 +43,18 @@ final class ContactsControllerNode: ASDisplayNode { self.addSubnode(self.contactListNode) self.presentationDataDisposable = (account.telegramApplicationContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - let previousTheme = strongSelf.presentationData.theme - let previousStrings = strongSelf.presentationData.strings - - strongSelf.presentationData = presentationData - - if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { - strongSelf.updateThemeAndStrings() - } + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() } - }) + } + }) inviteImpl = { [weak self] in let _ = (DeviceAccess.contacts diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index 148aa51ea5..d5016916a0 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -791,7 +791,7 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode { if let verificationIconNode = self.verificationIconNode { var iconFrame = verificationIconNode.frame - iconFrame.origin.x = offset + titleFrame.maxX + 3.0 + iconFrame.origin.x = titleFrame.maxX + 3.0 transition.updateFrame(node: verificationIconNode, frame: iconFrame) } diff --git a/TelegramUI/CreateChannelController.swift b/TelegramUI/CreateChannelController.swift index 18175bf44d..53933bcfbe 100644 --- a/TelegramUI/CreateChannelController.swift +++ b/TelegramUI/CreateChannelController.swift @@ -258,56 +258,73 @@ public func createChannelController(account: Account) -> ViewController { })) } }, changeProfilePhoto: { - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - - let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) - legacyController.statusBar.statusBarStyle = .Ignore - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - legacyController.bind(controller: navigationController) - - endEditingImpl?() - presentControllerImpl?(legacyController, nil) - - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false)! - let _ = currentAvatarMixin.swap(mixin) - mixin.didFinishWithImage = { image in - if let image = image, let data = UIImageJPEGRepresentation(image, 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) - account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) - uploadedAvatar.set(uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: resource)) - updateState { current in - var current = current - current.avatar = .image(representation, false) - return current + let _ = (account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in + return (transaction.getPeer(account.peerId), currentSearchBotsConfiguration(transaction: transaction)) + } |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) + + legacyController.bind(controller: navigationController) + + endEditingImpl?() + presentControllerImpl?(legacyController, nil) + + let completedImpl: (UIImage) -> Void = { image in + if let data = UIImageJPEGRepresentation(image, 0.6) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) + uploadedAvatar.set(uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: resource)) + updateState { current in + var current = current + current.avatar = .image(representation, false) + return current + } } } - } - if stateValue.with({ $0.avatar }) != nil { - mixin.didFinishWithDelete = { - updateState { current in - var current = current - current.avatar = nil - return current - } - uploadedAvatar.set(.never()) + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)! + let _ = currentAvatarMixin.swap(mixin) + mixin.requestSearchController = { assetsController in + let controller = WebSearchController(account: account, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(completion: { result in + assetsController?.dismiss() + completedImpl(result) + })) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } - } - mixin.didDismiss = { [weak legacyController] in - let _ = currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - let menuController = mixin.present() - if let menuController = menuController { - menuController.customRemoveFromParentViewController = { [weak legacyController] in + mixin.didFinishWithImage = { image in + if let image = image { + completedImpl(image) + } + } + if stateValue.with({ $0.avatar }) != nil { + mixin.didFinishWithDelete = { + updateState { current in + var current = current + current.avatar = nil + return current + } + uploadedAvatar.set(.never()) + } + } + mixin.didDismiss = { [weak legacyController] in + let _ = currentAvatarMixin.swap(nil) legacyController?.dismiss() } - } + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } + }) }) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) diff --git a/TelegramUI/CreateGroupController.swift b/TelegramUI/CreateGroupController.swift index 596bc03710..1488e208e0 100644 --- a/TelegramUI/CreateGroupController.swift +++ b/TelegramUI/CreateGroupController.swift @@ -277,56 +277,73 @@ public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewCo })) } }, changeProfilePhoto: { - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - - let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) - legacyController.statusBar.statusBarStyle = .Ignore - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - legacyController.bind(controller: navigationController) - - endEditingImpl?() - presentControllerImpl?(legacyController, nil) - - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false)! - let _ = currentAvatarMixin.swap(mixin) - mixin.didFinishWithImage = { image in - if let image = image, let data = UIImageJPEGRepresentation(image, 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) - account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) - uploadedAvatar.set(uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: resource)) - updateState { current in - var current = current - current.avatar = .image(representation, false) - return current + let _ = (account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in + return (transaction.getPeer(account.peerId), currentSearchBotsConfiguration(transaction: transaction)) + } |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) + + legacyController.bind(controller: navigationController) + + endEditingImpl?() + presentControllerImpl?(legacyController, nil) + + let completedImpl: (UIImage) -> Void = { image in + if let data = UIImageJPEGRepresentation(image, 0.6) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) + uploadedAvatar.set(uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: resource)) + updateState { current in + var current = current + current.avatar = .image(representation, false) + return current + } } } - } - if stateValue.with({ $0.avatar }) != nil { - mixin.didFinishWithDelete = { - updateState { current in - var current = current - current.avatar = nil - return current - } - uploadedAvatar.set(.never()) + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)! + let _ = currentAvatarMixin.swap(mixin) + mixin.requestSearchController = { assetsController in + let controller = WebSearchController(account: account, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(completion: { result in + assetsController?.dismiss() + completedImpl(result) + })) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } - } - mixin.didDismiss = { [weak legacyController] in - let _ = currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - let menuController = mixin.present() - if let menuController = menuController { - menuController.customRemoveFromParentViewController = { [weak legacyController] in + mixin.didFinishWithImage = { image in + if let image = image { + completedImpl(image) + } + } + if stateValue.with({ $0.avatar }) != nil { + mixin.didFinishWithDelete = { + updateState { current in + var current = current + current.avatar = nil + return current + } + uploadedAvatar.set(.never()) + } + } + mixin.didDismiss = { [weak legacyController] in + let _ = currentAvatarMixin.swap(nil) legacyController?.dismiss() } - } + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } + }) }) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.postbox.multiplePeersView(peerIds)) diff --git a/TelegramUI/DefaultDarkAccentPresentationTheme.swift b/TelegramUI/DefaultDarkAccentPresentationTheme.swift index 08df9818c0..b5f4c96852 100644 --- a/TelegramUI/DefaultDarkAccentPresentationTheme.swift +++ b/TelegramUI/DefaultDarkAccentPresentationTheme.swift @@ -171,15 +171,15 @@ private let bubble = PresentationThemeChatBubble( incomingFileDurationColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), outgoingFileDurationColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x18222D, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x18222D, alpha: 0.5)), - shareButtonStrokeColor: UIColor(rgb: 0x213040), - shareButtonForegroundColor: UIColor(rgb: 0xb2b2b2), //!!! + shareButtonStrokeColor: UIColor(rgb: 0x587fa3, alpha: 0.15), + shareButtonForegroundColor: UIColor(rgb: 0xb2b2b2), mediaOverlayControlBackgroundColor: UIColor(white: 0.0, alpha: 0.6), //!!! mediaOverlayControlForegroundColor: UIColor(white: 1.0, alpha: 1.0), //!!! actionButtonsIncomingFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x18222D, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x18222D, alpha: 0.5)), - actionButtonsIncomingStrokeColor: UIColor(rgb: 0x213040), + actionButtonsIncomingStrokeColor: UIColor(rgb: 0x587fa3, alpha: 0.15), actionButtonsIncomingTextColor: UIColor(rgb: 0xffffff), actionButtonsOutgoingFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x18222D, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x18222D, alpha: 0.5)), - actionButtonsOutgoingStrokeColor: UIColor(rgb: 0x213040), + actionButtonsOutgoingStrokeColor: UIColor(rgb: 0x587fa3, alpha: 0.15), actionButtonsOutgoingTextColor: UIColor(rgb: 0xffffff), selectionControlBorderColor: .white, selectionControlFillColor: accentColor, diff --git a/TelegramUI/DefaultDarkPresentationTheme.swift b/TelegramUI/DefaultDarkPresentationTheme.swift index dc9fff4017..65d30f6cdd 100644 --- a/TelegramUI/DefaultDarkPresentationTheme.swift +++ b/TelegramUI/DefaultDarkPresentationTheme.swift @@ -171,15 +171,15 @@ private let bubble = PresentationThemeChatBubble( incomingFileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), outgoingFileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), - shareButtonStrokeColor: UIColor(rgb: 0x1f1f1f), + shareButtonStrokeColor: UIColor(rgb: 0xb2b2b2, alpha: 0.18), shareButtonForegroundColor: UIColor(rgb: 0xb2b2b2), //!!! mediaOverlayControlBackgroundColor: UIColor(white: 0.0, alpha: 0.6), //!!! mediaOverlayControlForegroundColor: UIColor(white: 1.0, alpha: 1.0), //!!! actionButtonsIncomingFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), - actionButtonsIncomingStrokeColor: UIColor(rgb: 0x1f1f1f), + actionButtonsIncomingStrokeColor: UIColor(rgb: 0xb2b2b2, alpha: 0.18), actionButtonsIncomingTextColor: UIColor(rgb: 0xffffff), actionButtonsOutgoingFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), - actionButtonsOutgoingStrokeColor: UIColor(rgb: 0x1f1f1f), + actionButtonsOutgoingStrokeColor: UIColor(rgb: 0xb2b2b2, alpha: 0.18), actionButtonsOutgoingTextColor: UIColor(rgb: 0xffffff), selectionControlBorderColor: .white, selectionControlFillColor: accentColor, diff --git a/TelegramUI/EditSettingsController.swift b/TelegramUI/EditSettingsController.swift index 27d21dbd44..58f11f1a05 100644 --- a/TelegramUI/EditSettingsController.swift +++ b/TelegramUI/EditSettingsController.swift @@ -420,106 +420,117 @@ func editSettingsController(account: Account, currentName: ItemListAvatarAndName } } changeProfilePhotoImpl = { [weak controller] in - let _ = (account.postbox.transaction { transaction -> Peer? in - return transaction.getPeer(account.peerId) - } |> deliverOnMainQueue).start(next: { peer in - controller?.view.endEditing(true) - - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - - let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) - legacyController.statusBar.statusBarStyle = .Ignore - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - legacyController.bind(controller: navigationController) - - presentControllerImpl?(legacyController, nil) - - var hasPhotos = false - if let peer = peer, !peer.profileImageRepresentations.isEmpty { - hasPhotos = true - } - - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: hasPhotos, hasViewButton: hasPhotos, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)! - let _ = currentAvatarMixin.swap(mixin) - mixin.didFinishWithImage = { image in - if let image = image { - if let data = UIImageJPEGRepresentation(image, 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) - account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) - updateState { - $0.withUpdatedUpdatingAvatar(.image(representation, true)) - } - updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: resource) |> deliverOnMainQueue).start(next: { result in - switch result { - case .complete: - updateState { - $0.withUpdatedUpdatingAvatar(nil) - } - case .progress: - break - } - })) - } - } - } - mixin.didFinishWithDelete = { - let _ = currentAvatarMixin.swap(nil) + let _ = (account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in + return (transaction.getPeer(account.peerId), currentSearchBotsConfiguration(transaction: transaction)) + } |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in + controller?.view.endEditing(true) + + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) + legacyController.statusBar.statusBarStyle = .Ignore + + let emptyController = LegacyEmptyController(context: legacyController.context)! + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) + + legacyController.bind(controller: navigationController) + + presentControllerImpl?(legacyController, nil) + + var hasPhotos = false + if let peer = peer, !peer.profileImageRepresentations.isEmpty { + hasPhotos = true + } + + let completedImpl: (UIImage) -> Void = { image in + if let data = UIImageJPEGRepresentation(image, 0.6) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) updateState { - if let profileImage = peer?.smallProfileImage { - return $0.withUpdatedUpdatingAvatar(.image(profileImage, false)) - } else { - return $0.withUpdatedUpdatingAvatar(.none) - } + $0.withUpdatedUpdatingAvatar(.image(representation, true)) } - updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: nil) |> deliverOnMainQueue).start(next: { result in + updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: resource) |> deliverOnMainQueue).start(next: { result in switch result { - case .complete: - updateState { - $0.withUpdatedUpdatingAvatar(nil) - } - case .progress: - break + case .complete: + updateState { + $0.withUpdatedUpdatingAvatar(nil) + } + case .progress: + break } })) } - mixin.didFinishWithView = { - let _ = currentAvatarMixin.swap(nil) - - let _ = (account.postbox.loadedPeerWithId(account.peerId) - |> take(1) - |> deliverOnMainQueue).start(next: { peer in - if peer.smallProfileImage != nil { - let galleryController = AvatarGalleryController(account: account, peer: peer, replaceRootController: { controller, ready in - }) - /*hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in - avatarAndNameInfoContext.hiddenAvatarRepresentation = entry?.representations.first - updateHiddenAvatarImpl?() - }))*/ - presentControllerImpl?(galleryController, AvatarGalleryControllerPresentationArguments(transitionArguments: { entry in - return nil - })) - } else { - changeProfilePhotoImpl?() - } - }) + } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos, hasViewButton: hasPhotos, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)! + let _ = currentAvatarMixin.swap(mixin) + mixin.requestSearchController = { assetsController in + let controller = WebSearchController(account: account, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(completion: { result in + assetsController?.dismiss() + completedImpl(result) + })) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + mixin.didFinishWithImage = { image in + if let image = image { + completedImpl(image) } - mixin.didDismiss = { [weak legacyController] in - let _ = currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - let menuController = mixin.present() - if let menuController = menuController { - menuController.customRemoveFromParentViewController = { [weak legacyController] in - legacyController?.dismiss() + } + mixin.didFinishWithDelete = { + let _ = currentAvatarMixin.swap(nil) + updateState { + if let profileImage = peer?.smallProfileImage { + return $0.withUpdatedUpdatingAvatar(.image(profileImage, false)) + } else { + return $0.withUpdatedUpdatingAvatar(.none) } } - }) + updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: nil) |> deliverOnMainQueue).start(next: { result in + switch result { + case .complete: + updateState { + $0.withUpdatedUpdatingAvatar(nil) + } + case .progress: + break + } + })) + } + mixin.didFinishWithView = { + let _ = currentAvatarMixin.swap(nil) + + let _ = (account.postbox.loadedPeerWithId(account.peerId) + |> take(1) + |> deliverOnMainQueue).start(next: { peer in + if peer.smallProfileImage != nil { + let galleryController = AvatarGalleryController(account: account, peer: peer, replaceRootController: { controller, ready in + }) + /*hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in + avatarAndNameInfoContext.hiddenAvatarRepresentation = entry?.representations.first + updateHiddenAvatarImpl?() + }))*/ + presentControllerImpl?(galleryController, AvatarGalleryControllerPresentationArguments(transitionArguments: { entry in + return nil + })) + } else { + changeProfilePhotoImpl?() + } + }) + } + mixin.didDismiss = { [weak legacyController] in + let _ = currentAvatarMixin.swap(nil) + legacyController?.dismiss() + } + let menuController = mixin.present() + if let menuController = menuController { + menuController.customRemoveFromParentViewController = { [weak legacyController] in + legacyController?.dismiss() + } + } + }) } return controller diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 4422cf80d3..d77f4894de 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -180,7 +180,9 @@ func galleryItemForEntry(account: Account, presentationData: PresentationData, e /*case .twitter where webpageContent.embedUrl != nil && webpageContent.image != nil: return UniversalVideoGalleryItem(account: account, theme: theme, strings: strings, content: SystemVideoContent(url: webpageContent.embedUrl!, image: webpageContent.image!, dimensions: webpageContent.embedSize ?? CGSize(width: 640.0, height: 640.0), duration: Int32(webpageContent.duration ?? 0)), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: "")*/ default: - if let content = WebEmbedVideoContent(webPage: webpage, webpageContent: webpageContent) { + if let embedUrl = webpageContent.embedUrl, let image = webpageContent.image, URL(string: embedUrl)?.pathExtension == "mp4" { + return UniversalVideoGalleryItem(account: account, presentationData: presentationData, content: SystemVideoContent(url: embedUrl, imageReference: .webPage(webPage: WebpageReference(webpage), media: image), dimensions: webpageContent.embedSize ?? CGSize(width: 640.0, height: 640.0), duration: Int32(webpageContent.duration ?? 0)), originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), performAction: performAction, openActionOptions: openActionOptions) + } else if let content = WebEmbedVideoContent(webPage: webpage, webpageContent: webpageContent) { return UniversalVideoGalleryItem(account: account, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), performAction: performAction, openActionOptions: openActionOptions) } } diff --git a/TelegramUI/GalleryNavigationCheckNode.swift b/TelegramUI/GalleryNavigationCheckNode.swift new file mode 100644 index 0000000000..da2ea17989 --- /dev/null +++ b/TelegramUI/GalleryNavigationCheckNode.swift @@ -0,0 +1,39 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class GalleryNavigationCheckNode: ASDisplayNode { + private var checkNode: CheckNode + + init(theme: PresentationTheme) { + self.checkNode = CheckNode(strokeColor: theme.list.itemCheckColors.strokeColor, fillColor: theme.list.itemCheckColors.fillColor, foregroundColor: theme.list.itemCheckColors.foregroundColor, style: .navigation) + + super.init() + + self.addSubnode(self.checkNode) + } + + var isChecked: Bool { + return self.checkNode.isChecked + } + + func setIsChecked(_ isChecked: Bool, animated: Bool) { + self.checkNode.setIsChecked(isChecked, animated: animated) + } + + func addTarget(target: AnyObject?, action: Selector) { + self.checkNode.addTarget(target: target, action: action) + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: 39.0, height: 39.0) + } + + override func layout() { + super.layout() + + let size = self.bounds.size + let checkSize = CGSize(width: 39.0, height: 39.0) + self.checkNode.frame = CGRect(origin: CGPoint(x: floor((size.width - checkSize.width) / 2.0) + 11.0, y: floor((size.height - checkSize.height) / 2.0) + 3.0), size: checkSize) + } +} diff --git a/TelegramUI/GalleryNavigationRecipientNode.swift b/TelegramUI/GalleryNavigationRecipientNode.swift new file mode 100644 index 0000000000..22460a54a4 --- /dev/null +++ b/TelegramUI/GalleryNavigationRecipientNode.swift @@ -0,0 +1,37 @@ +import Foundation +import AsyncDisplayKit +import Display +import LegacyComponents + +final class GalleryNavigationRecipientNode: ASDisplayNode { + private var iconNode: ASImageNode + private var textNode: ASTextNode + + init(color: UIColor, title: String) { + self.iconNode = ASImageNode() + self.iconNode.alpha = 0.45 + self.iconNode.image = TGComponentsImageNamed("PhotoPickerArrow") + + self.textNode = ASTextNode() + self.textNode.attributedText = NSAttributedString(string: title, font: Font.bold(13.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.45)) + + super.init() + + self.addSubnode(self.iconNode) + self.addSubnode(self.textNode) + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: 30.0, height: 30.0) + } + + override func layout() { + super.layout() + + if let image = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: -2.0, y: 9.0), size: image.size) + } + + self.textNode.frame = CGRect(x: self.iconNode.frame.maxX + 6.0, y: 7.0, width: 150.0, height: 20.0) + } +} diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index d091234036..d239a7284b 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -1203,9 +1203,9 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl })) }) }, changeProfilePhoto: { - let _ = (account.postbox.transaction { transaction -> Peer? in - return transaction.getPeer(peerId) - } |> deliverOnMainQueue).start(next: { peer in + let _ = (account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in + return (transaction.getPeer(peerId), currentSearchBotsConfiguration(transaction: transaction)) + } |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) @@ -1225,28 +1225,39 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl hasPhotos = true } - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: hasPhotos, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false)! + let completedImpl: (UIImage) -> Void = { image in + if let data = UIImageJPEGRepresentation(image, 0.6) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) + updateState { + $0.withUpdatedUpdatingAvatar(.image(representation, true)) + } + updateAvatarDisposable.set((updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: peerId, photo: uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: resource)) |> deliverOnMainQueue).start(next: { result in + switch result { + case .complete: + updateState { + $0.withUpdatedUpdatingAvatar(nil) + } + case .progress: + break + } + })) + } + } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos, hasViewButton: false, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)! let _ = currentAvatarMixin.swap(mixin) + mixin.requestSearchController = { assetsController in + let controller = WebSearchController(account: account, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(completion: { result in + assetsController?.dismiss() + completedImpl(result) + })) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } mixin.didFinishWithImage = { image in if let image = image { - if let data = UIImageJPEGRepresentation(image, 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) - account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) - updateState { - $0.withUpdatedUpdatingAvatar(.image(representation, true)) - } - updateAvatarDisposable.set((updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: peerId, photo: uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: resource)) |> deliverOnMainQueue).start(next: { result in - switch result { - case .complete: - updateState { - $0.withUpdatedUpdatingAvatar(nil) - } - case .progress: - break - } - })) - } + completedImpl(image) } } mixin.didFinishWithDelete = { @@ -1361,9 +1372,25 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl var options: [ContactListAdditionalOption] = [] let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } var inviteByLinkImpl: (() -> Void)? - options.append(ContactListAdditionalOption(title: presentationData.strings.GroupInfo_InviteByLink, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/LinkActionIcon"), color: presentationData.theme.list.itemAccentColor), action: { - inviteByLinkImpl?() - })) + + var canCreateInviteLink = false + if let group = groupPeer as? TelegramGroup { + if case .creator = group.role { + canCreateInviteLink = true + } + } else if let channel = groupPeer as? TelegramChannel { + if channel.hasAdminRights(.canInviteUsers) { + canCreateInviteLink = true + } else if case let .group(info) = channel.info, info.flags.contains(.everyMemberCanInviteMembers) { + canCreateInviteLink = true + } + } + + if canCreateInviteLink { + options.append(ContactListAdditionalOption(title: presentationData.strings.GroupInfo_InviteByLink, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/LinkActionIcon"), color: presentationData.theme.list.itemAccentColor), action: { + inviteByLinkImpl?() + })) + } let contactsController: ViewController if peerId.namespace == Namespaces.Peer.CloudGroup { diff --git a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift index dc4fb29e82..795d6dac62 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -209,11 +209,16 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont } strongSelf.isLoadingMore = false var results: [ChatContextResult] = [] + var existingIds = Set() for result in currentProcessedResults.results { results.append(result) + existingIds.insert(result.id) } for result in nextResults.results { - results.append(result) + if !existingIds.contains(result.id) { + results.append(result) + existingIds.insert(result.id) + } } let mergedResults = ChatContextResultCollection(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, geoPoint: currentProcessedResults.geoPoint, queryId: nextResults.queryId, nextOffset: nextResults.nextOffset, presentation: currentProcessedResults.presentation, switchPeer: currentProcessedResults.switchPeer, results: results, cacheTimeout: currentProcessedResults.cacheTimeout) strongSelf.currentProcessedResults = mergedResults diff --git a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift index c49119a419..e005467dfb 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift @@ -195,8 +195,6 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updatedStatusSignal: Signal? -//messageFileMediaResourceStatus(account: account, file: file, message: message, isRecentActions: isRecentActions) - var imageResource: TelegramMediaResource? var stickerFile: TelegramMediaFile? diff --git a/TelegramUI/ID3Artwork.m b/TelegramUI/ID3Artwork.m index c1f9c83b23..a53b5c2a77 100644 --- a/TelegramUI/ID3Artwork.m +++ b/TelegramUI/ID3Artwork.m @@ -71,11 +71,18 @@ NSData * _Nullable albumArtworkData(NSData * _Nonnull data) { uint32_t pos = 0; while (pos < size) { const uint8_t * const frameBytes = ptr + pos; + if (ID3TagOffset + pos + 4 >= data.length) { + return nil; + } uint32_t frameSize = frameSizeForBytes(frameBytes, version); + if (ID3TagOffset + pos + frameSize >= data.length) { + return nil; + } if (isArtworkFrame(frameBytes, version)) { uint32_t frameOffset = frameOffsetForVersion(version); const uint8_t *ptr = frameBytes + frameOffset; + uint32_t start = ID3TagOffset + pos + frameOffset; bool isJpg = false; uint32_t imageOffset = UINT32_MAX; @@ -91,7 +98,6 @@ NSData * _Nullable albumArtworkData(NSData * _Nonnull data) { } if (imageOffset != UINT32_MAX) { - uint32_t start = ID3TagOffset + pos + frameOffset; if (isJpg) { NSMutableData *jpgData = [[NSMutableData alloc] initWithCapacity:frameSize + 1024]; uint8_t previousByte = 0xff; @@ -103,11 +109,7 @@ NSData * _Nullable albumArtworkData(NSData * _Nonnull data) { return nil; } uint8_t byte = (uint8_t)ptr[offset]; -// if (byte == 0x00 && previousByte == 0xff) { -// skippedBytes++; -// } else { - [jpgData appendBytes:&byte length:1]; -// } + [jpgData appendBytes:&byte length:1]; if (byte == 0xd9 && previousByte == 0xff) { break; } diff --git a/TelegramUI/InstantPageReferenceControllerNode.swift b/TelegramUI/InstantPageReferenceControllerNode.swift index 2e4187611f..cf1a828565 100644 --- a/TelegramUI/InstantPageReferenceControllerNode.swift +++ b/TelegramUI/InstantPageReferenceControllerNode.swift @@ -1,6 +1,7 @@ import Foundation import Display import AsyncDisplayKit +import Postbox import TelegramCore import SafariServices @@ -188,8 +189,13 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie if self.contentNode == nil || self.contentNode?.frame.width != width { self.contentNode?.removeFromSupernode() + var media: [MediaId: Media] = [:] + if case let .Loaded(content) = self.webPage.content, let instantPage = content.instantPage { + media = instantPage.media + } + let sideInset: CGFloat = 16.0 - let (_, items, contentSize) = layoutTextItemWithString(self.item.attributedString, boundingWidth: width - sideInset * 2.0, offset: CGPoint(x: sideInset, y: sideInset)) + let (_, items, contentSize) = layoutTextItemWithString(self.item.attributedString, boundingWidth: width - sideInset * 2.0, offset: CGPoint(x: sideInset, y: sideInset), media: media, webpage: self.webPage) let contentNode = InstantPageContentNode(account: self.account, strings: self.presentationData.strings, theme: self.theme, items: items, contentSize: CGSize(width: width, height: contentSize.height), inOverlayPanel: true, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in }) transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleAreaHeight), size: CGSize(width: width, height: contentSize.height))) self.contentContainerNode.insertSubnode(contentNode, at: 0) diff --git a/TelegramUI/InstantPageTextItem.swift b/TelegramUI/InstantPageTextItem.swift index 48c8248249..6f6e181483 100644 --- a/TelegramUI/InstantPageTextItem.swift +++ b/TelegramUI/InstantPageTextItem.swift @@ -508,7 +508,10 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt }) let delegate = CTRunDelegateCreate(&callbacks, extentBuffer) let attrDictionaryDelegate = [(kCTRunDelegateAttributeName as NSAttributedStringKey): (delegate as Any), NSAttributedStringKey(rawValue: InstantPageMediaIdAttribute): id.id, NSAttributedStringKey(rawValue: InstantPageMediaDimensionsAttribute): dimensions] - return NSAttributedString(string: " ", attributes: attrDictionaryDelegate) + let mutableAttributedString = attributedStringForRichText(.plain(" "), styleStack: styleStack, url: url).mutableCopy() as! NSMutableAttributedString + mutableAttributedString.addAttributes(attrDictionaryDelegate, range: NSMakeRange(0, mutableAttributedString.length)) + return mutableAttributedString + //return NSAttributedString(string: " ", attributes: attrDictionaryDelegate) case let .anchor(text, name): var empty = false var text = text @@ -569,7 +572,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo var workingLineOrigin = currentLineOrigin let currentMaxWidth = boundingWidth - workingLineOrigin.x - let lineCharacterCount: CFIndex + var lineCharacterCount: CFIndex var hadIndexOffset = false if minimizeWidth { var count = 0 @@ -584,6 +587,9 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo let suggestedLineBreak = CTTypesetterSuggestLineBreak(typesetter, lastIndex, Double(currentMaxWidth)) if let offset = indexOffset { lineCharacterCount = suggestedLineBreak + offset + if lineCharacterCount <= 0 { + lineCharacterCount = suggestedLineBreak + } indexOffset = nil hadIndexOffset = true } else { diff --git a/TelegramUI/InviteContactsController.swift b/TelegramUI/InviteContactsController.swift index 724acda96d..ae689151b3 100644 --- a/TelegramUI/InviteContactsController.swift +++ b/TelegramUI/InviteContactsController.swift @@ -44,18 +44,18 @@ public class InviteContactsController: ViewController, MFMessageComposeViewContr } self.presentationDataDisposable = (account.telegramApplicationContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - let previousTheme = strongSelf.presentationData.theme - let previousStrings = strongSelf.presentationData.strings - - strongSelf.presentationData = presentationData - - if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { - strongSelf.updateThemeAndStrings() - } + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() } - }) + } + }) } required public init(coder aDecoder: NSCoder) { diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index 1203230f00..58aa5ae325 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -210,7 +210,7 @@ class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { node.insets = layout.insets completion(node, { - return (nil, { _ in apply(false) }) + return (nil, { _ in apply(false, synchronousLoads) }) }) } } @@ -228,7 +228,7 @@ class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { _ in - apply(animated) + apply(animated, false) }) } } @@ -336,7 +336,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in if let strongSelf = self, let item = strongSelf.item, let layoutWidthAndNeighbors = strongSelf.layoutWidthAndNeighbors { let (_, apply) = strongSelf.asyncLayout()(item, layoutWidthAndNeighbors.0, layoutWidthAndNeighbors.1) - apply(true) + apply(true, false) } }) @@ -353,7 +353,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.avatarTapGesture(_:)))) } - func asyncLayout() -> (_ item: ItemListAvatarAndNameInfoItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + func asyncLayout() -> (_ item: ItemListAvatarAndNameInfoItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let layoutNameNode = TextNode.asyncLayout(self.nameNode) let layoutStatusNode = TextNode.asyncLayout(self.statusNode) let currentOverlayImage = self.updatingAvatarOverlay.image @@ -517,7 +517,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size - return (layout, { [weak self] animated in + return (layout, { [weak self] animated, synchronousLoads in if let strongSelf = self { strongSelf.item = item @@ -551,7 +551,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite } else if strongSelf.updatingAvatarOverlay.supernode != nil { if animated { strongSelf.updatingAvatarOverlay.alpha = 0.0 - strongSelf.updatingAvatarOverlay.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { value in + strongSelf.updatingAvatarOverlay.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, completion: { value in if value { self?.updatingAvatarOverlay.removeFromSupernode() } @@ -620,6 +620,10 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite let _ = nameNodeApply() let _ = statusNodeApply() + var ignoreEmpty = false + if case .editSettings = item.mode { + ignoreEmpty = true + } if let peer = item.peer { var overrideImage: AvatarNodeImageOverride? if let updatingImage = item.updatingImage { @@ -632,7 +636,8 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNode, Ite } else if case .editSettings = item.mode { overrideImage = AvatarNodeImageOverride.editAvatarIcon } - strongSelf.avatarNode.setPeer(account: item.account, peer: peer, overrideImage: overrideImage, emptyColor: item.theme.list.mediaPlaceholderColor, synchronousLoad: true) + + strongSelf.avatarNode.setPeer(account: item.account, peer: peer, overrideImage: overrideImage, emptyColor: ignoreEmpty ? nil : item.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) } let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: avatarOriginY), size: CGSize(width: 66.0, height: 66.0)) diff --git a/TelegramUI/LegacyAvatarPicker.swift b/TelegramUI/LegacyAvatarPicker.swift index 3ba161c40e..bf75f00a92 100644 --- a/TelegramUI/LegacyAvatarPicker.swift +++ b/TelegramUI/LegacyAvatarPicker.swift @@ -16,7 +16,7 @@ func presentLegacyAvatarPicker(holder: Atomic, signup: Bool, theme: P present(legacyController, nil) - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: false, hasViewButton: openCurrent != nil, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false, signup: signup)! + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: false, hasDeleteButton: false, hasViewButton: openCurrent != nil, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false, signup: signup)! let _ = holder.swap(mixin) mixin.didFinishWithImage = { image in guard let image = image else { diff --git a/TelegramUI/LegacyWebSearchEditor.swift b/TelegramUI/LegacyWebSearchEditor.swift new file mode 100644 index 0000000000..33a74503b7 --- /dev/null +++ b/TelegramUI/LegacyWebSearchEditor.swift @@ -0,0 +1,89 @@ +import Foundation +import LegacyComponents +import SwiftSignalKit +import TelegramCore +import Postbox +import SSignalKit +import UIKit +import Display + +func presentLegacyWebSearchEditor(account: Account, theme: PresentationTheme, result: ChatContextResult, initialLayout: ContainerViewLayout?, updateHiddenMedia: @escaping (String?) -> Void, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (UIImage) -> Void, present: @escaping (ViewController, Any?) -> Void) { + guard let item = legacyWebSearchItem(account: account, result: result) else { + return + } + + var screenImage: Signal = .single(nil) + if let resource = item.thumbnailResource { + screenImage = account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: true) + |> map { maybeData -> UIImage? in + if maybeData.complete { + if let loadedData = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []), let image = UIImage(data: loadedData) { + return image + } + } + return nil + } + } + + let _ = (screenImage + |> deliverOnMainQueue).start(next: { screenImage in + let legacyController = LegacyController(presentation: .custom, theme: theme, initialLayout: initialLayout) + legacyController.statusBar.statusBarStyle = .Ignore + + let controller = TGPhotoEditorController(context: legacyController.context, item: item, intent: TGPhotoEditorControllerAvatarIntent, adjustments: nil, caption: nil, screenImage: nil, availableTabs: TGPhotoEditorController.defaultTabsForAvatarIntent(), selectedTab: .cropTab)! + legacyController.bind(controller: controller) + + controller.editingContext = TGMediaEditingContext() + controller.didFinishEditing = { [weak controller] _, result, _, hasChanges in + if !hasChanges { + return + } + if let result = result { + completed(result) + } + controller?.dismiss(animated: true) + } + controller.requestThumbnailImage = { _ -> SSignal in + return item.thumbnailImageSignal() + } + controller.requestOriginalScreenSizeImage = { _, position -> SSignal in + return item.screenImageSignal(position) + } + controller.requestOriginalFullSizeImage = { _, position -> SSignal in + return item.originalImageSignal(position) + } + + let fromView = transitionView(result)! + let transition = TGMediaAvatarEditorTransition(controller: controller, from: fromView)! + transition.transitionHostView = transitionHostView() + transition.referenceFrame = { + return fromView.frame + } + transition.referenceImageSize = { + return item.dimensions + } + transition.referenceScreenImageSignal = { + return item.screenImageSignal(0.0) + } + transition.imageReady = { + updateHiddenMedia(result.id) + } + + controller.beginCustomTransitionOut = { [weak legacyController] outFrame, outView, completion in + transition.outReferenceFrame = outFrame + transition.repView = outView + transition.dismiss(animated: true, completion: { + updateHiddenMedia(nil) + if let completion = completion { + DispatchQueue.main.async { + completion() + } + } + legacyController?.dismiss() + }) + } + + present(legacyController, nil) + transition.present(animated: true) + }) +} diff --git a/TelegramUI/LegacyWebSearchGallery.swift b/TelegramUI/LegacyWebSearchGallery.swift index 61bc72660b..1835c59b8d 100644 --- a/TelegramUI/LegacyWebSearchGallery.swift +++ b/TelegramUI/LegacyWebSearchGallery.swift @@ -17,15 +17,29 @@ class LegacyWebSearchItem: NSObject, TGMediaEditableItem, TGMediaSelectableItem } let result: ChatContextResult + private(set) var thumbnailResource: TelegramMediaResource? + private(set) var imageResource: TelegramMediaResource? let dimensions: CGSize let thumbnailImage: Signal let originalImage: Signal + let progress: Signal - init(result: ChatContextResult, dimensions: CGSize, thumbnailImage: Signal, originalImage: Signal) { + init(result: ChatContextResult) { self.result = result + self.dimensions = CGSize() + self.thumbnailImage = .complete() + self.originalImage = .complete() + self.progress = .complete() + } + + init(result: ChatContextResult, thumbnailResource: TelegramMediaResource?, imageResource: TelegramMediaResource?, dimensions: CGSize, thumbnailImage: Signal, originalImage: Signal, progress: Signal) { + self.result = result + self.thumbnailResource = thumbnailResource + self.imageResource = imageResource self.dimensions = dimensions self.thumbnailImage = thumbnailImage self.originalImage = originalImage + self.progress = progress } var originalSize: CGSize { @@ -45,6 +59,30 @@ class LegacyWebSearchItem: NSObject, TGMediaEditableItem, TGMediaSelectableItem }) } + func screenImageAndProgressSignal() -> SSignal { + return SSignal { subscriber in + let imageDisposable = self.originalImage.start(next: { image in + if !image.degraded() { + subscriber?.putNext(1.0) + } + subscriber?.putNext(image) + if !image.degraded() { + subscriber?.putCompletion() + } + }) + + let progressDisposable = (self.progress + |> deliverOnMainQueue).start(next: { next in + subscriber?.putNext(next) + }) + + return SBlockDisposable { + imageDisposable.dispose() + progressDisposable.dispose() + } + } + } + func screenImageSignal(_ position: TimeInterval) -> SSignal! { return self.originalImageSignal(position) } @@ -53,7 +91,9 @@ class LegacyWebSearchItem: NSObject, TGMediaEditableItem, TGMediaSelectableItem return SSignal(generator: { subscriber -> SDisposable? in let disposable = self.originalImage.start(next: { image in subscriber?.putNext(image) - subscriber?.putCompletion() + if !image.degraded() { + subscriber?.putCompletion() + } }) return SBlockDisposable(block: { @@ -93,14 +133,26 @@ private class LegacyWebSearchGalleryItem: TGModernGalleryImageItem, TGModernGall override func viewClass() -> AnyClass! { return LegacyWebSearchGalleryItemView.self } + + override func isEqual(_ object: Any?) -> Bool { + if let item = object as? LegacyWebSearchGalleryItem { + return item.item.result.id == self.item.result.id + } + return false + } } -private class LegacyWebSearchGalleryItemView: TGModernGalleryImageItemView, TGModernGalleryEditableItemView -{ +private class LegacyWebSearchGalleryItemView: TGModernGalleryImageItemView, TGModernGalleryEditableItemView { + private let readyForTransition = SVariable()! + func setHiddenAsBeingEdited(_ hidden: Bool) { self.imageView.isHidden = hidden } + override func readyForTransitionIn() -> SSignal! { + return self.readyForTransition.signal()!.take(1) + } + override func setItem(_ item: TGModernGalleryItem!, synchronously: Bool) { if let item = item as? LegacyWebSearchGalleryItem { self._setItem(item) @@ -110,7 +162,7 @@ private class LegacyWebSearchGalleryItemView: TGModernGalleryImageItemView, TGMo if let image = result as? UIImage { return SSignal.single(image) } else if result == nil, let mediaItem = item.editableMediaItem() as? LegacyWebSearchItem { - return mediaItem.originalImageSignal(0.0) + return mediaItem.screenImageAndProgressSignal() } else { return SSignal.complete() } @@ -120,6 +172,7 @@ private class LegacyWebSearchGalleryItemView: TGModernGalleryImageItemView, TGMo if let strongSelf = self, let image = next as? UIImage { strongSelf.imageSize = image.size strongSelf.reset() + strongSelf.readyForTransition.set(SSignal.single(true)) } })) @@ -144,63 +197,90 @@ private class LegacyWebSearchGalleryItemView: TGModernGalleryImageItemView, TGMo } } -private func galleryItems(account: Account, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext, editingContext: TGMediaEditingContext) -> ([TGModernGalleryItem], TGModernGalleryItem?) { +func legacyWebSearchItem(account: Account, result: ChatContextResult) -> LegacyWebSearchItem? { + var thumbnailDimensions: CGSize? + var thumbnailResource: TelegramMediaResource? + var imageResource: TelegramMediaResource? + var imageDimensions = CGSize() + + let thumbnailSignal: Signal + let originalSignal: Signal + + switch result { + case let .externalReference(_, _, _, _, _, _, content, thumbnail, _): + if let content = content { + imageResource = content.resource + } + if let thumbnail = thumbnail { + thumbnailResource = thumbnail.resource + thumbnailDimensions = thumbnail.dimensions + } + if let dimensions = content?.dimensions { + imageDimensions = dimensions + } + case let .internalReference(_, _, _, _, _, image, _, _): + if let image = image { + if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: CGSize(width: 1000.0, height: 800.0)) { + imageDimensions = imageRepresentation.dimensions + imageResource = imageRepresentation.resource + } + if let thumbnailRepresentation = imageRepresentationLargerThan(image.representations, size: CGSize(width: 200.0, height: 100.0)) { + thumbnailDimensions = thumbnailRepresentation.dimensions + thumbnailResource = thumbnailRepresentation.resource + } + } + } + + if let imageResource = imageResource { + let progressSignal = account.postbox.mediaBox.resourceStatus(imageResource) + |> map { status -> Float in + switch status { + case .Local: + return 1.0 + case .Remote: + return 0.0 + case let .Fetching(_, progress): + return progress + } + } + + var representations: [TelegramMediaImageRepresentation] = [] + if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { + representations.append(TelegramMediaImageRepresentation(dimensions: thumbnailDimensions, resource: thumbnailResource)) + } + representations.append(TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: imageResource)) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, reference: nil, partialReference: nil) + thumbnailSignal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: .standalone(media: tmpImage), autoFetchFullSize: false) + |> mapToSignal { (thumbnailData, _, _) -> Signal in + if let data = thumbnailData, let image = UIImage(data: data) { + return .single(image) + } else { + return .complete() + } + } + originalSignal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: .standalone(media: tmpImage), autoFetchFullSize: true) + |> mapToSignal { (thumbnailData, fullSizeData, fullSizeComplete) -> Signal in + if fullSizeComplete, let data = fullSizeData, let image = UIImage(data: data) { + return .single(image) + } else if let data = thumbnailData, let image = UIImage(data: data) { + image.setDegraded(true) + return .single(image) + } else { + return .complete() + } + } + + return LegacyWebSearchItem(result: result, thumbnailResource: thumbnailResource, imageResource: imageResource, dimensions: imageDimensions, thumbnailImage: thumbnailSignal, originalImage: originalSignal, progress: progressSignal) + } else { + return nil + } +} + +private func galleryItems(account: Account, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext) -> ([TGModernGalleryItem], TGModernGalleryItem?) { var focusItem: TGModernGalleryItem? var galleryItems: [TGModernGalleryItem] = [] for result in results { - var thumbnailDimensions: CGSize? - var thumbnailResource: TelegramMediaResource? - var imageResource: TelegramMediaResource? - var imageDimensions = CGSize() - - let thumbnailSignal: Signal - let originalSignal: Signal - switch result { - case let .externalReference(_, _, _, _, _, _, content, thumbnail, _): - if let content = content { - imageResource = content.resource - } - if let thumbnail = thumbnail { - thumbnailResource = thumbnail.resource - thumbnailDimensions = thumbnail.dimensions - } - if let dimensions = content?.dimensions { - imageDimensions = dimensions - } -// if let imageResource = imageResource { -// updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(imageResource) -// } - case let .internalReference(_, _, _, _, _, image, _, _): - if let image = image { - if let largestRepresentation = largestImageRepresentation(image.representations) { - imageDimensions = largestRepresentation.dimensions - imageResource = imageRepresentationLargerThan(image.representations, size: CGSize(width: 1000.0, height: 800.0))?.resource - } - if let thumbnailRepresentation = imageRepresentationLargerThan(image.representations, size: CGSize(width: 200.0, height: 100.0)) { - thumbnailDimensions = thumbnailRepresentation.dimensions - thumbnailResource = thumbnailRepresentation.resource - } - } - } - - if let imageResource = imageResource { - var representations: [TelegramMediaImageRepresentation] = [] - if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: thumbnailDimensions, resource: thumbnailResource)) - } - representations.append(TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: imageResource)) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, reference: nil, partialReference: nil) - thumbnailSignal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: .standalone(media: tmpImage), autoFetchFullSize: true) - |> mapToSignal { (thumbnailData, fullSizeData, fullSizeComplete) -> Signal in - if let data = fullSizeData, let image = UIImage(data: data) { - return .single(image) - } else { - return .complete() - } - } - originalSignal = thumbnailSignal - - let item = LegacyWebSearchItem(result: result, dimensions: imageDimensions, thumbnailImage: thumbnailSignal, originalImage: originalSignal) + if let item = legacyWebSearchItem(account: account, result: result) { let galleryItem = LegacyWebSearchGalleryItem(item: item) galleryItem.selectionContext = selectionContext galleryItem.editingContext = editingContext @@ -213,11 +293,12 @@ private func galleryItems(account: Account, results: [ChatContextResult], curren return (galleryItems, focusItem) } -func presentLegacyWebSearchGallery(account: Account, peer: Peer?, theme: PresentationTheme, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext, editingContext: TGMediaEditingContext, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (ChatContextResult) -> Void, present: (ViewController, Any?) -> Void) { +func presentLegacyWebSearchGallery(account: Account, peer: Peer?, theme: PresentationTheme, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (ChatContextResult) -> Void, present: (ViewController, Any?) -> Void) { let legacyController = LegacyController(presentation: .custom, theme: theme, initialLayout: initialLayout) legacyController.statusBar.statusBarStyle = .Ignore let controller = TGModernGalleryController(context: legacyController.context)! + controller.asyncTransitionIn = true legacyController.bind(controller: controller) let (items, focusItem) = galleryItems(account: account, results: results, current: current, selectionContext: selectionContext, editingContext: editingContext) @@ -228,9 +309,6 @@ func presentLegacyWebSearchGallery(account: Account, peer: Peer?, theme: Present } controller.model = model model.controller = controller - model.externalSelectionCount = { - return 0 - } model.useGalleryImageAsEditableItemImage = true model.storeOriginalImageForItem = { item, image in editingContext.setOriginalImage(image, for: item, synchronous: false) @@ -240,17 +318,22 @@ func presentLegacyWebSearchGallery(account: Account, peer: Peer?, theme: Present editingContext.setAdjustments(adjustments, for: item) } editingContext.setTemporaryRep(representation, for: item) - //if let selectionContext = selectionContext, { - // selectionContex - //} + if let selectionContext = selectionContext, adjustments != nil, let item = item as? TGMediaSelectableItem { + selectionContext.setItem(item, selected: true) + } } model.didFinishEditingItem = { item, adjustments, result, thumbnail in editingContext.setImage(result, thumbnailImage: thumbnail, for: item, synchronous: true) } model.saveItemCaption = { item, caption, entities in editingContext.setCaption(caption, entities: entities, for: item) + if let selectionContext = selectionContext, let caption = caption, caption.count > 0, let item = item as? TGMediaSelectableItem { + selectionContext.setItem(item, selected: true) + } + } + if let selectionContext = selectionContext { + model.interfaceView.updateSelectionInterface(selectionContext.count(), counterVisible: selectionContext.count() > 0, animated: false) } - //[model.interfaceView updateSelectionInterface:[self totalSelectionCount] counterVisible:([self totalSelectionCount] > 0) animated:false]; model.interfaceView.donePressed = { item in if let item = item as? LegacyWebSearchGalleryItem { controller.dismissWhenReady(animated: false) @@ -284,46 +367,41 @@ func presentLegacyWebSearchGallery(account: Account, peer: Peer?, theme: Present legacyController?.dismiss() } present(legacyController, nil) - - - - -// if (item.selectionContext != nil && adjustments != nil && [editableItem conformsToProtocol:@protocol(TGMediaSelectableItem)]) -// [item.selectionContext setItem:(id)editableItem selected:true]; -// }; - - -// model.interfaceView.donePressed = ^(id item) -// { -// __strong TGWebSearchController *strongSelf = weakSelf; -// if (strongSelf == nil) -// return; -// -// NSMutableArray *selectedItems = [strongSelf selectedItems]; -// -// if (selectedItems.count == 0) -// [selectedItems addObject:[item webSearchResult]]; -// -// strongSelf->_selectedItems = selectedItems; -// [strongSelf complete]; -// }; -// _galleryModel = model; -// modernGallery.model = model; -// -// __weak TGModernGalleryController *weakGallery = modernGallery; -// modernGallery.itemFocused = ^(id item) -// { -// __strong TGWebSearchController *strongSelf = weakSelf; -// __strong TGModernGalleryController *strongGallery = weakGallery; -// if (strongSelf != nil) -// { -// if (strongGallery.previewMode) -// return; -// -// id listItem = [strongSelf listItemForSearchResult:[item webSearchResult]]; -// strongSelf->_hiddenItem = listItem; -// [strongSelf updateHiddenItemAnimated:false]; -// } -// }; -// +} + +func legacyEnqueueWebSearchMessages(_ selectionState: TGMediaSelectionContext, _ editingState: TGMediaEditingContext, enqueueChatContextResult: (ChatContextResult) -> Void, enqueueMediaMessages: ([Any]) -> Void) +{ + var results: [ChatContextResult] = [] + for item in selectionState.selectedItems() { + if let item = item as? LegacyWebSearchItem { + results.append(item.result) + } + } + + if !results.isEmpty { + var signals: [Any] = [] + for result in results { + let editableItem = LegacyWebSearchItem(result: result) + if editingState.adjustments(for: editableItem) != nil { + if let imageSignal = editingState.imageSignal(for: editableItem) { + let signal = imageSignal.map { image -> Any in + if let image = image as? UIImage { + let dict: [AnyHashable: Any] = [ + "type": "editedPhoto", + "image": image + ] + return legacyAssetPickerItemGenerator()(dict, nil, nil, nil) as Any + } else { + return SSignal.complete() + } + } + signals.append(signal as Any) + } + } else { + enqueueChatContextResult(result) + } + } + + enqueueMediaMessages(signals) + } } diff --git a/TelegramUI/MessageContentKind.swift b/TelegramUI/MessageContentKind.swift index a079dc69b1..6d52b069c1 100644 --- a/TelegramUI/MessageContentKind.swift +++ b/TelegramUI/MessageContentKind.swift @@ -71,77 +71,84 @@ public enum MessageContentKind: Equatable { public func messageContentKind(_ message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> MessageContentKind { for media in message.media { - switch media { - case let expiredMedia as TelegramMediaExpiredContent: - switch expiredMedia.data { - case .image: - return .expiredImage - case .file: - return .expiredVideo - } - case _ as TelegramMediaImage: - return .image - case let file as TelegramMediaFile: - var fileName: String = "" - for attribute in file.attributes { - switch attribute { - case let .Sticker(text, _, _): - return .sticker(text) - case let .FileName(name): - fileName = name - case let .Audio(isVoice, _, title, performer, _): - if isVoice { - return .audioMessage - } else { - if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { - return .file(title + " — " + performer) - } else if let title = title, !title.isEmpty { - return .file(title) - } else if let performer = performer, !performer.isEmpty { - return .file(performer) - } - } - case let .Video(_, _, flags): - if file.isAnimated { - return .animation - } else { - if flags.contains(.instantRoundVideo) { - return .videoMessage - } else { - return .video - } - } - default: - break - } - } - return .file(fileName) - case _ as TelegramMediaContact: - return .contact - case let game as TelegramMediaGame: - return .game(game.title) - case let location as TelegramMediaMap: - if location.liveBroadcastingTimeout != nil { - return .liveLocation - } else { - return .location - } - case _ as TelegramMediaAction: - return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId) ?? "") - case let poll as TelegramMediaPoll: - return .text(poll.text) - default: - break + if let kind = mediaContentKind(media, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId) { + return kind } } return .text(message.text) } - -func descriptionStringForMessage(_ message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> (String, Bool) { - if !message.text.isEmpty { - return (message.text, false) + +public func mediaContentKind(_ media: Media, message: Message? = nil, strings: PresentationStrings? = nil, nameDisplayOrder: PresentationPersonNameOrder? = nil, accountPeerId: PeerId? = nil) -> MessageContentKind? { + switch media { + case let expiredMedia as TelegramMediaExpiredContent: + switch expiredMedia.data { + case .image: + return .expiredImage + case .file: + return .expiredVideo + } + case _ as TelegramMediaImage: + return .image + case let file as TelegramMediaFile: + var fileName: String = "" + for attribute in file.attributes { + switch attribute { + case let .Sticker(text, _, _): + return .sticker(text) + case let .FileName(name): + fileName = name + case let .Audio(isVoice, _, title, performer, _): + if isVoice { + return .audioMessage + } else { + if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { + return .file(title + " — " + performer) + } else if let title = title, !title.isEmpty { + return .file(title) + } else if let performer = performer, !performer.isEmpty { + return .file(performer) + } + } + case let .Video(_, _, flags): + if file.isAnimated { + return .animation + } else { + if flags.contains(.instantRoundVideo) { + return .videoMessage + } else { + return .video + } + } + default: + break + } + } + return .file(fileName) + case _ as TelegramMediaContact: + return .contact + case let game as TelegramMediaGame: + return .game(game.title) + case let location as TelegramMediaMap: + if location.liveBroadcastingTimeout != nil { + return .liveLocation + } else { + return .location + } + case _ as TelegramMediaAction: + if let message = message, let strings = strings, let nameDisplayOrder = nameDisplayOrder, let accountPeerId = accountPeerId { + return .text(plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId) ?? "") + } else { + return nil + } + case let poll as TelegramMediaPoll: + return .text(poll.text) + default: + return nil } - switch messageContentKind(message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId) { +} + +func stringForMediaKind(_ kind: MessageContentKind, strings: PresentationStrings) -> (String, Bool) { + switch kind { case let .text(text): return (text, false) case .image: @@ -180,3 +187,10 @@ func descriptionStringForMessage(_ message: Message, strings: PresentationString return (strings.Message_VideoExpired, true) } } + +func descriptionStringForMessage(_ message: Message, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId) -> (String, Bool) { + if !message.text.isEmpty { + return (message.text, false) + } + return stringForMediaKind(messageContentKind(message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId), strings: strings) +} diff --git a/TelegramUI/NotificationExceptions.swift b/TelegramUI/NotificationExceptions.swift index f318551f59..15daf6c967 100644 --- a/TelegramUI/NotificationExceptions.swift +++ b/TelegramUI/NotificationExceptions.swift @@ -32,7 +32,6 @@ public class NotificationExceptionsController: ViewController { super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) - self.editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed)) self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) @@ -72,7 +71,7 @@ public class NotificationExceptionsController: ViewController { private func updateThemeAndStrings() { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) - self.title = self.presentationData.strings.Settings_AppLanguage + self.title = self.presentationData.strings.Notifications_ExceptionsTitle self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.controllerNode.updatePresentationData(self.presentationData) diff --git a/TelegramUI/PeerAvatar.swift b/TelegramUI/PeerAvatar.swift index ce501fb63f..4dd947442a 100644 --- a/TelegramUI/PeerAvatar.swift +++ b/TelegramUI/PeerAvatar.swift @@ -67,6 +67,9 @@ func peerAvatarImage(account: Account, peer: Peer, authorOfMessage: MessageRefer return imageData |> mapToSignal { data -> Signal in let generate = deferred { () -> Signal in + if emptyColor == nil && data == nil { + return .single(nil) + } return .single(generateImage(displayDimensions, contextGenerator: { size, context -> Void in if let data = data { if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { diff --git a/TelegramUI/PeerSelectionController.swift b/TelegramUI/PeerSelectionController.swift index 0b7e79df4c..2a3e0776a7 100644 --- a/TelegramUI/PeerSelectionController.swift +++ b/TelegramUI/PeerSelectionController.swift @@ -60,6 +60,20 @@ public final class PeerSelectionController: ViewController { strongSelf.peerSelectionNode.scrollToTop() } } + + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() + } + } + }) } required public init(coder aDecoder: NSCoder) { @@ -68,6 +82,14 @@ public final class PeerSelectionController: ViewController { deinit { self.openMessageFromSearchDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) + self.title = self.presentationData.strings.Conversation_ForwardTitle + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) } override public func loadDisplayNode() { diff --git a/TelegramUI/PeerSelectionControllerNode.swift b/TelegramUI/PeerSelectionControllerNode.swift index ba8b71fdbf..4bf9460718 100644 --- a/TelegramUI/PeerSelectionControllerNode.swift +++ b/TelegramUI/PeerSelectionControllerNode.swift @@ -120,6 +120,10 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.searchDisplayController?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) self.chatListNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations) + + self.toolbarBackgroundNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + self.toolbarSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor + self.segmentedControl?.tintColor = self.presentationData.theme.rootController.navigationBar.accentTextColor } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -140,10 +144,6 @@ final class PeerSelectionControllerNode: ASDisplayNode { transition.updateFrame(view: segmentedControl, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - controlSize.width) / 2.0), y: layout.size.height - toolbarHeight + floor((44.0 - controlSize.height) / 2.0)), size: controlSize)) } - - - - var insets = layout.insets(options: [.input]) insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) insets.bottom = max(insets.bottom, cleanInsets.bottom) diff --git a/TelegramUI/PermissionInfoItem.swift b/TelegramUI/PermissionInfoItem.swift index e058354e60..200748125c 100644 --- a/TelegramUI/PermissionInfoItem.swift +++ b/TelegramUI/PermissionInfoItem.swift @@ -152,7 +152,7 @@ class PermissionInfoItemNode: ListViewItemNode { self.addSubnode(self.labelNode) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) - self.addSubnode(self.closeButton) + //self.addSubnode(self.closeButton) } func asyncLayout() -> (_ item: PermissionInfoItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors?) -> (ListViewItemNodeLayout, () -> Void) { diff --git a/TelegramUI/SearchBarNode.swift b/TelegramUI/SearchBarNode.swift index 4e6a46194d..636b6f4c3f 100644 --- a/TelegramUI/SearchBarNode.swift +++ b/TelegramUI/SearchBarNode.swift @@ -443,13 +443,13 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { func transitionOut(to node: SearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { let targetTextBackgroundFrame = node.convert(node.backgroundNode.frame, to: self) - let duration: Double = 0.5 + let duration: Double = transition.isAnimated ? 0.5 : 0.0 let timingFunction = kCAMediaTimingFunctionSpring node.isHidden = true self.clearButton.isHidden = true self.textField.text = "" - + var backgroundCompleted = false var separatorCompleted = false var textBackgroundCompleted = false diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index 25c56d716a..33020efc94 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -616,9 +616,9 @@ public func settingsController(account: Account, accountManager: AccountManager) }) changeProfilePhotoImpl = { - let _ = (account.postbox.transaction { transaction -> Peer? in - return transaction.getPeer(account.peerId) - } |> deliverOnMainQueue).start(next: { peer in + let _ = (account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in + return (transaction.getPeer(account.peerId), currentSearchBotsConfiguration(transaction: transaction)) + } |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) @@ -638,28 +638,39 @@ public func settingsController(account: Account, accountManager: AccountManager) hasPhotos = true } - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasDeleteButton: hasPhotos, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false)! + let completedImpl: (UIImage) -> Void = { image in + if let data = UIImageJPEGRepresentation(image, 0.6) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) + updateState { + $0.withUpdatedUpdatingAvatar(.image(representation, true)) + } + updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: resource) |> deliverOnMainQueue).start(next: { result in + switch result { + case .complete: + updateState { + $0.withUpdatedUpdatingAvatar(nil) + } + case .progress: + break + } + })) + } + } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos, hasViewButton: false, personalPhoto: true, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)! let _ = currentAvatarMixin.swap(mixin) + mixin.requestSearchController = { assetsController in + let controller = WebSearchController(account: account, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(completion: { result in + assetsController?.dismiss() + completedImpl(result) + })) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } mixin.didFinishWithImage = { image in if let image = image { - if let data = UIImageJPEGRepresentation(image, 0.6) { - let resource = LocalFileMediaResource(fileId: arc4random64()) - account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource) - updateState { - $0.withUpdatedUpdatingAvatar(.image(representation, true)) - } - updateAvatarDisposable.set((updateAccountPhoto(account: account, resource: resource) |> deliverOnMainQueue).start(next: { result in - switch result { - case .complete: - updateState { - $0.withUpdatedUpdatingAvatar(nil) - } - case .progress: - break - } - })) - } + completedImpl(image) } } mixin.didFinishWithDelete = { diff --git a/TelegramUI/SetupTwoStepVerificationController.swift b/TelegramUI/SetupTwoStepVerificationController.swift index ea670c810f..c39a7f011c 100644 --- a/TelegramUI/SetupTwoStepVerificationController.swift +++ b/TelegramUI/SetupTwoStepVerificationController.swift @@ -93,7 +93,6 @@ class SetupTwoStepVerificationController: ViewController { private func updateThemeAndStrings() { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) - self.title = self.presentationData.strings.Settings_AppLanguage self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.controllerNode.updatePresentationData(self.presentationData) } diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index 27dc25ad59..fbfeb7005a 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -25,6 +25,7 @@ private final class UserInfoControllerArguments { let displayCopyContextMenu: (UserInfoEntryTag, String) -> Void let call: () -> Void let openCallMenu: (String) -> Void + let aboutLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void let displayAboutContextMenu: (String) -> Void let openEncryptionKey: (SecretChatKeyFingerprint) -> Void let addBotToGroup: () -> Void @@ -34,7 +35,7 @@ private final class UserInfoControllerArguments { let botPrivacy: () -> Void let report: () -> Void - init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, addContact: @escaping () -> Void, shareContact: @escaping () -> Void, shareMyContact: @escaping () -> Void, startSecretChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, changeNotificationSoundSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayCopyContextMenu: @escaping (UserInfoEntryTag, String) -> Void, call: @escaping () -> Void, openCallMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, openEncryptionKey: @escaping (SecretChatKeyFingerprint) -> Void, addBotToGroup: @escaping () -> Void, shareBot: @escaping () -> Void, botSettings: @escaping () -> Void, botHelp: @escaping () -> Void, botPrivacy: @escaping () -> Void, report: @escaping () -> Void) { + init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, addContact: @escaping () -> Void, shareContact: @escaping () -> Void, shareMyContact: @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, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, displayAboutContextMenu: @escaping (String) -> Void, openEncryptionKey: @escaping (SecretChatKeyFingerprint) -> Void, addBotToGroup: @escaping () -> Void, shareBot: @escaping () -> Void, botSettings: @escaping () -> Void, botHelp: @escaping () -> Void, botPrivacy: @escaping () -> Void, report: @escaping () -> Void) { self.account = account self.avatarAndNameInfoContext = avatarAndNameInfoContext self.updateEditingName = updateEditingName @@ -55,6 +56,7 @@ private final class UserInfoControllerArguments { self.displayCopyContextMenu = displayCopyContextMenu self.call = call self.openCallMenu = openCallMenu + self.aboutLinkAction = aboutLinkAction self.displayAboutContextMenu = displayAboutContextMenu self.openEncryptionKey = openEncryptionKey self.addBotToGroup = addBotToGroup @@ -93,7 +95,7 @@ private func areMessagesEqual(_ lhsMessage: Message, _ rhsMessage: Message) -> B private enum UserInfoEntry: ItemListNodeEntry { case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, displayCall: Bool) case calls(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, messages: [Message]) - case about(PresentationTheme, String, String) + case about(PresentationTheme, Peer, String, String) case phoneNumber(PresentationTheme, Int, String, String, Bool) case userName(PresentationTheme, String, String) case sendMessage(PresentationTheme, String) @@ -192,8 +194,8 @@ private enum UserInfoEntry: ItemListNodeEntry { } else { return false } - case let .about(lhsTheme, lhsText, lhsValue): - if case let .about(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + case let .about(lhsTheme, lhsPeer, lhsText, lhsValue): + if case let .about(rhsTheme, rhsPeer, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsPeer.isEqual(rhsPeer), lhsText == rhsText, lhsValue == rhsValue { return true } else { return false @@ -380,9 +382,15 @@ private enum UserInfoEntry: ItemListNodeEntry { } : nil) case let .calls(theme, strings, dateTimeFormat, messages): return ItemListCallListItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, messages: messages, sectionId: self.section, style: .plain) - case let .about(theme, text, value): - return ItemListTextWithLabelItem(theme: theme, label: text, text: value, enabledEntitiyTypes: [], multiline: true, sectionId: self.section, action: { + case let .about(theme, peer, text, value): + var enabledEntitiyTypes: EnabledEntityTypes = [] + if let peer = peer as? TelegramUser, let _ = peer.botInfo { + enabledEntitiyTypes = [.url, .mention, .hashtag] + } + return ItemListTextWithLabelItem(theme: theme, label: text, text: value, enabledEntitiyTypes: enabledEntitiyTypes, multiline: true, sectionId: self.section, action: nil, longTapAction: { arguments.displayAboutContextMenu(value) + }, linkItemAction: { action, itemLink in + arguments.aboutLinkAction(action, itemLink) }, tag: UserInfoEntryTag.about) case let .phoneNumber(theme, _, label, value, isMain): return ItemListTextWithLabelItem(theme: theme, label: label, text: value, textColor: isMain ? .highlighted : .accent, enabledEntitiyTypes: [], multiline: false, sectionId: self.section, action: { @@ -618,7 +626,7 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat } else { title = presentationData.strings.Profile_About } - entries.append(UserInfoEntry.about(presentationData.theme, title, about)) + entries.append(UserInfoEntry.about(presentationData.theme, peer, title, about)) } if !isEditing { @@ -764,10 +772,14 @@ public func userInfoController(account: Account, peerId: PeerId, mode: UserInfoC let createSecretChatDisposable = MetaDisposable() actionsDisposable.add(createSecretChatDisposable) + let navigateDisposable = MetaDisposable() + actionsDisposable.add(navigateDisposable) + var avatarGalleryTransitionArguments: ((AvatarGalleryEntry) -> GalleryTransitionArguments?)? let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext() var updateHiddenAvatarImpl: (() -> Void)? + var aboutLinkActionImpl: ((TextLinkItemActionType, TextLinkItem) -> Void)? var displayAboutContextMenuImpl: ((String) -> Void)? var displayCopyContextMenuImpl: ((UserInfoEntryTag, String) -> Void)? @@ -915,7 +927,7 @@ public func userInfoController(account: Account, peerId: PeerId, mode: UserInfoC } let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - if let user = peer as? TelegramUser, user.botInfo != nil { + if let peer = peer as? TelegramUser, let _ = peer.botInfo { updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peer.id, isBlocked: value).start()) if !value { let _ = enqueueMessages(account: account, peerId: peer.id, messages: [.message(text: "/start", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() @@ -989,6 +1001,8 @@ public func userInfoController(account: Account, peerId: PeerId, mode: UserInfoC account.telegramApplicationContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(number).replacingOccurrences(of: " ", with: ""))") } }) + }, aboutLinkAction: { action, itemLink in + aboutLinkActionImpl?(action, itemLink) }, displayAboutContextMenu: { text in displayAboutContextMenuImpl?(text) }, openEncryptionKey: { fingerprint in @@ -1311,6 +1325,11 @@ public func userInfoController(account: Account, peerId: PeerId, mode: UserInfoC } } } + aboutLinkActionImpl = { [weak controller] action, itemLink in + if let controller = controller { + handlePeerInfoAboutTextAction(account: account, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink) + } + } displayAboutContextMenuImpl = { [weak controller] text in if let strongController = controller { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } diff --git a/TelegramUI/WebSearchBadgeNode.swift b/TelegramUI/WebSearchBadgeNode.swift new file mode 100644 index 0000000000..9adcd8072f --- /dev/null +++ b/TelegramUI/WebSearchBadgeNode.swift @@ -0,0 +1,94 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class WebSearchBadgeNode: ASDisplayNode { + private var fillColor: UIColor + private var strokeColor: UIColor + private var textColor: UIColor + + private let textNode: ASTextNode + private let backgroundNode: ASImageNode + + private let font: UIFont = UIFont(name: ".SFCompactRounded-Semibold", size: 17.0)! + + var text: String = "" { + didSet { + self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) + self.invalidateCalculatedLayout() + } + } + + convenience init(theme: PresentationTheme) { + self.init(fillColor: theme.list.itemCheckColors.fillColor, strokeColor: theme.list.itemCheckColors.fillColor, textColor: theme.list.itemCheckColors.foregroundColor) + } + + init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { + self.fillColor = fillColor + self.strokeColor = strokeColor + self.textColor = textColor + + self.textNode = ASTextNode() + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 22.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textNode) + } + + func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { + self.fillColor = fillColor + self.strokeColor = strokeColor + self.textColor = textColor + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 22.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) + self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) + } + + func animateBump(incremented: Bool) { + if incremented { + let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) + firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.2) + firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.2, completion: { finished in + if finished { + let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) + secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0) + secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0) + } + }) + } else { + let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) + firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 0.8) + firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 0.8, completion: { finished in + if finished { + let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) + secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0) + secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0) + } + }) + } + } + + func animateOut() { + let timingFunction = kCAMediaTimingFunctionEaseInEaseOut + self.backgroundNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil) + self.textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil) + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + let badgeSize = self.textNode.measure(constrainedSize) + let backgroundSize = CGSize(width: max(22.0, badgeSize.width + 12.0), height: 22.0) + let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize) + self.backgroundNode.frame = backgroundFrame + self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: floorToScreenPixels((backgroundFrame.size.height - badgeSize.height) / 2.0) - UIScreenPixel), size: badgeSize) + + return backgroundSize + } +} diff --git a/TelegramUI/WebSearchController.swift b/TelegramUI/WebSearchController.swift index 866c321a9f..cc7cee135c 100644 --- a/TelegramUI/WebSearchController.swift +++ b/TelegramUI/WebSearchController.swift @@ -6,43 +6,91 @@ import AsyncDisplayKit import TelegramCore import LegacyComponents -private func requestContextResults(account: Account, botId: PeerId, query: String, peerId: PeerId, offset: String = "", existingResults: ChatContextResultCollection? = nil, limit: Int = 30) -> Signal { +private func requestContextResults(account: Account, botId: PeerId, query: String, peerId: PeerId, offset: String = "", existingResults: ChatContextResultCollection? = nil, limit: Int = 60) -> Signal { return requestChatContextResults(account: account, botId: botId, peerId: peerId, query: query, offset: offset) |> mapToSignal { results -> Signal in var collection = existingResults if let existingResults = existingResults, let results = results { var newResults: [ChatContextResult] = [] - newResults.append(contentsOf: existingResults.results) - newResults.append(contentsOf: results.results) + var existingIds = Set() + for result in existingResults.results { + newResults.append(result) + existingIds.insert(result.id) + } + for result in results.results { + if !existingIds.contains(result.id) { + newResults.append(result) + existingIds.insert(result.id) + } + } collection = ChatContextResultCollection(botId: existingResults.botId, peerId: existingResults.peerId, query: existingResults.query, geoPoint: existingResults.geoPoint, queryId: results.queryId, nextOffset: results.nextOffset, presentation: existingResults.presentation, switchPeer: existingResults.switchPeer, results: newResults, cacheTimeout: existingResults.cacheTimeout) } else { collection = results } if let collection = collection, collection.results.count < limit, let nextOffset = collection.nextOffset { - return requestContextResults(account: account, botId: botId, query: query, peerId: peerId, offset: nextOffset, existingResults: collection) + let nextResults = requestContextResults(account: account, botId: botId, query: query, peerId: peerId, offset: nextOffset, existingResults: collection) + if collection.results.count > 10 { + return .single(collection) + |> then(nextResults) + } else { + return nextResults + } } else { return .single(collection) } } } +enum WebSearchMode { + case media + case avatar +} + +enum WebSearchControllerMode { + case media(completion: (TGMediaSelectionContext, TGMediaEditingContext) -> Void) + case avatar(completion: (UIImage) -> Void) + + var mode: WebSearchMode { + switch self { + case .media: + return .media + case .avatar: + return .avatar + } + } +} + final class WebSearchControllerInteraction { let openResult: (ChatContextResult) -> Void let setSearchQuery: (String) -> Void let deleteRecentQuery: (String) -> Void - let toggleSelection: ([String], Bool) -> Void - let sendSelected: (ChatContextResultCollection, ChatContextResult?) -> Void - var selectionState: WebSearchSelectionState? + let toggleSelection: (ChatContextResult, Bool) -> Void + let sendSelected: (ChatContextResult?) -> Void + let avatarCompleted: (UIImage) -> Void + let selectionState: TGMediaSelectionContext? + let editingState: TGMediaEditingContext var hiddenMediaId: String? - let editingContext: TGMediaEditingContext - init(openResult: @escaping (ChatContextResult) -> Void, setSearchQuery: @escaping (String) -> Void, deleteRecentQuery: @escaping (String) -> Void, toggleSelection: @escaping ([String], Bool) -> Void, sendSelected: @escaping (ChatContextResultCollection, ChatContextResult?) -> Void, editingContext: TGMediaEditingContext) { + init(openResult: @escaping (ChatContextResult) -> Void, setSearchQuery: @escaping (String) -> Void, deleteRecentQuery: @escaping (String) -> Void, toggleSelection: @escaping (ChatContextResult, Bool) -> Void, sendSelected: @escaping (ChatContextResult?) -> Void, avatarCompleted: @escaping (UIImage) -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { self.openResult = openResult self.setSearchQuery = setSearchQuery self.deleteRecentQuery = deleteRecentQuery self.toggleSelection = toggleSelection self.sendSelected = sendSelected - self.editingContext = editingContext + self.avatarCompleted = avatarCompleted + self.selectionState = selectionState + self.editingState = editingState + } +} + +private func selectionChangedSignal(selectionState: TGMediaSelectionContext) -> Signal { + return Signal { subscriber in + let disposable = selectionState.selectionChangedSignal()?.start(next: { next in + subscriber.putNext(Void()) + }, completed: {}) + return ActionDisposable { + disposable?.dispose() + } } } @@ -50,7 +98,8 @@ final class WebSearchController: ViewController { private var validLayout: ContainerViewLayout? private let account: Account - private let chatLocation: ChatLocation + private let mode: WebSearchControllerMode + private let peer: Peer? private let configuration: SearchBotsConfiguration private var controllerNode: WebSearchControllerNode { @@ -70,17 +119,19 @@ final class WebSearchController: ViewController { private var disposable: Disposable? private let resultsDisposable = MetaDisposable() + private var selectionDisposable: Disposable? private var navigationContentNode: WebSearchNavigationContentNode? - init(account: Account, chatLocation: ChatLocation, configuration: SearchBotsConfiguration, sendSelected: @escaping ([String], ChatContextResultCollection, TGMediaEditingContext) -> Void) { + init(account: Account, peer: Peer?, configuration: SearchBotsConfiguration, mode: WebSearchControllerMode) { self.account = account - self.chatLocation = chatLocation + self.mode = mode + self.peer = peer self.configuration = configuration let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.interfaceState = WebSearchInterfaceState(presentationData: presentationData) - + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme).withUpdatedSeparatorColor(presentationData.theme.rootController.navigationBar.backgroundColor), strings: NavigationBarStrings(presentationStrings: presentationData.strings))) self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBar.style.style @@ -101,8 +152,8 @@ final class WebSearchController: ViewController { } strongSelf.updateInterfaceState { current -> WebSearchInterfaceState in var updated = current - if current.state?.mode != settings.mode { - updated = updated.withUpdatedMode(settings.mode) + if case .media = mode, current.state?.scope != settings.scope { + updated = updated.withUpdatedScope(settings.scope) } if current.presentationData !== presentationData { updated = updated.withUpdatedPresentationData(presentationData) @@ -121,12 +172,19 @@ final class WebSearchController: ViewController { } self.navigationBar?.setContentNode(navigationContentNode, animated: false) - let editingContext = TGMediaEditingContext() + let selectionState: TGMediaSelectionContext? + switch self.mode { + case .media: + selectionState = TGMediaSelectionContext() + case .avatar: + selectionState = nil + } + let editingState = TGMediaEditingContext() self.controllerInteraction = WebSearchControllerInteraction(openResult: { [weak self] result in if let strongSelf = self { strongSelf.controllerNode.openResult(currentResult: result, present: { [weak self] viewController, arguments in if let strongSelf = self { - strongSelf.present(viewController, in: .window(.root), with: arguments) + strongSelf.present(viewController, in: .window(.root), with: arguments, blockInteraction: true) } }) } @@ -140,19 +198,35 @@ final class WebSearchController: ViewController { if let strongSelf = self { _ = removeRecentWebSearchQuery(postbox: strongSelf.account.postbox, string: query).start() } - }, toggleSelection: { [weak self] ids, value in + }, toggleSelection: { [weak self] result, value in if let strongSelf = self { - strongSelf.updateInterfaceState { $0.withToggledSelectedMessages(ids, value: value) } + let item = LegacyWebSearchItem(result: result) + strongSelf.controllerInteraction?.selectionState?.setItem(item, selected: value) } - }, sendSelected: { [weak self] collection, current in - if let strongSelf = self, let state = strongSelf.interfaceState.state { - var selectedIds = state.selectionState.selectedIds + }, sendSelected: { current in + if let selectionState = selectionState { if let current = current { - selectedIds.insert(current.id) + let currentItem = LegacyWebSearchItem(result: current) + selectionState.setItem(currentItem, selected: true) + } + if case let .media(sendSelected) = mode { + sendSelected(selectionState, editingState) } - sendSelected(Array(selectedIds), collection, editingContext) } - }, editingContext: editingContext) + }, avatarCompleted: { result in + if case let .avatar(avatarCompleted) = mode { + avatarCompleted(result) + } + }, selectionState: selectionState, editingState: editingState) + + if let selectionState = selectionState { + self.selectionDisposable = (selectionChangedSignal(selectionState: selectionState) + |> deliverOnMainQueue).start(next: { [weak self] _ in + if let strongSelf = self { + strongSelf.controllerNode.updateSelectionState(animated: true) + } + }) + } } required public init(coder aDecoder: NSCoder) { @@ -161,6 +235,8 @@ final class WebSearchController: ViewController { deinit { self.disposable?.dispose() + self.resultsDisposable.dispose() + self.selectionDisposable?.dispose() } public override func viewDidAppear(_ animated: Bool) { @@ -181,7 +257,7 @@ final class WebSearchController: ViewController { } override public func loadDisplayNode() { - self.displayNode = WebSearchControllerNode(account: self.account, theme: self.interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, controllerInteraction: self.controllerInteraction!) + self.displayNode = WebSearchControllerNode(account: self.account, theme: self.interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, controllerInteraction: self.controllerInteraction!, peer: self.peer, mode: self.mode.mode) self.controllerNode.requestUpdateInterfaceState = { [weak self] animated, f in if let strongSelf = self { strongSelf.updateInterfaceState(f) @@ -212,8 +288,6 @@ final class WebSearchController: ViewController { self.interfaceState = updatedInterfaceState self.interfaceStatePromise.set(updatedInterfaceState) - self.controllerInteraction?.selectionState = updatedInterfaceState.state?.selectionState - if self.isNodeLoaded { if previousTheme !== updatedInterfaceState.presentationData.theme || previousStrings !== updatedInterfaceState.presentationData.strings { self.controllerNode.updatePresentationData(theme: updatedInterfaceState.presentationData.theme, strings: updatedInterfaceState.presentationData.strings) @@ -229,29 +303,26 @@ final class WebSearchController: ViewController { let _ = addRecentWebSearchQuery(postbox: self.account.postbox, string: query).start() } - let mode = self.interfaceStatePromise.get() - |> map { state -> WebSearchMode? in - return state.state?.mode + let scope: Signal + switch self.mode { + case .media: + scope = self.interfaceStatePromise.get() + |> map { state -> WebSearchScope? in + return state.state?.scope + } + |> distinctUntilChanged + case .avatar: + scope = .single(.images) } - |> distinctUntilChanged self.updateInterfaceState { $0.withUpdatedQuery(query) } - var results = mode - |> mapToSignal { mode -> (Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>) in - if let mode = mode { - return self.signalForQuery(query, mode: mode) - |> deliverOnMainQueue - |> beforeStarted { [weak self] in - if let strongSelf = self { - strongSelf.navigationContentNode?.setActivity(true) - } - } - |> afterCompleted { [weak self] in - if let strongSelf = self { - strongSelf.navigationContentNode?.setActivity(false) - } - } + let scopes: [WebSearchScope: Promise<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?>] = [.images: Promise(initializeOnFirstAccess: self.signalForQuery(query, scope: .images)), .gifs: Promise(initializeOnFirstAccess: self.signalForQuery(query, scope: .gifs))] + + var results = scope + |> mapToSignal { scope -> (Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>) in + if let scope = scope, let scopeResults = scopes[scope] { + return scopeResults.get() } else { return .complete() } @@ -276,29 +347,16 @@ final class WebSearchController: ViewController { })) } - private func signalForQuery(_ query: String, mode: WebSearchMode) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> { + private func signalForQuery(_ query: String, scope: WebSearchScope) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> { var delayRequest = true - var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() -// if let previousQuery = previousQuery { -// switch previousQuery { -// case let .contextRequest(currentAddressName, currentContextQuery) where currentAddressName == addressName: -// if query.isEmpty && !currentContextQuery.isEmpty { -// delayRequest = false -// } -// default: -// delayRequest = false -// signal = .single({ _ in return .contextRequestResult(nil, nil) }) -// } -// } else { - signal = .single({ _ in return .contextRequestResult(nil, nil) }) -// } + var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ _ in return .contextRequestResult(nil, nil) }) - guard case let .peer(peerId) = self.chatLocation else { + guard let peerId = self.peer?.id else { return .single({ _ in return .contextRequestResult(nil, nil) }) } let botName: String? - switch mode { + switch scope { case .images: botName = self.configuration.imageBotUsername case .gifs: @@ -354,7 +412,18 @@ final class WebSearchController: ViewController { return .single({ _ in return nil }) } } - return signal |> then(contextBot) + return (signal |> then(contextBot)) + |> deliverOnMainQueue + |> beforeStarted { [weak self] in + if let strongSelf = self { + strongSelf.navigationContentNode?.setActivity(true) + } + } + |> afterCompleted { [weak self] in + if let strongSelf = self { + strongSelf.navigationContentNode?.setActivity(false) + } + } } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { diff --git a/TelegramUI/WebSearchControllerNode.swift b/TelegramUI/WebSearchControllerNode.swift index f5a626f965..64edcf61bd 100644 --- a/TelegramUI/WebSearchControllerNode.swift +++ b/TelegramUI/WebSearchControllerNode.swift @@ -113,8 +113,10 @@ private func preparedWebSearchRecentTransition(from fromEntries: [WebSearchRecen class WebSearchControllerNode: ASDisplayNode { private let account: Account + private let peer: Peer? private var theme: PresentationTheme private var strings: PresentationStrings + private let mode: WebSearchMode private let controllerInteraction: WebSearchControllerInteraction private var webSearchInterfaceState: WebSearchInterfaceState @@ -128,6 +130,7 @@ class WebSearchControllerNode: ASDisplayNode { private let toolbarSeparatorNode: ASDisplayNode private let cancelButton: HighlightableButtonNode private let sendButton: HighlightableButtonNode + private let badgeNode: WebSearchBadgeNode private let attributionNode: ASImageNode @@ -145,8 +148,6 @@ class WebSearchControllerNode: ASDisplayNode { private var hasMore = false private var isLoadingMore = false - private let selectionContext = TGMediaSelectionContext() - private let hiddenMediaId = Promise(nil) private var hiddenMediaDisposable: Disposable? @@ -163,11 +164,13 @@ class WebSearchControllerNode: ASDisplayNode { var cancel: (() -> Void)? var dismissInput: (() -> Void)? - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: WebSearchControllerInteraction) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: WebSearchControllerInteraction, peer: Peer?, mode: WebSearchMode) { self.account = account self.theme = theme self.strings = strings self.controllerInteraction = controllerInteraction + self.peer = peer + self.mode = mode self.webSearchInterfaceState = WebSearchInterfaceState(presentationData: account.telegramApplicationContext.currentPresentationData.with { $0 }) self.webSearchInterfaceStatePromise = ValuePromise(self.webSearchInterfaceState, ignoreRepeated: true) @@ -186,6 +189,8 @@ class WebSearchControllerNode: ASDisplayNode { self.cancelButton = HighlightableButtonNode() self.sendButton = HighlightableButtonNode() + self.badgeNode = WebSearchBadgeNode(theme: theme) + self.gridNode = GridNode() self.gridNode.backgroundColor = theme.list.plainBackgroundColor @@ -204,12 +209,15 @@ class WebSearchControllerNode: ASDisplayNode { self.addSubnode(self.recentQueriesNode) self.addSubnode(self.segmentedBackgroundNode) self.addSubnode(self.segmentedSeparatorNode) - self.view.addSubview(self.segmentedControl) + if case .media = mode { + self.view.addSubview(self.segmentedControl) + } self.addSubnode(self.toolbarBackgroundNode) self.addSubnode(self.toolbarSeparatorNode) self.addSubnode(self.cancelButton) self.addSubnode(self.sendButton) self.addSubnode(self.attributionNode) + self.addSubnode(self.badgeNode) self.segmentedControl.addTarget(self, action: #selector(self.indexChanged), for: .valueChanged) self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) @@ -246,7 +254,7 @@ class WebSearchControllerNode: ASDisplayNode { self.gridNode.visibleItemsUpdated = { [weak self] visibleItems in if let strongSelf = self, let bottom = visibleItems.bottom, let entries = strongSelf.currentEntries { - if bottom.0 <= entries.count { + if bottom.0 >= entries.count - 8 { strongSelf.loadMore() } } @@ -260,6 +268,18 @@ class WebSearchControllerNode: ASDisplayNode { self?.dismissInput?() } + self.sendButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self, strongSelf.badgeNode.alpha > 0.0 { + if highlighted { + strongSelf.badgeNode.layer.removeAnimation(forKey: "opacity") + strongSelf.badgeNode.alpha = 0.4 + } else { + strongSelf.badgeNode.alpha = 1.0 + strongSelf.badgeNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.hiddenMediaDisposable = (self.hiddenMediaId.get() |> deliverOnMainQueue).start(next: { [weak self] id in if let strongSelf = self { @@ -291,7 +311,12 @@ class WebSearchControllerNode: ASDisplayNode { func applyPresentationData(themeUpdated: Bool = true) { self.cancelButton.setTitle(self.strings.Common_Cancel, with: Font.regular(17.0), with: self.theme.rootController.navigationBar.accentTextColor, for: .normal) - self.sendButton.setTitle(self.strings.MediaPicker_Send, with: Font.medium(17.0), with: self.theme.rootController.navigationBar.accentTextColor, for: .normal) + + if let selectionState = self.controllerInteraction.selectionState { + let sendEnabled = selectionState.count() > 0 + let color = sendEnabled ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.disabledButtonColor + self.sendButton.setTitle(self.strings.MediaPicker_Send, with: Font.medium(17.0), with: color, for: .normal) + } if themeUpdated { self.backgroundColor = self.theme.chatList.backgroundColor @@ -322,7 +347,7 @@ class WebSearchControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight - let segmentedHeight: CGFloat = 40.0 + let segmentedHeight: CGFloat = self.segmentedControl.superview != nil ? 40.0 : 5.0 let panelY: CGFloat = insets.top - UIScreenPixel - 4.0 transition.updateFrame(node: self.segmentedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: layout.size.width, height: segmentedHeight))) @@ -342,15 +367,54 @@ class WebSearchControllerNode: ASDisplayNode { if let image = self.attributionNode.image { transition.updateFrame(node: self.attributionNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - image.size.width) / 2.0), y: toolbarY + floor((toolbarHeight - image.size.height) / 2.0)), size: image.size)) - transition.updateAlpha(node: self.attributionNode, alpha: self.webSearchInterfaceState.state?.mode == .gifs ? 1.0 : 0.0) + transition.updateAlpha(node: self.attributionNode, alpha: self.webSearchInterfaceState.state?.scope == .gifs ? 1.0 : 0.0) } - let toolbarPadding: CGFloat = 15.0 + let toolbarPadding: CGFloat = 10.0 let cancelSize = self.cancelButton.measure(CGSize(width: layout.size.width, height: toolbarHeight)) transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: toolbarPadding + layout.safeInsets.left, y: toolbarY), size: CGSize(width: cancelSize.width, height: toolbarHeight))) let sendSize = self.sendButton.measure(CGSize(width: layout.size.width, height: toolbarHeight)) - transition.updateFrame(node: self.sendButton, frame: CGRect(origin: CGPoint(x: layout.size.width - toolbarPadding - layout.safeInsets.right - sendSize.width, y: toolbarY), size: CGSize(width: sendSize.width, height: toolbarHeight))) + let sendFrame = CGRect(origin: CGPoint(x: layout.size.width - toolbarPadding - layout.safeInsets.right - sendSize.width, y: toolbarY), size: CGSize(width: sendSize.width, height: toolbarHeight)) + transition.updateFrame(node: self.sendButton, frame: sendFrame) + + if let selectionState = self.controllerInteraction.selectionState { + self.sendButton.isHidden = false + + let previousSendEnabled = self.sendButton.isEnabled + let sendEnabled = selectionState.count() > 0 + self.sendButton.isEnabled = sendEnabled + if sendEnabled != previousSendEnabled { + let color = sendEnabled ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.disabledButtonColor + self.sendButton.setTitle(self.strings.MediaPicker_Send, with: Font.medium(17.0), with: color, for: .normal) + } + + let selectedCount = selectionState.count() + let badgeText = String(selectedCount) + if selectedCount > 0 && (self.badgeNode.text != badgeText || self.badgeNode.alpha < 1.0) { + if transition.isAnimated { + var incremented = true + if let previousCount = Int(self.badgeNode.text) { + incremented = selectedCount > previousCount || self.badgeNode.alpha < 1.0 + } + self.badgeNode.animateBump(incremented: incremented) + } + self.badgeNode.text = badgeText + + let badgeSize = self.badgeNode.measure(layout.size) + transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: CGPoint(x: sendFrame.minX - badgeSize.width - 6.0, y: toolbarY + 11.0), size: badgeSize)) + transition.updateAlpha(node: self.badgeNode, alpha: 1.0) + } else if selectedCount == 0 { + if transition.isAnimated { + self.badgeNode.animateOut() + } + let badgeSize = CGSize(width: 22.0, height: 22.0) + transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: CGPoint(x: sendFrame.minX - badgeSize.width - 6.0, y: toolbarY + 11.0), size: badgeSize)) + transition.updateAlpha(node: self.badgeNode, alpha: 0.0) + } + } else { + self.sendButton.isHidden = true + } let previousBounds = self.gridNode.bounds self.gridNode.bounds = CGRect(x: previousBounds.origin.x, y: previousBounds.origin.y, width: layout.size.width, height: layout.size.height) @@ -396,7 +460,19 @@ class WebSearchControllerNode: ASDisplayNode { self.webSearchInterfaceStatePromise.set(self.webSearchInterfaceState) if let state = interfaceState.state { - self.segmentedControl.selectedSegmentIndex = Int(state.mode.rawValue) + self.segmentedControl.selectedSegmentIndex = Int(state.scope.rawValue) + } + + if let validLayout = self.containerLayout { + self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) + } + } + + func updateSelectionState(animated: Bool) { + self.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? WebSearchItemNode { + itemNode.updateSelectionState(animated: animated) + } } if let validLayout = self.containerLayout { @@ -421,7 +497,7 @@ class WebSearchControllerNode: ASDisplayNode { } private func loadMore() { - guard !self.isLoadingMore, let currentProcessedResults = self.currentProcessedResults, let nextOffset = currentProcessedResults.nextOffset else { + guard !self.isLoadingMore, let currentProcessedResults = self.currentProcessedResults, currentProcessedResults.results.count > 55, let nextOffset = currentProcessedResults.nextOffset else { return } self.isLoadingMore = true @@ -432,11 +508,16 @@ class WebSearchControllerNode: ASDisplayNode { } strongSelf.isLoadingMore = false var results: [ChatContextResult] = [] + var existingIds = Set() for result in currentProcessedResults.results { results.append(result) + existingIds.insert(result.id) } for result in nextResults.results { - results.append(result) + if !existingIds.contains(result.id) { + results.append(result) + existingIds.insert(result.id) + } } let mergedResults = ChatContextResultCollection(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, geoPoint: currentProcessedResults.geoPoint, queryId: nextResults.queryId, nextOffset: nextResults.nextOffset, presentation: currentProcessedResults.presentation, switchPeer: currentProcessedResults.switchPeer, results: results, cacheTimeout: currentProcessedResults.cacheTimeout) strongSelf.currentProcessedResults = mergedResults @@ -527,11 +608,13 @@ class WebSearchControllerNode: ASDisplayNode { } @objc private func indexChanged() { - self.requestUpdateInterfaceState(true) { current in - if let mode = WebSearchMode(rawValue: Int32(self.segmentedControl.selectedSegmentIndex)) { - return current.withUpdatedMode(mode) + if let scope = WebSearchScope(rawValue: Int32(self.segmentedControl.selectedSegmentIndex)) { + let _ = updateWebSearchSettingsInteractively(postbox: self.account.postbox) { _ -> WebSearchSettings in + return WebSearchSettings(scope: scope) + }.start() + self.requestUpdateInterfaceState(true) { current in + return current.withUpdatedScope(scope) } - return current } } @@ -540,67 +623,85 @@ class WebSearchControllerNode: ASDisplayNode { } @objc private func sendPressed() { - if let results = self.currentProcessedResults { - self.controllerInteraction.sendSelected(results, nil) - } + self.controllerInteraction.sendSelected(nil) self.cancel?() } - func openResult(currentResult: ChatContextResult, present: (ViewController, Any?) -> Void) { - if let state = self.webSearchInterfaceState.state, state.mode == .images { - if let results = self.currentProcessedResults?.results { - presentLegacyWebSearchGallery(account: self.account, peer: nil, theme: self.theme, results: results, current: currentResult, selectionContext: self.selectionContext, editingContext: self.controllerInteraction.editingContext, updateHiddenMedia: { [weak self] id in - self?.hiddenMediaId.set(.single(id)) - }, initialLayout: self.containerLayout?.0, transitionHostView: { [weak self] in - return self?.gridNode.view - }, transitionView: { [weak self] result in - return self?.transitionView(for: result) - }, completed: { [weak self] result in - if let strongSelf = self, let results = strongSelf.currentProcessedResults { - strongSelf.controllerInteraction.sendSelected(results, nil) - strongSelf.cancel?() - } - }, present: present) - } - } else { - if let results = self.currentProcessedResults?.results { - var entries: [WebSearchGalleryEntry] = [] - var centralIndex: Int = 0 - for i in 0 ..< results.count { - entries.append(WebSearchGalleryEntry(result: results[i])) - if results[i] == currentResult { - centralIndex = i - } + func openResult(currentResult: ChatContextResult, present: @escaping (ViewController, Any?) -> Void) { + if self.controllerInteraction.selectionState != nil { + if let state = self.webSearchInterfaceState.state, state.scope == .images { + if let results = self.currentProcessedResults?.results { + presentLegacyWebSearchGallery(account: self.account, peer: self.peer, theme: self.theme, results: results, current: currentResult, selectionContext: self.controllerInteraction.selectionState, editingContext: self.controllerInteraction.editingState, updateHiddenMedia: { [weak self] id in + self?.hiddenMediaId.set(.single(id)) + }, initialLayout: self.containerLayout?.0, transitionHostView: { [weak self] in + return self?.gridNode.view + }, transitionView: { [weak self] result in + return self?.transitionView(for: result) + }, completed: { [weak self] result in + if let strongSelf = self { + strongSelf.controllerInteraction.sendSelected(result) + strongSelf.cancel?() + } + }, present: present) } - - let controller = WebSearchGalleryController(account: self.account, entries: entries, centralIndex: centralIndex, replaceRootController: { (controller, _) in + } else { + if let results = self.currentProcessedResults?.results { + var entries: [WebSearchGalleryEntry] = [] + var centralIndex: Int = 0 + for i in 0 ..< results.count { + entries.append(WebSearchGalleryEntry(result: results[i])) + if results[i] == currentResult { + centralIndex = i + } + } - }, baseNavigationController: nil) - self.hiddenMediaId.set((controller.hiddenMedia |> deliverOnMainQueue) - |> map { entry in - return entry?.result.id - }) - present(controller, WebSearchGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in - if let strongSelf = self { - var transitionNode: WebSearchItemNode? - strongSelf.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? WebSearchItemNode, itemNode.item?.result.id == entry.result.id { - transitionNode = itemNode + let controller = WebSearchGalleryController(account: self.account, peer: self.peer, selectionState: self.controllerInteraction.selectionState, editingState: self.controllerInteraction.editingState, entries: entries, centralIndex: centralIndex, replaceRootController: { (controller, _) in + + }, baseNavigationController: nil, sendCurrent: { [weak self] result in + if let strongSelf = self { + strongSelf.controllerInteraction.sendSelected(result) + strongSelf.cancel?() + } + }) + self.hiddenMediaId.set((controller.hiddenMedia |> deliverOnMainQueue) + |> map { entry in + return entry?.result.id + }) + present(controller, WebSearchGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in + if let strongSelf = self { + var transitionNode: WebSearchItemNode? + strongSelf.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? WebSearchItemNode, itemNode.item?.result.id == entry.result.id { + transitionNode = itemNode + } + } + if let transitionNode = transitionNode { + return GalleryTransitionArguments(transitionNode: (transitionNode, { [weak transitionNode] in + return transitionNode?.transitionView().snapshotContentTree(unhide: true) + }), addToTransitionSurface: { view in + if let strongSelf = self { + strongSelf.gridNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.gridNode.view) + } + }) } } - if let transitionNode = transitionNode { - return GalleryTransitionArguments(transitionNode: (transitionNode, { [weak transitionNode] in - return transitionNode?.transitionView().snapshotContentTree(unhide: true) - }), addToTransitionSurface: { view in - if let strongSelf = self { - strongSelf.gridNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.gridNode.view) - } - }) - } - } - return nil - })) + return nil + })) + } } + } else { + presentLegacyWebSearchEditor(account: self.account, theme: self.theme, result: currentResult, initialLayout: self.containerLayout?.0, updateHiddenMedia: { [weak self] id in + self?.hiddenMediaId.set(.single(id)) + }, transitionHostView: { [weak self] in + return self?.gridNode.view + }, transitionView: { [weak self] result in + return self?.transitionView(for: result) + }, completed: { [weak self] result in + if let strongSelf = self { + strongSelf.controllerInteraction.avatarCompleted(result) + strongSelf.cancel?() + } + }, present: present) } } diff --git a/TelegramUI/WebSearchGalleryController.swift b/TelegramUI/WebSearchGalleryController.swift index 403d555e36..9cd333cd03 100644 --- a/TelegramUI/WebSearchGalleryController.swift +++ b/TelegramUI/WebSearchGalleryController.swift @@ -5,7 +5,21 @@ import Postbox import SwiftSignalKit import AsyncDisplayKit import TelegramCore -import SafariServices +import LegacyComponents + +final class WebSearchGalleryControllerInteraction { + let dismiss: (Bool) -> Void + let send: (ChatContextResult) -> Void + let selectionState: TGMediaSelectionContext? + let editingState: TGMediaEditingContext + + init(dismiss: @escaping (Bool) -> Void, send: @escaping (ChatContextResult) -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { + self.dismiss = dismiss + self.send = send + self.selectionState = selectionState + self.editingState = editingState + } +} struct WebSearchGalleryEntry: Equatable { let result: ChatContextResult @@ -14,16 +28,16 @@ struct WebSearchGalleryEntry: Equatable { return lhs.result == rhs.result } - func item(account: Account, presentationData: PresentationData) -> GalleryItem { + func item(account: Account, presentationData: PresentationData, controllerInteraction: WebSearchGalleryControllerInteraction?) -> GalleryItem { switch self.result { - case let .externalReference(queryId, id, type, _, _, url, content, thumbnail, _): + case let .externalReference(_, _, type, _, _, _, content, thumbnail, _): if let content = content, type == "gif", let thumbnailResource = thumbnail?.resource, let dimensions = content.dimensions { let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])) - return UniversalVideoGalleryItem(account: account, presentationData: presentationData, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: fileReference, streamVideo: false, loopVideo: true, enableSound: false, fetchAutomatically: true), originData: nil, indexData: nil, contentInfo: nil, caption: NSAttributedString(), credit: nil, performAction: { _ in }, openActionOptions: { _ in }) + return WebSearchVideoGalleryItem(account: account, presentationData: presentationData, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: fileReference, streamVideo: false, loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) } - case let .internalReference(queryId, id, _, _, _, _, file, _): + case let .internalReference(_, _, _, _, _, _, file, _): if let file = file { - return UniversalVideoGalleryItem(account: account, presentationData: presentationData, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: .standalone(media: file), streamVideo: false, loopVideo: true, enableSound: false, fetchAutomatically: true), originData: nil, indexData: nil, contentInfo: nil, caption: NSAttributedString(), credit: nil, performAction: { _ in }, openActionOptions: { _ in }) + return WebSearchVideoGalleryItem(account: account, presentationData: presentationData, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: .standalone(media: file), streamVideo: false, loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) } } preconditionFailure() @@ -31,9 +45,11 @@ struct WebSearchGalleryEntry: Equatable { } final class WebSearchGalleryControllerPresentationArguments { + let animated: Bool let transitionArguments: (WebSearchGalleryEntry) -> GalleryTransitionArguments? - init(transitionArguments: @escaping (WebSearchGalleryEntry) -> GalleryTransitionArguments?) { + init(animated: Bool = true, transitionArguments: @escaping (WebSearchGalleryEntry) -> GalleryTransitionArguments?) { + self.animated = animated self.transitionArguments = transitionArguments } } @@ -45,6 +61,7 @@ class WebSearchGalleryController: ViewController { private let account: Account private var presentationData: PresentationData + private var controllerInteraction: WebSearchGalleryControllerInteraction? private let _ready = Promise() override var ready: Promise { @@ -63,6 +80,8 @@ class WebSearchGalleryController: ViewController { private let centralItemFooterContentNode = Promise() private let centralItemAttributesDisposable = DisposableSet(); + private var checkNode: GalleryNavigationCheckNode? + private let _hiddenMedia = Promise(nil) var hiddenMedia: Signal { return self._hiddenMedia.get() @@ -71,7 +90,7 @@ class WebSearchGalleryController: ViewController { private let replaceRootController: (ViewController, ValuePromise?) -> Void private let baseNavigationController: NavigationController? - init(account: Account, entries: [WebSearchGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?) { + init(account: Account, peer: Peer?, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext, entries: [WebSearchGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, ValuePromise?) -> Void, baseNavigationController: NavigationController?, sendCurrent: @escaping (ChatContextResult) -> Void) { self.account = account self.replaceRootController = replaceRootController self.baseNavigationController = baseNavigationController @@ -80,8 +99,24 @@ class WebSearchGalleryController: ViewController { super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: GalleryController.darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) - let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed)) - self.navigationItem.leftBarButtonItem = backItem + self.controllerInteraction = WebSearchGalleryControllerInteraction(dismiss: { [weak self] animated in + self?.dismiss(forceAway: false) + }, send: { [weak self] current in + sendCurrent(current) + self?.dismiss(forceAway: true) + }, selectionState: selectionState, editingState: editingState) + + if let title = peer?.displayTitle { + let recipientNode = GalleryNavigationRecipientNode(color: .white, title: title) + let leftItem = UIBarButtonItem(customDisplayNode: recipientNode) + self.navigationItem.leftBarButtonItem = leftItem + } + + let checkNode = GalleryNavigationCheckNode(theme: self.presentationData.theme) + checkNode.addTarget(target: self, action: #selector(self.checkPressed)) + let rightItem = UIBarButtonItem(customDisplayNode: checkNode) + self.navigationItem.rightBarButtonItem = rightItem + self.checkNode = checkNode self.statusBar.statusBarStyle = .White @@ -93,7 +128,7 @@ class WebSearchGalleryController: ViewController { strongSelf.centralEntryIndex = centralIndex if strongSelf.isViewLoaded { strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ - $0.item(account: account, presentationData: strongSelf.presentationData) + $0.item(account: account, presentationData: strongSelf.presentationData, controllerInteraction: strongSelf.controllerInteraction) }), centralItemIndex: centralIndex, keepFirst: false) let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in @@ -128,11 +163,16 @@ class WebSearchGalleryController: ViewController { self.centralItemAttributesDisposable.dispose() } - @objc func donePressed() { - self.dismiss(forceAway: false) + @objc func checkPressed() { + if let checkNode = self.checkNode, let controllerInteraction = self.controllerInteraction, let centralItemNode = self.galleryNode.pager.centralItemNode() as? WebSearchVideoGalleryItemNode, let item = centralItemNode.item { + let legacyItem = LegacyWebSearchItem(result: item.result) + + checkNode.setIsChecked(!checkNode.isChecked, animated: true) + controllerInteraction.selectionState?.setItem(legacyItem, selected: checkNode.isChecked) + } } - private func dismiss(forceAway: Bool) { + private func dismiss(forceAway: Bool, animated: Bool = true) { var animatedOutNode = true var animatedOutInterface = false @@ -143,22 +183,27 @@ class WebSearchGalleryController: ViewController { } } - if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? WebSearchGalleryControllerPresentationArguments { - if !self.entries.isEmpty { - if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway { - animatedOutNode = false - centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: { - animatedOutNode = true - completion() - }) + if animated { + if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? WebSearchGalleryControllerPresentationArguments { + if !self.entries.isEmpty { + if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway { + animatedOutNode = false + centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: { + animatedOutNode = true + completion() + }) + } } } - } - - self.galleryNode.animateOut(animateContent: animatedOutNode, completion: { + + self.galleryNode.animateOut(animateContent: animatedOutNode, completion: { + animatedOutInterface = true + completion() + }) + } else { animatedOutInterface = true completion() - }) + } } override func loadDisplayNode() { @@ -195,14 +240,14 @@ class WebSearchGalleryController: ViewController { } self.galleryNode.pager.replaceItems(self.entries.map({ - $0.item(account: account, presentationData: self.presentationData) + $0.item(account: account, presentationData: self.presentationData, controllerInteraction: self.controllerInteraction) }), centralItemIndex: self.centralEntryIndex) self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in if let strongSelf = self { - var hiddenItem: WebSearchGalleryEntry? + var item: WebSearchGalleryEntry? if let index = index { - hiddenItem = strongSelf.entries[index] + item = strongSelf.entries[index] if let node = strongSelf.galleryNode.pager.centralItemNode() { strongSelf.centralItemTitle.set(node.title()) @@ -210,9 +255,13 @@ class WebSearchGalleryController: ViewController { strongSelf.centralItemNavigationStyle.set(node.navigationStyle()) strongSelf.centralItemFooterContentNode.set(node.footerContent()) } + + if let checkNode = strongSelf.checkNode, let controllerInteraction = strongSelf.controllerInteraction, let selectionState = controllerInteraction.selectionState, let item = item { + checkNode.setIsChecked(selectionState.isIdentifierSelected(item.result.id), animated: false) + } } if strongSelf.didSetReady { - strongSelf._hiddenMedia.set(.single(hiddenItem)) + strongSelf._hiddenMedia.set(.single(item)) } } } @@ -234,10 +283,19 @@ class WebSearchGalleryController: ViewController { self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) self.centralItemFooterContentNode.set(centralItemNode.footerContent()) - if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]) { + let item = self.entries[centralItemNode.index] + if let transitionArguments = presentationArguments.transitionArguments(item) { nodeAnimatesItself = true - centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface) + centralItemNode.activateAsInitial() + if presentationArguments.animated { + centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface) + } + + if let checkNode = self.checkNode, let controllerInteraction = self.controllerInteraction, let selectionState = controllerInteraction.selectionState { + checkNode.setIsChecked(selectionState.isIdentifierSelected(item.result.id), animated: false) + } + self._hiddenMedia.set(.single(self.entries[centralItemNode.index])) } } diff --git a/TelegramUI/WebSearchGalleryFooterContentNode.swift b/TelegramUI/WebSearchGalleryFooterContentNode.swift new file mode 100644 index 0000000000..2600d8a6ac --- /dev/null +++ b/TelegramUI/WebSearchGalleryFooterContentNode.swift @@ -0,0 +1,83 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit +import LegacyComponents + +final class WebSearchGalleryFooterContentNode: GalleryFooterContentNode { + private let account: Account + private var theme: PresentationTheme + private var strings: PresentationStrings + + private let cancelButton: HighlightableButtonNode + private let sendButton: HighlightableButtonNode + + var cancel: (() -> Void)? + var send: (() -> Void)? + + init(account: Account, presentationData: PresentationData) { + self.account = account + self.theme = presentationData.theme + self.strings = presentationData.strings + + self.cancelButton = HighlightableButtonNode() + self.cancelButton.setImage(TGComponentsImageNamed("PhotoPickerBackIcon"), for: [.normal]) + self.sendButton = HighlightableButtonNode() + self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(self.theme), for: [.normal]) + + super.init() + + self.addSubnode(self.cancelButton) + self.addSubnode(self.sendButton) + + self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) + self.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside) + } + + func setCaption(_ caption: String) { + + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let panelSize: CGFloat = 49.0 + var panelHeight: CGFloat = panelSize + bottomInset + panelHeight += contentInset + var textFrame = CGRect() +// if !self.textNode.isHidden { +// let sideInset: CGFloat = 8.0 + leftInset +// let topInset: CGFloat = 8.0 +// let textBottomInset: CGFloat = 8.0 +// let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) +// panelHeight += textSize.height + topInset + textBottomInset +// textFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: textSize) +// } + + //self.textNode.frame = textFrame + + self.cancelButton.frame = CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - panelSize), size: CGSize(width: panelSize, height: panelSize)) + self.sendButton.frame = CGRect(origin: CGPoint(x: width - panelSize - rightInset, y: panelHeight - bottomInset - panelSize), size: CGSize(width: panelSize, height: panelSize)) + + return panelHeight + } + + override func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) { + self.cancelButton.alpha = 1.0 + self.sendButton.alpha = 1.0 + } + + override func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + self.cancelButton.alpha = 0.0 + self.sendButton.alpha = 0.0 + completion() + } + + @objc func cancelButtonPressed() { + self.cancel?() + } + + @objc func sendButtonPressed() { + self.send?() + } +} diff --git a/TelegramUI/WebSearchInterfaceState.swift b/TelegramUI/WebSearchInterfaceState.swift index c0525ff5cc..7b91023e0d 100644 --- a/TelegramUI/WebSearchInterfaceState.swift +++ b/TelegramUI/WebSearchInterfaceState.swift @@ -1,26 +1,13 @@ import Foundation -struct WebSearchSelectionState: Equatable { - let selectedIds: Set - - static func ==(lhs: WebSearchSelectionState, rhs: WebSearchSelectionState) -> Bool { - return lhs.selectedIds == rhs.selectedIds - } - - init(selectedIds: Set) { - self.selectedIds = selectedIds - } -} - -enum WebSearchMode: Int32 { +enum WebSearchScope: Int32 { case images case gifs } struct WebSearchInterfaceInnerState: Equatable { - let mode: WebSearchMode + let scope: WebSearchScope let query: String - let selectionState: WebSearchSelectionState } struct WebSearchInterfaceState: Equatable { @@ -37,27 +24,12 @@ struct WebSearchInterfaceState: Equatable { self.presentationData = presentationData } - func withUpdatedMode(_ mode: WebSearchMode) -> WebSearchInterfaceState { - return WebSearchInterfaceState(state: WebSearchInterfaceInnerState(mode: mode, query: self.state?.query ?? "", selectionState: self.state?.selectionState ?? WebSearchSelectionState(selectedIds: [])), presentationData: self.presentationData) + func withUpdatedScope(_ scope: WebSearchScope) -> WebSearchInterfaceState { + return WebSearchInterfaceState(state: WebSearchInterfaceInnerState(scope: scope, query: self.state?.query ?? ""), presentationData: self.presentationData) } func withUpdatedQuery(_ query: String) -> WebSearchInterfaceState { - return WebSearchInterfaceState(state: WebSearchInterfaceInnerState(mode: self.state?.mode ?? .images, query: query, selectionState: self.state?.selectionState ?? WebSearchSelectionState(selectedIds: [])), presentationData: self.presentationData) - } - - func withToggledSelectedMessages(_ ids: [String], value: Bool) -> WebSearchInterfaceState { - var selectedIds = Set() - if let selectionState = self.state?.selectionState { - selectedIds.formUnion(selectionState.selectedIds) - } - for id in ids { - if value { - selectedIds.insert(id) - } else { - selectedIds.remove(id) - } - } - return WebSearchInterfaceState(state: WebSearchInterfaceInnerState(mode: self.state?.mode ?? .images, query: self.state?.query ?? "", selectionState: WebSearchSelectionState(selectedIds: selectedIds)), presentationData: self.presentationData) + return WebSearchInterfaceState(state: WebSearchInterfaceInnerState(scope: self.state?.scope ?? .images, query: query), presentationData: self.presentationData) } func withUpdatedPresentationData(_ presentationData: PresentationData) -> WebSearchInterfaceState { diff --git a/TelegramUI/WebSearchItem.swift b/TelegramUI/WebSearchItem.swift index da1522de45..719dccf63a 100644 --- a/TelegramUI/WebSearchItem.swift +++ b/TelegramUI/WebSearchItem.swift @@ -40,10 +40,9 @@ final class WebSearchItem: GridItem { final class WebSearchItemNode: GridItemNode { private let imageNodeBackground: ASDisplayNode private let imageNode: TransformImageNode - private var selectionNode: GridMessageSelectionNode? + private var checkNode: CheckNode? private var currentImageResource: TelegramMediaResource? - private var currentVideoFile: TelegramMediaFile? private var currentDimensions: CGSize? private(set) var item: WebSearchItem? @@ -52,8 +51,6 @@ final class WebSearchItemNode: GridItemNode { private let fetchDisposable = MetaDisposable() private var resourceStatus: MediaResourceStatus? - private let statusNode: RadialStatusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) - override init() { self.imageNodeBackground = ASDisplayNode() self.imageNodeBackground.isLayerBacked = true @@ -86,35 +83,33 @@ final class WebSearchItemNode: GridItemNode { func setup(item: WebSearchItem) { if self.item !== item { var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - var updatedStatusSignal: Signal? + var thumbnailDimensions: CGSize? + var thumbnailResource: TelegramMediaResource? var imageResource: TelegramMediaResource? - var videoFile: TelegramMediaFile? var imageDimensions: CGSize? switch item.result { - case let .externalReference(_, _, type, title, _, url, content, thumbnail, _): + case let .externalReference(_, _, type, _, _, _, content, thumbnail, _): if let content = content { imageResource = content.resource } else if let thumbnail = thumbnail { imageResource = thumbnail.resource } imageDimensions = content?.dimensions - if type == "gif", let thumbnailResource = imageResource, let content = content, let dimensions = content.dimensions { - videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) - imageResource = nil + if type == "gif", let imageResource = imageResource, let content = content, let dimensions = content.dimensions { + thumbnailResource = imageResource + thumbnailDimensions = dimensions } - - if let file = videoFile { - updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(file.resource) - } else if let imageResource = imageResource { - updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(imageResource) - } - case let .internalReference(_, _, _, title, _, image, file, _): + case let .internalReference(_, _, _, _, _, image, file, _): if let image = image { if let largestRepresentation = largestImageRepresentation(image.representations) { imageDimensions = largestRepresentation.dimensions } imageResource = imageRepresentationLargerThan(image.representations, size: CGSize(width: 200.0, height: 100.0))?.resource + if let thumbnailRepresentation = smallestImageRepresentation(image.representations) { + thumbnailDimensions = thumbnailRepresentation.dimensions + thumbnailResource = thumbnailRepresentation.resource + } } else if let file = file { if let dimensions = file.dimensions { imageDimensions = dimensions @@ -123,32 +118,26 @@ final class WebSearchItemNode: GridItemNode { } imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource } - - // if let file = file { - // if file.isVideo && file.isAnimated { - // videoFile = file - // imageResource = nil - // updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(file.resource) - // } else if let imageResource = imageResource { - // updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(imageResource) - // } - // } else if let imageResource = imageResource { - // updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(imageResource) - // } } + var representations: [TelegramMediaImageRepresentation] = [] + if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { + representations.append(TelegramMediaImageRepresentation(dimensions: thumbnailDimensions, resource: thumbnailResource)) + } if let imageResource = imageResource, let imageDimensions = imageDimensions { - let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: imageResource) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], reference: nil, partialReference: nil) + representations.append(TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: imageResource)) + } + if !representations.isEmpty { + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, reference: nil, partialReference: nil) updateImageSignal = mediaGridMessagePhoto(account: item.account, photoReference: .standalone(media: tmpImage)) } else { updateImageSignal = .complete() } if let updateImageSignal = updateImageSignal { - let editingContext = item.controllerInteraction.editingContext + let editingContext = item.controllerInteraction.editingState let editedImageSignal = Signal { subscriber in - let editableItem = LegacyWebSearchItem(result: item.result, dimensions: CGSize(), thumbnailImage: .complete(), originalImage: .complete()) + let editableItem = LegacyWebSearchItem(result: item.result) if let signal = editingContext.thumbnailImageSignal(for: editableItem) { let disposable = signal.start(next: { next in if let image = next as? UIImage { @@ -200,46 +189,49 @@ final class WebSearchItemNode: GridItemNode { } self.currentImageResource = imageResource - self.currentVideoFile = videoFile self.currentDimensions = imageDimensions if let _ = imageDimensions { self.setNeedsLayout() } + self.updateHiddenMedia() } self.item = item self.updateSelectionState(animated: false) } + @objc func toggleSelection() { + if let checkNode = self.checkNode, let item = self.item { + checkNode.setIsChecked(!checkNode.isChecked, animated: true) + item.controllerInteraction.toggleSelection(item.result, checkNode.isChecked) + } + } + func updateSelectionState(animated: Bool) { - if self.selectionNode == nil, let item = self.item { - let selectionNode = GridMessageSelectionNode(theme: item.theme, toggle: { [weak self] value in - if let strongSelf = self, let item = strongSelf.item { - item.controllerInteraction.toggleSelection([item.result.id], value) - strongSelf.updateSelectionState(animated: true) - } - }) - self.addSubnode(selectionNode) - self.selectionNode = selectionNode + if self.checkNode == nil, let item = self.item, let _ = item.controllerInteraction.selectionState { + let checkNode = CheckNode(strokeColor: item.theme.list.itemCheckColors.strokeColor, fillColor: item.theme.list.itemCheckColors.fillColor, foregroundColor: item.theme.list.itemCheckColors.foregroundColor, style: .overlay) + checkNode.addTarget(target: self, action: #selector(self.toggleSelection)) + self.addSubnode(checkNode) + self.checkNode = checkNode self.setNeedsLayout() } if let item = self.item { if let selectionState = item.controllerInteraction.selectionState { - let selected = selectionState.selectedIds.contains(item.result.id) - self.selectionNode?.updateSelected(selected, animated: animated) + let selected = selectionState.isIdentifierSelected(item.result.id) + self.checkNode?.setIsChecked(selected, animated: animated) } } } func updateHiddenMedia() { if let item = self.item { - self.imageNode.isHidden = item.controllerInteraction.hiddenMediaId == item.result.id + self.isHidden = item.controllerInteraction.hiddenMediaId == item.result.id } } func transitionView() -> UIView { - let view = self.imageNode.view.snapshotContentTree(unhide: true)! + let view = self.view.snapshotContentTree(unhide: true, keepTransform: true)! view.frame = self.convert(self.bounds, to: nil) return view } @@ -256,11 +248,7 @@ final class WebSearchItemNode: GridItemNode { } let checkSize = CGSize(width: 32.0, height: 32.0) - self.selectionNode?.frame = CGRect(origin: CGPoint(x: imageFrame.width - checkSize.width, y: 0.0), size: checkSize) - let progressDiameter: CGFloat = 40.0 - self.statusNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - progressDiameter) / 2.0), y: floor((imageFrame.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter)) - - //self.videoAccessoryNode.frame = CGRect(origin: CGPoint(x: imageFrame.maxX - self.videoAccessoryNode.contentSize.width - 5, y: imageFrame.maxY - self.videoAccessoryNode.contentSize.height - 5), size: self.videoAccessoryNode.contentSize) + self.checkNode?.frame = CGRect(origin: CGPoint(x: imageFrame.width - checkSize.width, y: 0.0), size: checkSize) } @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { @@ -274,9 +262,6 @@ final class WebSearchItemNode: GridItemNode { switch gesture { case .tap: item.controllerInteraction.openResult(item.result) - - case .longTap: - break default: break } diff --git a/TelegramUI/WebSearchRecentQueryItem.swift b/TelegramUI/WebSearchRecentQueryItem.swift index f1feee0a62..c50710ec61 100644 --- a/TelegramUI/WebSearchRecentQueryItem.swift +++ b/TelegramUI/WebSearchRecentQueryItem.swift @@ -75,6 +75,7 @@ class WebSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { private var textNode: TextNode? private var item: WebSearchRecentQueryItem? + private var layoutParams: ListViewItemLayoutParams? required init() { self.backgroundNode = ASDisplayNode() @@ -152,6 +153,7 @@ class WebSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { return (nil, { _ in if let strongSelf = self { strongSelf.item = item + strongSelf.layoutParams = params if let _ = updatedTheme { strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor @@ -205,95 +207,16 @@ class WebSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) -// if let _ = self.item, let params = self.layoutParams?.5 { -// let editingOffset: CGFloat -// if let selectableControlNode = self.selectableControlNode { -// editingOffset = selectableControlNode.bounds.size.width -// var selectableControlFrame = selectableControlNode.frame -// selectableControlFrame.origin.x = params.leftInset + offset -// transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame) -// } else { -// editingOffset = 0.0 -// } -// -// if let reorderControlNode = self.reorderControlNode { -// var reorderControlFrame = reorderControlNode.frame -// reorderControlFrame.origin.x = params.width - params.rightInset - reorderControlFrame.size.width + offset -// transition.updateFrame(node: reorderControlNode, frame: reorderControlFrame) -// } -// -// let leftInset: CGFloat = params.leftInset + 78.0 -// -// let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: 8.0), size: CGSize(width: params.width - leftInset - params.rightInset - 10.0 - 1.0 - editingOffset, height: itemHeight - 12.0 - 9.0)) -// -// let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + offset, dy: 0.0) -// -// var avatarFrame = self.avatarNode.frame -// avatarFrame.origin.x = leftInset - 78.0 + editingOffset + 10.0 + offset -// transition.updateFrame(node: self.avatarNode, frame: avatarFrame) -// if let multipleAvatarsNode = self.multipleAvatarsNode { -// transition.updateFrame(node: multipleAvatarsNode, frame: avatarFrame) -// } -// -// var titleOffset: CGFloat = 0.0 -// if let secretIconNode = self.secretIconNode, let image = secretIconNode.image { -// transition.updateFrame(node: secretIconNode, frame: CGRect(origin: CGPoint(x: contentRect.minX, y: secretIconNode.frame.minY), size: image.size)) -// titleOffset += image.size.width + 3.0 -// } -// -// let titleFrame = self.titleNode.frame -// transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: titleFrame.origin.y), size: titleFrame.size)) -// -// let authorFrame = self.authorNode.frame -// transition.updateFrame(node: self.authorNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: authorFrame.origin.y), size: authorFrame.size)) -// -// transition.updateFrame(node: self.inputActivitiesNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: self.inputActivitiesNode.frame.minY), size: self.inputActivitiesNode.bounds.size)) -// -// let textFrame = self.textNode.frame -// transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: textFrame.origin.y), size: textFrame.size)) -// -// let dateFrame = self.dateNode.frame -// transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width, y: dateFrame.minY), size: dateFrame.size)) -// -// 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 + 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)) -// nextTitleIconOrigin += verificationIconNode.bounds.size.width + 5.0 -// } -// -// let mutedIconFrame = self.mutedIconNode.frame -// transition.updateFrame(node: self.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: contentRect.origin.y + 6.0), size: mutedIconFrame.size)) -// nextTitleIconOrigin += mutedIconFrame.size.width + 3.0 -// -// let badgeBackgroundFrame = self.badgeBackgroundNode.frame -// let updatedBadgeBackgroundFrame = CGRect(origin: CGPoint(x: contentRect.maxX - badgeBackgroundFrame.size.width, y: contentRect.maxY - badgeBackgroundFrame.size.height - 2.0), size: badgeBackgroundFrame.size) -// transition.updateFrame(node: self.badgeBackgroundNode, frame: updatedBadgeBackgroundFrame) -// -// if self.mentionBadgeNode.supernode != nil { -// let mentionBadgeSize = self.mentionBadgeNode.bounds.size -// let mentionBadgeOffset: CGFloat -// if updatedBadgeBackgroundFrame.size.width.isZero || self.badgeBackgroundNode.image == nil { -// mentionBadgeOffset = contentRect.maxX - mentionBadgeSize.width -// } else { -// mentionBadgeOffset = contentRect.maxX - updatedBadgeBackgroundFrame.size.width - 6.0 - mentionBadgeSize.width -// } -// -// let badgeBackgroundWidth = mentionBadgeSize.width -// let badgeBackgroundFrame = CGRect(x: mentionBadgeOffset, y: self.mentionBadgeNode.frame.origin.y, width: badgeBackgroundWidth, height: mentionBadgeSize.height) -// transition.updateFrame(node: self.mentionBadgeNode, frame: badgeBackgroundFrame) -// } -// -// let badgeTextFrame = self.badgeTextNode.frame -// transition.updateFrame(node: self.badgeTextNode, frame: CGRect(origin: CGPoint(x: updatedBadgeBackgroundFrame.midX - badgeTextFrame.size.width / 2.0, y: badgeTextFrame.minY), size: badgeTextFrame.size)) -// } + if let params = self.layoutParams, let textNode = self.textNode { + let leftInset: CGFloat = 15.0 + params.leftInset + + var textFrame = textNode.frame + textFrame.origin.x = leftInset + offset + transition.updateFrame(node: textNode, frame: textFrame) + } } override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { - var close = true if let item = self.item { switch option.key { case RevealOptionKey.delete.rawValue: @@ -302,9 +225,7 @@ class WebSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { break } } - if close { - self.setRevealOptionsOpened(false, animated: true) - self.revealOptionsInteractivelyClosed() - } + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() } } diff --git a/TelegramUI/WebSearchSettings.swift b/TelegramUI/WebSearchSettings.swift index b35fdd0e27..e31f40028a 100644 --- a/TelegramUI/WebSearchSettings.swift +++ b/TelegramUI/WebSearchSettings.swift @@ -3,22 +3,22 @@ import Postbox import SwiftSignalKit struct WebSearchSettings: Equatable, PreferencesEntry { - var mode: WebSearchMode + var scope: WebSearchScope static var defaultSettings: WebSearchSettings { - return WebSearchSettings(mode: .images) + return WebSearchSettings(scope: .images) } - init(mode: WebSearchMode) { - self.mode = mode + init(scope: WebSearchScope) { + self.scope = scope } init(decoder: PostboxDecoder) { - self.mode = WebSearchMode(rawValue: decoder.decodeInt32ForKey("mode", orElse: 0)) ?? .images + self.scope = WebSearchScope(rawValue: decoder.decodeInt32ForKey("scope", orElse: 0)) ?? .images } func encode(_ encoder: PostboxEncoder) { - encoder.encodeInt32(self.mode.rawValue, forKey: "mode") + encoder.encodeInt32(self.scope.rawValue, forKey: "scope") } func isEqual(to: PreferencesEntry) -> Bool { diff --git a/TelegramUI/WebSearchVideoGalleryItem.swift b/TelegramUI/WebSearchVideoGalleryItem.swift new file mode 100644 index 0000000000..26b2591bf1 --- /dev/null +++ b/TelegramUI/WebSearchVideoGalleryItem.swift @@ -0,0 +1,520 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import Display +import Postbox + +class WebSearchVideoGalleryItem: GalleryItem { + let account: Account + let presentationData: PresentationData + let result: ChatContextResult + let content: UniversalVideoContent + let controllerInteraction: WebSearchGalleryControllerInteraction? + + init(account: Account, presentationData: PresentationData, result: ChatContextResult, content: UniversalVideoContent, controllerInteraction: WebSearchGalleryControllerInteraction?) { + self.account = account + self.presentationData = presentationData + self.result = result + self.content = content + self.controllerInteraction = controllerInteraction + } + + func node() -> GalleryItemNode { + let node = WebSearchVideoGalleryItemNode(account: self.account, presentationData: self.presentationData, controllerInteraction: self.controllerInteraction) + node.setupItem(self) + return node + } + + func updateNode(node: GalleryItemNode) { + if let node = node as? WebSearchVideoGalleryItemNode { + node.setupItem(self) + } + } + + func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { + return nil + } +} + +private struct FetchControls { + let fetch: () -> Void + let cancel: () -> Void +} + +final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { + private let account: Account + private let strings: PresentationStrings + private let controllerInteraction: WebSearchGalleryControllerInteraction? + + fileprivate let _ready = Promise() + + private let footerContentNode: WebSearchGalleryFooterContentNode + + private var videoNode: UniversalVideoNode? + private let statusButtonNode: HighlightableButtonNode + private let statusNode: RadialStatusNode + + private var isCentral = false + private var validLayout: (ContainerViewLayout, CGFloat)? + private var didPause = false + private var isPaused = true + + private var requiresDownload = false + + var item: WebSearchVideoGalleryItem? + + private let statusDisposable = MetaDisposable() + + private let fetchDisposable = MetaDisposable() + private var fetchStatus: MediaResourceStatus? + private var fetchControls: FetchControls? + + var playbackCompleted: (() -> Void)? + + init(account: Account, presentationData: PresentationData, controllerInteraction: WebSearchGalleryControllerInteraction?) { + self.account = account + self.strings = presentationData.strings + self.controllerInteraction = controllerInteraction + + self.footerContentNode = WebSearchGalleryFooterContentNode(account: account, presentationData: presentationData) + + self.statusButtonNode = HighlightableButtonNode() + self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) + + super.init() + + self.statusButtonNode.addSubnode(self.statusNode) + self.statusButtonNode.addTarget(self, action: #selector(statusButtonPressed), forControlEvents: .touchUpInside) + + self.addSubnode(self.statusButtonNode) + + self.footerContentNode.cancel = { + controllerInteraction?.dismiss(true) + } + self.footerContentNode.send = { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + controllerInteraction?.send(item.result) + } + } + } + + deinit { + self.statusDisposable.dispose() + } + + override func ready() -> Signal { + return self._ready.get() + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + self.validLayout = (layout, navigationBarHeight) + + let statusDiameter: CGFloat = 50.0 + let statusFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusDiameter) / 2.0), y: floor((layout.size.height - statusDiameter) / 2.0)), size: CGSize(width: statusDiameter, height: statusDiameter)) + transition.updateFrame(node: self.statusButtonNode, frame: statusFrame) + transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusFrame.size)) + } + + func setupItem(_ item: WebSearchVideoGalleryItem) { + if self.item?.content.id != item.content.id { + var isAnimated = false + var mediaResource: MediaResource? + if let content = item.content as? NativeVideoContent { + isAnimated = content.fileReference.media.isAnimated + mediaResource = content.fileReference.media.resource + } + + if let videoNode = self.videoNode { + videoNode.canAttachContent = false + videoNode.removeFromSupernode() + } + + guard let mediaManager = item.account.telegramApplicationContext.mediaManager else { + preconditionFailure() + } + + let videoNode = UniversalVideoNode(postbox: item.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) + let videoSize = CGSize(width: item.content.dimensions.width * 2.0, height: item.content.dimensions.height * 2.0) + videoNode.updateLayout(size: videoSize, transition: .immediate) + self.videoNode = videoNode + videoNode.isUserInteractionEnabled = false + videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335) + videoNode.canAttachContent = true + + self.requiresDownload = true + var mediaFileStatus: Signal = .single(nil) + if let mediaResource = mediaResource { + mediaFileStatus = item.account.postbox.mediaBox.resourceStatus(mediaResource) + |> map(Optional.init) + } + + self.statusDisposable.set((combineLatest(videoNode.status, mediaFileStatus) + |> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in + if let strongSelf = self { + 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 + case let .buffering(_, whilePlaying): + initialBuffering = true + isPaused = !whilePlaying + var isStreaming = false + if let fetchStatus = strongSelf.fetchStatus { + switch fetchStatus { + case .Local: + break + default: + isStreaming = true + } + } + if let content = item.content as? NativeVideoContent, !isStreaming { + initialBuffering = false + if !content.enableSound { + isPaused = false + } + } + default: + if let content = item.content as? NativeVideoContent, !content.streamVideo { + if !content.enableSound { + isPaused = false + } + } + } + } + + var fetching = false + if initialBuffering { + strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false), animated: false, completion: {}) + } else { + var state: RadialStatusNodeState = .play(.white) + + if let fetchStatus = fetchStatus { + if strongSelf.requiresDownload { + switch fetchStatus { + case .Remote: + state = .download(.white) + case let .Fetching(_, progress): + fetching = true + isPaused = true + state = .progress(color: .white, lineWidth: nil, value: CGFloat(progress), cancelEnabled: false) + default: + break + } + } + } + strongSelf.statusNode.transitionToState(state, animated: false, completion: {}) + } + + strongSelf.isPaused = isPaused + strongSelf.fetchStatus = fetchStatus + + strongSelf.statusButtonNode.isHidden = !initialBuffering && !isPaused && !fetching + } + })) + + self.zoomableContent = (videoSize, videoNode) + + videoNode.playbackCompleted = { [weak videoNode] in + Queue.mainQueue().async { + //item.playbackCompleted() + if !isAnimated { + videoNode?.seek(0.0) + } + } + } + + self._ready.set(videoNode.ready) + } + + self.item = item + } + + override func centralityUpdated(isCentral: Bool) { + super.centralityUpdated(isCentral: isCentral) + + if self.isCentral != isCentral { + self.isCentral = isCentral + + if let videoNode = self.videoNode, videoNode.ownsContentNode { + if isCentral { + videoNode.play() + } else { + videoNode.pause() + } + } + } + } + + override func activateAsInitial() { + if self.isCentral { + self.videoNode?.play() + } + } + + override func animateIn(from node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void) { + guard let videoNode = self.videoNode else { + return + } + + if let node = node.0 as? OverlayMediaItemNode { + var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) + + videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + + transformedFrame.origin = CGPoint() + + let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + + self.account.telegramApplicationContext.mediaManager?.setOverlayVideoNode(nil) + } else { + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) + let transformedCopyViewFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) + + let surfaceCopyView = node.1()! + let copyView = node.1()! + + addToTransitionSurface(surfaceCopyView) + + var transformedSurfaceFrame: CGRect? + var transformedSurfaceFinalFrame: CGRect? + if let contentSurface = surfaceCopyView.superview { + transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) + transformedSurfaceFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: contentSurface) + } + + if let transformedSurfaceFrame = transformedSurfaceFrame { + surfaceCopyView.frame = transformedSurfaceFrame + } + + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) + copyView.frame = transformedSelfFrame + + copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false) + + surfaceCopyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + + copyView.layer.animatePosition(from: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak copyView] _ in + copyView?.removeFromSuperview() + }) + let scale = CGSize(width: transformedCopyViewFinalFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewFinalFrame.size.height / transformedSelfFrame.size.height) + copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) + + if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceFinalFrame = transformedSurfaceFinalFrame { + surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak surfaceCopyView] _ in + surfaceCopyView?.removeFromSuperview() + }) + let scale = CGSize(width: transformedSurfaceFinalFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceFinalFrame.size.height / transformedSurfaceFrame.size.height) + surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) + } + + videoNode.allowsGroupOpacity = true + videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak videoNode] _ in + videoNode?.allowsGroupOpacity = false + }) + videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + + transformedFrame.origin = CGPoint() + + let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + + self.statusButtonNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusButtonNode.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.statusButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.statusButtonNode.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + } + } + + override func animateOut(to node: (ASDisplayNode, () -> UIView?), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + guard let videoNode = self.videoNode else { + completion() + return + } + + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) + let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) + + var positionCompleted = false + var boundsCompleted = false + var copyCompleted = false + + let copyView = node.1()! + let surfaceCopyView = node.1()! + + addToTransitionSurface(surfaceCopyView) + + var transformedSurfaceFrame: CGRect? + var transformedSurfaceCopyViewInitialFrame: CGRect? + if let contentSurface = surfaceCopyView.superview { + transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) + transformedSurfaceCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: contentSurface) + } + + self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) + copyView.frame = transformedSelfFrame + + let intermediateCompletion = { [weak copyView, weak surfaceCopyView] in + if positionCompleted && boundsCompleted && copyCompleted { + copyView?.removeFromSuperview() + surfaceCopyView?.removeFromSuperview() + completion() + } + } + + copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false) + surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) + + copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) + copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + copyCompleted = true + intermediateCompletion() + }) + + if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedCopyViewInitialFrame = transformedSurfaceCopyViewInitialFrame { + surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSurfaceFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSurfaceFrame.size.height) + surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) + } + + videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + positionCompleted = true + intermediateCompletion() + }) + + videoNode.allowsGroupOpacity = true + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in + videoNode?.allowsGroupOpacity = false + }) + + self.statusButtonNode.layer.animatePosition(from: self.statusButtonNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + //positionCompleted = true + //intermediateCompletion() + }) + self.statusButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.statusButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) + + transformedFrame.origin = CGPoint() + + let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + boundsCompleted = true + intermediateCompletion() + }) + } + + func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) { + guard let videoNode = self.videoNode else { + completion() + return + } + + var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) + let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) + let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) + let transformedSelfTargetSuperFrame = videoNode.view.convert(videoNode.view.bounds, to: node.view.superview) + + var positionCompleted = false + var boundsCompleted = false + var copyCompleted = false + var nodeCompleted = false + + let copyView = node.view.snapshotContentTree()! + + videoNode.isHidden = true + copyView.frame = transformedSelfFrame + + let intermediateCompletion = { [weak copyView] in + if positionCompleted && boundsCompleted && copyCompleted && nodeCompleted { + copyView?.removeFromSuperview() + completion() + } + } + + copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) + + copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) + copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + copyCompleted = true + intermediateCompletion() + }) + + videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + positionCompleted = true + intermediateCompletion() + }) + + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + + self.statusButtonNode.layer.animatePosition(from: self.statusButtonNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + //positionCompleted = true + //intermediateCompletion() + }) + self.statusButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.statusButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) + + transformedFrame.origin = CGPoint() + + let videoTransform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) + videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: videoTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + boundsCompleted = true + intermediateCompletion() + }) + + let nodeTransform = CATransform3DScale(node.layer.transform, videoNode.layer.bounds.size.width / transformedFrame.size.width, videoNode.layer.bounds.size.height / transformedFrame.size.height, 1.0) + node.layer.animatePosition(from: CGPoint(x: transformedSelfTargetSuperFrame.midX, y: transformedSelfTargetSuperFrame.midY), to: node.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + node.layer.animate(from: NSValue(caTransform3D: nodeTransform), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + nodeCompleted = true + intermediateCompletion() + }) + } + + @objc func statusButtonPressed() { + if let videoNode = self.videoNode { + if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { + self.toggleControlsVisibility() + } + + if let fetchStatus = self.fetchStatus { + switch fetchStatus { + case .Local: + videoNode.togglePlayPause() + case .Remote: + if self.requiresDownload { + self.fetchControls?.fetch() + } else { + videoNode.togglePlayPause() + } + case .Fetching: + self.fetchControls?.cancel() + } + } else { + videoNode.togglePlayPause() + } + } + } + + override func footerContent() -> Signal { + return .single(self.footerContentNode) + } +} diff --git a/TelegramUI/WebpagePreviewAccessoryPanelNode.swift b/TelegramUI/WebpagePreviewAccessoryPanelNode.swift index 1b0d276809..e7aa2bed1e 100644 --- a/TelegramUI/WebpagePreviewAccessoryPanelNode.swift +++ b/TelegramUI/WebpagePreviewAccessoryPanelNode.swift @@ -72,11 +72,11 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { } if let text = self.titleString?.string { - titleString = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) + self.titleString = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) } if let text = self.textString?.string { - textString = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) + self.textString = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) } self.updateWebpage() @@ -101,6 +101,16 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { authorName = self.strings.Channel_NotificationLoading text = self.url case let .Loaded(content): + if let contentText = content.text { + text = contentText + } else { + if let file = content.file, let mediaKind = mediaContentKind(file) { + text = stringForMediaKind(mediaKind, strings: self.strings).0 + } else if let _ = content.image { + text = stringForMediaKind(.image, strings: self.strings).0 + } + } + if let title = content.title { authorName = title } else if let websiteName = content.websiteName { @@ -108,7 +118,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { } else { authorName = content.displayUrl } - text = content.text ?? "" + } self.titleString = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor)