diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3c103b5c24..b02c43c5f8 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7585,9 +7585,7 @@ Sorry for the inconvenience."; "Premium.Stickers.Proceed" = "Unlock Premium Stickers"; "Premium.Reactions.Proceed" = "Unlock Premium Reactions"; - "Premium.AppIcons.Proceed" = "Unlock Premium Icons"; - "Premium.NoAds.Proceed" = "About Telegram Premium"; "AccessDenied.LocationPreciseDenied" = "To share your specific location in this chat, please go to Settings > Privacy > Location Services > Telegram and set Precise Location to On."; @@ -8271,5 +8269,11 @@ Sorry for the inconvenience."; "EmojiSearch.SearchTopicIconsPlaceholder" = "Search Topic Icons"; "EmojiSearch.SearchTopicIconsEmptyResult" = "No emoji found"; -"Username.UsernamePurchaseAvailable" = "Sorry, this username is occupied by someone. But it's available for purchase through official @auction."; -"Channel.Username.UsernamePurchaseAvailable" = "Sorry, this link is occupied by someone. But it's available for purchase through official @auction."; +"Username.UsernamePurchaseAvailable" = "Sorry, this username is occupied by someone. But it's available for purchase on [fragment.com]()."; +"Channel.Username.UsernamePurchaseAvailable" = "Sorry, this link is occupied by someone. But it's available for purchase on [fragment.com]()."; + +"DownloadList.IncreaseSpeed" = "Increase Speed"; +"Conversation.IncreaseSpeed" = "Increase Speed"; + +"Premium.ChatManagement.Proceed" = "About Telegram Premium"; +"Premium.FasterSpeed.Proceed" = "About Telegram Premium"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index d4bd95daf8..aa51262afe 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -836,6 +836,7 @@ public enum PremiumIntroSource { case profile(PeerId) case emojiStatus(PeerId, Int64, TelegramMediaFile?, LoadedStickerPack?) case voiceToText + case fasterDownload } #if ENABLE_WALLET diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 65508cd3de..20f5e29d3c 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -601,11 +601,11 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { context.sharedContext.hasOngoingCall.get(), itemNode.listNode.preloadItems.get() ) - |> map { hasOngoingCall, preloadItems -> [ChatHistoryPreloadItem] in + |> map { hasOngoingCall, preloadItems -> Set in if hasOngoingCall { - return [] + return Set() } else { - return preloadItems + return Set(preloadItems) } }) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 6872c774f4..7a54d41732 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -33,6 +33,7 @@ import Postbox import TelegramAnimatedStickerNode import AnimationCache import MultiAnimationRenderer +import PremiumUI private enum ChatListTokenId: Int32 { case archive @@ -284,7 +285,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo isForum = true } - filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: strongSelf.hasDownloads).map(\.filter) + filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && strongSelf.hasDownloads).map(\.filter) } strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilter?.id, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition) } @@ -896,13 +897,15 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let items = combineLatest(queue: .mainQueue(), context.sharedContext.chatAvailableMessageActions(engine: context.engine, accountPeerId: context.account.peerId, messageIds: [message.id], messages: [message.id: message], peers: [:]), - isCachedValue |> take(1) + isCachedValue |> take(1), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) ) |> deliverOnMainQueue - |> map { [weak self] actions, isCachedValue -> [ContextMenuItem] in + |> map { [weak self] actions, isCachedValue, accountPeer -> [ContextMenuItem] in guard let strongSelf = self else { return [] } + let isPremium = accountPeer?.isPremium ?? false var items: [ContextMenuItem] = [] @@ -920,6 +923,31 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo }) }))) } else { + if !isPremium, let size = downloadResource?.size, size >= 300 * 1024 * 1024 { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.DownloadList_IncreaseSpeed, textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Speed"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + guard let strongSelf = self else { + f(.default) + return + } + + let context = strongSelf.context + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumDemoScreen(context: context, subject: .fasterDownload, action: { + let controller = PremiumIntroScreen(context: context, source: .fasterDownload) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + strongSelf.navigationController?.pushViewController(controller, animated: false, completion: {}) + + f(.default) + }))) + items.append(.separator) + } + if let downloadResource = downloadResource, !downloadResource.isFirstInList { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.DownloadList_RaisePriority, textColor: .primary, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Raise"), color: theme.contextMenu.primaryColor) @@ -1032,7 +1060,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c.dismiss(completion: { [weak self] in - self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), nil, message.id, false) + self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), message.threadId, message.id, false) }) }))) @@ -1078,7 +1106,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, f in c.dismiss(completion: { - self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), nil, message.id, false) + self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), message.threadId, message.id, false) }) }))) diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 0f983e673e..f0d7fc81dc 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -908,6 +908,10 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsStackNode.frame.midX } + if case .reference = self.source { + actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsStackNode.frame.midX + } + let actionsVerticalTransitionDirection: CGFloat if let contentNode = contentNode { if contentNode.frame.minY < self.actionsStackNode.frame.minY { @@ -1123,6 +1127,11 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if case .center = actionsHorizontalAlignment { actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsStackNode.frame.midX } + + if case .reference = self.source { + actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsStackNode.frame.midX + } + let actionsPositionDeltaYDistance = -animationInContentDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing self.actionsStackNode.layer.animate( from: NSValue(cgPoint: CGPoint()), diff --git a/submodules/Display/Source/TapLongTapOrDoubleTapGestureRecognizer.swift b/submodules/Display/Source/TapLongTapOrDoubleTapGestureRecognizer.swift index a6e37daf86..d5993751d1 100644 --- a/submodules/Display/Source/TapLongTapOrDoubleTapGestureRecognizer.swift +++ b/submodules/Display/Source/TapLongTapOrDoubleTapGestureRecognizer.swift @@ -196,9 +196,18 @@ public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, self.highlight?(touchLocation) } - if let hitResult = self.view?.hitTest(touch.location(in: self.view), with: event), let _ = hitResult as? UIButton { - self.state = .failed - return + if let hitResult = self.view?.hitTest(touch.location(in: self.view), with: event) { + var fail = false + if let _ = hitResult as? UIButton { + fail = true + } else if let node = hitResult.asyncdisplaykit_node, node is ASControlNode { + fail = true + } + + if fail { + self.state = .failed + return + } } self.tapCount += 1 diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index fe075fecb1..392584de68 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -19,6 +19,7 @@ import Speak import TranslateUI import ShareController import UndoUI +import ContextUI enum ChatMediaGalleryThumbnail: Equatable { case image(ImageMediaReference) @@ -201,6 +202,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private let recognitionOverlayContentNode: ImageRecognitionOverlayContentNode + private let moreBarButton: MoreHeaderButton + private var tilingNode: TilingNode? fileprivate let _ready = Promise() fileprivate let _title = Promise() @@ -238,6 +241,10 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) self.statusNode.isHidden = true + self.moreBarButton = MoreHeaderButton() + self.moreBarButton.isUserInteractionEnabled = true + self.moreBarButton.setContent(.more(optionsCircleImage(dark: false))) + super.init() self.clipsToBounds = true @@ -275,6 +282,11 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } } } + + self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside) + self.moreBarButton.contextAction = { [weak self] sourceNode, gesture in + self?.openMoreMenu(sourceNode: sourceNode, gesture: gesture) + } } override func isPagingEnabled() -> Signal { @@ -326,75 +338,75 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { case .medium, .full: strongSelf.statusNodeContainer.isHidden = true - Queue.concurrentDefaultQueue().async { - if let message = strongSelf.message, !message.isCopyProtected() && !imageReference.media.flags.contains(.hasStickers) { - strongSelf.recognitionDisposable.set((recognizedContent(engine: strongSelf.context.engine, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id) - |> deliverOnMainQueue).start(next: { [weak self] results in - if let strongSelf = self { - strongSelf.recognizedContentNode?.removeFromSupernode() - if !results.isEmpty { - let size = strongSelf.imageNode.bounds.size - let recognizedContentNode = RecognizedContentContainer(size: size, recognitions: results, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, present: { [weak self] c, a in - if let strongSelf = self { - strongSelf.galleryController()?.presentInGlobalOverlay(c, with: a) - } - }, performAction: { [weak self] string, action in - guard let strongSelf = self else { - return - } - switch action { - case .copy: - UIPasteboard.general.string = string - if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with({ $0 }) - let tooltipController = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }) - controller.present(tooltipController, in: .window(.root)) - } - case .share: - if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController { - let shareController = ShareController(context: strongSelf.context, subject: .text(string), externalShare: true, immediateExternalShare: false, updatedPresentationData: (strongSelf.context.sharedContext.currentPresentationData.with({ $0 }), strongSelf.context.sharedContext.presentationData)) - controller.present(shareController, in: .window(.root)) - } - case .lookup: - let controller = UIReferenceLibraryViewController(term: string) - if let window = strongSelf.baseNavigationController()?.view.window { - controller.popoverPresentationController?.sourceView = window - controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0)) - window.rootViewController?.present(controller, animated: true) - } - case .speak: - let _ = speakText(context: strongSelf.context, text: string) - case .translate: - if let parentController = strongSelf.baseNavigationController()?.topViewController as? ViewController { - let controller = TranslateScreen(context: strongSelf.context, text: string, fromLanguage: nil) - controller.pushController = { [weak parentController] c in - (parentController?.navigationController as? NavigationController)?._keepModalDismissProgress = true - parentController?.push(c) - } - controller.presentController = { [weak parentController] c in - parentController?.present(c, in: .window(.root)) - } - parentController.present(controller, in: .window(.root)) - } - } - }) - recognizedContentNode.barcodeAction = { [weak self] payload, rect in - guard let strongSelf = self, let message = strongSelf.message else { - return - } - strongSelf.footerContentNode.openActionOptions?(.url(url: payload, concealed: true), message) - } - recognizedContentNode.alpha = 0.0 - recognizedContentNode.frame = CGRect(origin: CGPoint(), size: size) - recognizedContentNode.update(size: strongSelf.imageNode.bounds.size, transition: .immediate) - strongSelf.imageNode.addSubnode(recognizedContentNode) - strongSelf.recognizedContentNode = recognizedContentNode - strongSelf.recognitionOverlayContentNode.transitionIn() - } - } - })) - } - } +// Queue.concurrentDefaultQueue().async { +// if let message = strongSelf.message, !message.isCopyProtected() && !imageReference.media.flags.contains(.hasStickers) { +// strongSelf.recognitionDisposable.set((recognizedContent(engine: strongSelf.context.engine, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id) +// |> deliverOnMainQueue).start(next: { [weak self] results in +// if let strongSelf = self { +// strongSelf.recognizedContentNode?.removeFromSupernode() +// if !results.isEmpty { +// let size = strongSelf.imageNode.bounds.size +// let recognizedContentNode = RecognizedContentContainer(size: size, recognitions: results, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, present: { [weak self] c, a in +// if let strongSelf = self { +// strongSelf.galleryController()?.presentInGlobalOverlay(c, with: a) +// } +// }, performAction: { [weak self] string, action in +// guard let strongSelf = self else { +// return +// } +// switch action { +// case .copy: +// UIPasteboard.general.string = string +// if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController { +// let presentationData = strongSelf.context.sharedContext.currentPresentationData.with({ $0 }) +// let tooltipController = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }) +// controller.present(tooltipController, in: .window(.root)) +// } +// case .share: +// if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController { +// let shareController = ShareController(context: strongSelf.context, subject: .text(string), externalShare: true, immediateExternalShare: false, updatedPresentationData: (strongSelf.context.sharedContext.currentPresentationData.with({ $0 }), strongSelf.context.sharedContext.presentationData)) +// controller.present(shareController, in: .window(.root)) +// } +// case .lookup: +// let controller = UIReferenceLibraryViewController(term: string) +// if let window = strongSelf.baseNavigationController()?.view.window { +// controller.popoverPresentationController?.sourceView = window +// controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0)) +// window.rootViewController?.present(controller, animated: true) +// } +// case .speak: +// let _ = speakText(context: strongSelf.context, text: string) +// case .translate: +// if let parentController = strongSelf.baseNavigationController()?.topViewController as? ViewController { +// let controller = TranslateScreen(context: strongSelf.context, text: string, fromLanguage: nil) +// controller.pushController = { [weak parentController] c in +// (parentController?.navigationController as? NavigationController)?._keepModalDismissProgress = true +// parentController?.push(c) +// } +// controller.presentController = { [weak parentController] c in +// parentController?.present(c, in: .window(.root)) +// } +// parentController.present(controller, in: .window(.root)) +// } +// } +// }) +// recognizedContentNode.barcodeAction = { [weak self] payload, rect in +// guard let strongSelf = self, let message = strongSelf.message else { +// return +// } +// strongSelf.footerContentNode.openActionOptions?(.url(url: payload, concealed: true), message) +// } +// recognizedContentNode.alpha = 0.0 +// recognizedContentNode.frame = CGRect(origin: CGPoint(), size: size) +// recognizedContentNode.update(size: strongSelf.imageNode.bounds.size, transition: .immediate) +// strongSelf.imageNode.addSubnode(recognizedContentNode) +// strongSelf.recognizedContentNode = recognizedContentNode +// strongSelf.recognitionOverlayContentNode.transitionIn() +// } +// } +// })) +// } +// } case .none, .blurred: strongSelf.statusNodeContainer.isHidden = false @@ -411,12 +423,17 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } else { self._ready.set(.single(Void())) } + + var barButtonItems: [UIBarButtonItem] = [] if imageReference.media.flags.contains(.hasStickers) { let rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Stickers"), color: .white), style: .plain, target: self, action: #selector(self.openStickersButtonPressed)) - self._rightBarButtonItems.set(.single([rightBarButtonItem])) - } else { - self._rightBarButtonItems.set(.single([])) + barButtonItems.append(rightBarButtonItem) } + if self.message != nil { + let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)! + barButtonItems.append(moreMenuItem) + } + self._rightBarButtonItems.set(.single(barButtonItems)) } self.contextAndMedia = (self.context, imageReference.abstract) } @@ -453,6 +470,125 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self._ready.set(.single(Void())) } + @objc private func moreButtonPressed() { + self.moreBarButton.play() + self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil) + } + + private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> { + var items: [ContextMenuItem] = [] + + if let message = self.message { + let context = self.context + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in + + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let strongSelf = self, let peer = peer else { + return + } + if let navigationController = strongSelf.baseNavigationController() { + strongSelf.beginCustomDismiss(true) + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: true, timecode: nil))) + + Queue.mainQueue().after(0.3) { + strongSelf.completeCustomDismiss() + } + } + f(.default) + }) + }))) + } + +// if #available(iOS 11.0, *) { +// items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in +// f(.default) +// guard let strongSelf = self else { +// return +// } +// strongSelf.beginAirPlaySetup() +// }))) +// } + +// if let (message, _, _) = strongSelf.contentInfo() { +// for media in message.media { +// if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { +// let url = content.url +// +// let item = OpenInItem.url(url: url) +// let openText = strongSelf.presentationData.strings.Conversation_FileOpenIn +// items.append(.action(ContextMenuActionItem(text: openText, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in +// f(.default) +// +// if let strongSelf = self, let controller = strongSelf.galleryController() { +// var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } +// if !presentationData.theme.overallDarkAppearance { +// presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) +// } +// let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in +// if let strongSelf = self { +// strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {}) +// } +// }) +// controller.present(actionSheet, in: .window(.root)) +// } +// }))) +// break +// } +// } +// } + +// if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() { +// items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in +// f(.default) +// +// if let strongSelf = self { +// switch strongSelf.fetchStatus { +// case .Local: +// let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .message(message: MessageReference(message), media: file)) +// |> deliverOnMainQueue).start(completed: { +// guard let strongSelf = self else { +// return +// } +// guard let controller = strongSelf.galleryController() else { +// return +// } +// controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_VideoSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) +// }) +// default: +// guard let controller = strongSelf.galleryController() else { +// return +// } +// controller.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Gallery_WaitForVideoDownoad, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { +// })]), in: .window(.root)) +// } +// } +// }))) +// } +// if strongSelf.canDelete() { +// items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in +// f(.default) +// +// if let strongSelf = self { +// strongSelf.footerContentNode.deleteButtonPressed() +// } +// }))) +// } + + return .single(items) + } + + private func openMoreMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { + let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems() + guard let controller = self.baseNavigationController()?.topViewController as? ViewController else { + return + } + + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + controller.presentInGlobalOverlay(contextController) + } + @objc func openStickersButtonPressed() { guard let (context, media) = self.contextAndMedia else { return diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 11142b4271..8ecb9adaed 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -290,7 +290,7 @@ func optionsBackgroundImage(dark: Bool) -> UIImage? { })?.stretchableImage(withLeftCapWidth: 14, topCapHeight: 14) } -private func optionsCircleImage(dark: Bool) -> UIImage? { +func optionsCircleImage(dark: Bool) -> UIImage? { return generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -339,7 +339,7 @@ private func optionsRateImage(rate: String, isLarge: Bool, color: UIColor = .whi }) } -private final class MoreHeaderButton: HighlightableButtonNode { +final class MoreHeaderButton: HighlightableButtonNode { enum Content { case image(UIImage?) case more(UIImage?) @@ -2475,6 +2475,29 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } var items: [ContextMenuItem] = [] + + if let (message, _, _) = strongSelf.contentInfo() { + let context = strongSelf.context + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in + + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let strongSelf = self, let peer = peer else { + return + } + if let navigationController = strongSelf.baseNavigationController() { + strongSelf.beginCustomDismiss(true) + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: true, timecode: nil))) + + Queue.mainQueue().after(0.3) { + strongSelf.completeCustomDismiss() + } + } + f(.default) + }) + }))) + } var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal var speedIconText: String = "1x" @@ -2804,7 +2827,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } -private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { +final class HeaderContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController private let sourceNode: ContextReferenceContentNode diff --git a/submodules/ItemListUI/Sources/Items/ItemListActivityTextItem.swift b/submodules/ItemListUI/Sources/Items/ItemListActivityTextItem.swift index db68050cbc..414679cc4a 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListActivityTextItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListActivityTextItem.swift @@ -13,6 +13,7 @@ public class ItemListActivityTextItem: ListViewItem, ItemListItem { case generic case constructive case destructive + case warning } let displayActivity: Bool @@ -123,6 +124,8 @@ public class ItemListActivityTextItemNode: ListViewItemNode { textColor = item.presentationData.theme.list.freeTextSuccessColor case .destructive: textColor = item.presentationData.theme.list.freeTextErrorColor + case .warning: + textColor = UIColor(rgb: 0xef8c00) } let attributedString = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: textColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: textColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 7de263d94b..3b6186dab0 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -20,6 +20,8 @@ import ContextUI import FileMediaResourceStatus import ManagedAnimationNode import ShimmerEffect +import ComponentFlow +import EmojiStatusComponent private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:]) @@ -159,6 +161,167 @@ final class CachedChatListSearchResult { } public final class ListMessageFileItemNode: ListMessageNode { + public final class DescriptionNode: ASDisplayNode { + let descriptionNode: TextNode + var titleTopicArrowNode: ASImageNode? + var topicTitleNode: TextNode? + var titleTopicIconView: ComponentHostView? + var titleTopicIconComponent: EmojiStatusComponent? + + var visibilityStatus: Bool = false { + didSet { + if self.visibilityStatus != oldValue { + if let titleTopicIconView = self.titleTopicIconView, let titleTopicIconComponent = self.titleTopicIconComponent { + let _ = titleTopicIconView.update( + transition: .immediate, + component: AnyComponent(titleTopicIconComponent.withVisibleForAnimations(self.visibilityStatus)), + environment: {}, + containerSize: titleTopicIconView.bounds.size + ) + } + } + } + } + + override init() { + self.descriptionNode = TextNode() + self.descriptionNode.displaysAsynchronously = true + + super.init() + + self.addSubnode(self.descriptionNode) + } + + func asyncLayout() -> (_ context: AccountContext, _ constrainedWidth: CGFloat, _ theme: PresentationTheme, _ authorTitle: NSAttributedString?, _ topic: (title: NSAttributedString, iconId: Int64?, iconColor: Int32)?) -> (CGSize, () -> Void) { + let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode) + let makeTopicTitleLayout = TextNode.asyncLayout(self.topicTitleNode) + + return { [weak self] context, constrainedWidth, theme, authorTitle, topic in + var maxTitleWidth = constrainedWidth + if let _ = topic { + maxTitleWidth = floor(constrainedWidth * 0.7) + } + + let descriptionLayout = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: authorTitle, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + + var remainingWidth = constrainedWidth - descriptionLayout.0.size.width + + var topicTitleArguments: TextNodeLayoutArguments? + var arrowIconImage: UIImage? + if let topic = topic { + remainingWidth -= 22.0 + 2.0 + + if authorTitle != nil { + arrowIconImage = PresentationResourcesChatList.topicArrowIcon(theme) + if let arrowIconImage = arrowIconImage { + remainingWidth -= arrowIconImage.size.width + 6.0 * 2.0 + } + } + + topicTitleArguments = TextNodeLayoutArguments(attributedString: topic.title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: remainingWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)) + } + + let topicTitleLayout = topicTitleArguments.flatMap(makeTopicTitleLayout) + + var size = descriptionLayout.0.size + if let topicTitleLayout = topicTitleLayout { + size.height = max(size.height, topicTitleLayout.0.size.height) + size.width += 10.0 + topicTitleLayout.0.size.width + } + + return (size, { + guard let self else { + return + } + + let _ = descriptionLayout.1() + let authorFrame = CGRect(origin: CGPoint(), size: descriptionLayout.0.size) + self.descriptionNode.frame = authorFrame + + var nextX = authorFrame.maxX - 1.0 + if authorTitle == nil { + nextX = 0.0 + } + + if let arrowIconImage = arrowIconImage { + let titleTopicArrowNode: ASImageNode + if let current = self.titleTopicArrowNode { + titleTopicArrowNode = current + } else { + titleTopicArrowNode = ASImageNode() + self.titleTopicArrowNode = titleTopicArrowNode + self.addSubnode(titleTopicArrowNode) + } + titleTopicArrowNode.image = arrowIconImage + nextX += 6.0 + titleTopicArrowNode.frame = CGRect(origin: CGPoint(x: nextX, y: 5.0), size: arrowIconImage.size) + nextX += arrowIconImage.size.width + 6.0 + } else { + if let titleTopicArrowNode = self.titleTopicArrowNode { + self.titleTopicArrowNode = nil + titleTopicArrowNode.removeFromSupernode() + } + } + + if let topic { + let titleTopicIconView: ComponentHostView + if let current = self.titleTopicIconView { + titleTopicIconView = current + } else { + titleTopicIconView = ComponentHostView() + self.titleTopicIconView = titleTopicIconView + self.view.addSubview(titleTopicIconView) + } + + let titleTopicIconContent: EmojiStatusComponent.Content + if let fileId = topic.iconId, fileId != 0 { + titleTopicIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 36.0, height: 36.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(2)) + } else { + titleTopicIconContent = .topic(title: String(topic.title.string.prefix(1)), color: topic.iconColor, size: CGSize(width: 22.0, height: 22.0)) + } + + let titleTopicIconComponent = EmojiStatusComponent( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + content: titleTopicIconContent, + isVisibleForAnimations: self.visibilityStatus, + action: nil + ) + self.titleTopicIconComponent = titleTopicIconComponent + + let iconSize = titleTopicIconView.update( + transition: .immediate, + component: AnyComponent(titleTopicIconComponent), + environment: {}, + containerSize: CGSize(width: 22.0, height: 22.0) + ) + titleTopicIconView.frame = CGRect(origin: CGPoint(x: nextX, y: UIScreenPixel), size: iconSize) + nextX += iconSize.width + 2.0 + } else { + if let titleTopicIconView = self.titleTopicIconView { + self.titleTopicIconView = nil + titleTopicIconView.removeFromSuperview() + } + } + + if let topicTitleLayout = topicTitleLayout { + let topicTitleNode = topicTitleLayout.1() + if topicTitleNode.supernode == nil { + self.addSubnode(topicTitleNode) + self.topicTitleNode = topicTitleNode + } + + topicTitleNode.frame = CGRect(origin: CGPoint(x: nextX - 1.0, y: 0.0), size: topicTitleLayout.0.size) + } else if let topicTitleNode = self.topicTitleNode { + self.topicTitleNode = nil + topicTitleNode.removeFromSupernode() + } + }) + } + } + } + private let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode private let extractedBackgroundImageNode: ASImageNode @@ -377,6 +540,7 @@ public final class ListMessageFileItemNode: ListMessageNode { let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode) let textNodeMakeLayout = TextNode.asyncLayout(self.textNode) let descriptionNodeMakeLayout = TextNode.asyncLayout(self.descriptionNode) +// let newDescriptionNodeMakeLayout = self.descriptionNode.asyncLayout() let extensionIconTextMakeLayout = TextNode.asyncLayout(self.extensionIconText) let dateNodeMakeLayout = TextNode.asyncLayout(self.dateNode) let iconImageLayout = self.iconImageNode.asyncLayout() @@ -754,6 +918,9 @@ public final class ListMessageFileItemNode: ListMessageNode { let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) +// let forumThreadTitle: (title: NSAttributedString, iconId: Int64?, iconColor: Int32)? = nil +// let (newDescriptionNodeLayout, newDescriptionNodeApply) = newDescriptionNodeMakeLayout(item.context, params.width - leftInset - rightInset - 30.0, item.presentationData.theme.theme, descriptionText, forumThreadTitle) + var (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(TextNodeLayoutArguments(attributedString: extensionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) if extensionTextLayout.truncated, let text = extensionText?.string { extensionText = NSAttributedString(string: text, font: smallExtensionFont, textColor: .white, paragraphAlignment: .center) diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index d71e5880a1..dab8cc8e02 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -42,8 +42,9 @@ private final class ChannelVisibilityControllerArguments { let toggleApproveMembers: (Bool) -> Void let activateLink: (String) -> Void let deactivateLink: (String) -> Void + let openAuction: (String) -> Void - init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, linkContextAction: @escaping (ASDisplayNode, ContextGesture?) -> Void, manageInviteLinks: @escaping () -> Void, openLink: @escaping (ExportedInvitation) -> Void, toggleForwarding: @escaping (Bool) -> Void, updateJoinToSend: @escaping (CurrentChannelJoinToSend) -> Void, toggleApproveMembers: @escaping (Bool) -> Void, activateLink: @escaping (String) -> Void, deactivateLink: @escaping (String) -> Void) { + init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, linkContextAction: @escaping (ASDisplayNode, ContextGesture?) -> Void, manageInviteLinks: @escaping () -> Void, openLink: @escaping (ExportedInvitation) -> Void, toggleForwarding: @escaping (Bool) -> Void, updateJoinToSend: @escaping (CurrentChannelJoinToSend) -> Void, toggleApproveMembers: @escaping (Bool) -> Void, activateLink: @escaping (String) -> Void, deactivateLink: @escaping (String) -> Void, openAuction: @escaping (String) -> Void) { self.context = context self.updateCurrentType = updateCurrentType self.updatePublicLinkText = updatePublicLinkText @@ -60,6 +61,7 @@ private final class ChannelVisibilityControllerArguments { self.toggleApproveMembers = toggleApproveMembers self.activateLink = activateLink self.deactivateLink = deactivateLink + self.openAuction = openAuction } } @@ -109,7 +111,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { case privateLinkManageInfo(PresentationTheme, String) case publicLinkInfo(PresentationTheme, String) - case publicLinkStatus(PresentationTheme, String, AddressNameValidationStatus) + case publicLinkStatus(PresentationTheme, String, AddressNameValidationStatus, String) case existingLinksInfo(PresentationTheme, String) case existingLinkPeerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, ItemListPeerItemEditing, Bool) @@ -317,8 +319,8 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { } else { return false } - case let .publicLinkStatus(lhsTheme, lhsText, lhsStatus): - if case let .publicLinkStatus(rhsTheme, rhsText, rhsStatus) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStatus == rhsStatus { + case let .publicLinkStatus(lhsTheme, lhsText, lhsStatus, lhsUsername): + if case let .publicLinkStatus(rhsTheme, rhsText, rhsStatus, rhsUsername) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStatus == rhsStatus, lhsUsername == rhsUsername { return true } else { return false @@ -671,7 +673,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .publicLinkInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) - case let .publicLinkStatus(_, text, status): + case let .publicLinkStatus(_, text, status, username): var displayActivity = false let textColor: ItemListActivityTextItem.TextColor switch status { @@ -686,13 +688,15 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { case .taken: textColor = .destructive case .purchaseAvailable: - textColor = .generic + textColor = .warning } case .checking: textColor = .generic displayActivity = true } - return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: text, color: textColor, linkAction: { _ in }, sectionId: self.section) + return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: text, color: textColor, linkAction: { _ in + arguments.openAuction(username) + }, sectionId: self.section) case let .existingLinksInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .existingLinkPeerItem(_, _, _, dateTimeFormat, nameDisplayOrder, peer, editing, enabled): @@ -1074,7 +1078,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa text = presentationData.strings.Channel_Username_CheckingUsername } - entries.append(.publicLinkStatus(presentationData.theme, text, status)) + entries.append(.publicLinkStatus(presentationData.theme, text, status, currentUsername)) } if isGroup { if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil { @@ -1275,7 +1279,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa text = presentationData.strings.Channel_Username_CheckingUsername } - entries.append(.publicLinkStatus(presentationData.theme, text, status)) + entries.append(.publicLinkStatus(presentationData.theme, text, status, currentUsername)) } entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp)) @@ -1742,6 +1746,10 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta let _ = context.engine.peers.toggleAddressNameActive(domain: .peer(peerId), name: name, active: false).start() })]), nil) }) + }, openAuction: { username in + dismissInputImpl?() + + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://fragment.com/username/\(username)", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) }) let peerView = context.account.viewTracker.peerView(peerId) diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 28f6b09005..efee1944ae 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -252,7 +252,7 @@ final class PageComponent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 40.0)) ) context.add(text - .position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 80.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 60.0 + text.size.height / 2.0)) ) context.add(content .position(CGPoint(x: content.size.width / 2.0, y: content.size.height / 2.0)) @@ -972,6 +972,10 @@ private final class DemoSheetContent: CombinedComponent { buttonText = strings.Premium_Gift_GiftSubscription(price ?? "–").string case .other: switch component.subject { + case .fasterDownload: + buttonText = strings.Premium_FasterSpeed_Proceed + case .advancedChatManagement: + buttonText = strings.Premium_ChatManagement_Proceed case .uniqueReactions: buttonText = strings.Premium_Reactions_Proceed buttonAnimationName = "premium_unlock" @@ -1030,6 +1034,10 @@ private final class DemoSheetContent: CombinedComponent { var contentHeight: CGFloat = context.availableSize.width + 146.0 if case .other = component.source { contentHeight -= 40.0 + + if [.advancedChatManagement, .fasterDownload].contains(component.subject) { + contentHeight += 20.0 + } } let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 20.0), size: button.size) diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index e25230a7af..855f46ccee 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -159,6 +159,12 @@ public enum PremiumSource: Equatable { } else { return false } + case .fasterDownload: + if case .fasterDownload = rhs { + return true + } else { + return false + } } } @@ -184,6 +190,7 @@ public enum PremiumSource: Equatable { case gift(from: PeerId, to: PeerId, duration: Int32) case giftTerms case voiceToText + case fasterDownload var identifier: String? { switch self { @@ -225,6 +232,8 @@ public enum PremiumSource: Equatable { return "emoji_status" case .voiceToText: return "voice_to_text" + case .fasterDownload: + return "faster_download" case .gift, .giftTerms: return nil case let .deeplink(reference): diff --git a/submodules/SettingsUI/Sources/UsernameSetupController.swift b/submodules/SettingsUI/Sources/UsernameSetupController.swift index 96bd2c9301..f4c2ff96e3 100644 --- a/submodules/SettingsUI/Sources/UsernameSetupController.swift +++ b/submodules/SettingsUI/Sources/UsernameSetupController.swift @@ -15,19 +15,19 @@ import TextFormat private final class UsernameSetupControllerArguments { let account: Account - let updatePublicLinkText: (String?, String) -> Void let shareLink: () -> Void - let activateLink: (String) -> Void let deactivateLink: (String) -> Void + let openAuction: (String) -> Void - init(account: Account, updatePublicLinkText: @escaping (String?, String) -> Void, shareLink: @escaping () -> Void, activateLink: @escaping (String) -> Void, deactivateLink: @escaping (String) -> Void) { + init(account: Account, updatePublicLinkText: @escaping (String?, String) -> Void, shareLink: @escaping () -> Void, activateLink: @escaping (String) -> Void, deactivateLink: @escaping (String) -> Void, openAuction: @escaping (String) -> Void) { self.account = account self.updatePublicLinkText = updatePublicLinkText self.shareLink = shareLink self.activateLink = activateLink self.deactivateLink = deactivateLink + self.openAuction = openAuction } } @@ -56,7 +56,7 @@ private enum UsernameSetupEntryId: Hashable { private enum UsernameSetupEntry: ItemListNodeEntry { case publicLinkHeader(PresentationTheme, String) case editablePublicLink(PresentationTheme, PresentationStrings, String, String?, String) - case publicLinkStatus(PresentationTheme, String, AddressNameValidationStatus, String) + case publicLinkStatus(PresentationTheme, String, AddressNameValidationStatus, String, String) case publicLinkInfo(PresentationTheme, String) case additionalLinkHeader(PresentationTheme, String) @@ -111,8 +111,8 @@ private enum UsernameSetupEntry: ItemListNodeEntry { } else { return false } - case let .publicLinkStatus(lhsTheme, lhsAddressName, lhsStatus, lhsText): - if case let .publicLinkStatus(rhsTheme, rhsAddressName, rhsStatus, rhsText) = rhs, lhsTheme === rhsTheme, lhsAddressName == rhsAddressName, lhsStatus == rhsStatus, lhsText == rhsText { + case let .publicLinkStatus(lhsTheme, lhsAddressName, lhsStatus, lhsText, lhsUsername): + if case let .publicLinkStatus(rhsTheme, rhsAddressName, rhsStatus, rhsText, rhsUsername) = rhs, lhsTheme === rhsTheme, lhsAddressName == rhsAddressName, lhsStatus == rhsStatus, lhsText == rhsText, lhsUsername == rhsUsername { return true } else { return false @@ -208,7 +208,7 @@ private enum UsernameSetupEntry: ItemListNodeEntry { arguments.shareLink() } }) - case let .publicLinkStatus(_, _, status, text): + case let .publicLinkStatus(_, _, status, text, username): var displayActivity = false let textColor: ItemListActivityTextItem.TextColor switch status { @@ -219,7 +219,7 @@ private enum UsernameSetupEntry: ItemListNodeEntry { case .available: textColor = .constructive case .purchaseAvailable: - textColor = .generic + textColor = .warning case .invalid, .taken: textColor = .destructive } @@ -227,7 +227,9 @@ private enum UsernameSetupEntry: ItemListNodeEntry { textColor = .generic displayActivity = true } - return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: text, color: textColor, linkAction: { _ in }, sectionId: self.section) + return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: text, color: textColor, linkAction: { _ in + arguments.openAuction(username) + }, sectionId: self.section) case let .additionalLinkHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .additionalLink(_, link, _): @@ -343,7 +345,7 @@ private func usernameSetupControllerEntries(presentationData: PresentationData, case .checking: statusText = presentationData.strings.Username_CheckingUsername } - entries.append(.publicLinkStatus(presentationData.theme, currentUsername, status, statusText)) + entries.append(.publicLinkStatus(presentationData.theme, currentUsername, status, statusText, currentUsername)) } var infoText = presentationData.strings.Username_Help @@ -471,6 +473,10 @@ public func usernameSetupController(context: AccountContext) -> ViewController { presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Username_DeactivateAlertTitle, text: presentationData.strings.Username_DeactivateAlertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Username_DeactivateAlertHide, action: { let _ = context.engine.peers.toggleAddressNameActive(domain: .account, name: name, active: false).start() })]), nil) + }, openAuction: { username in + dismissInputImpl?() + + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://fragment.com/username/\(username)", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) }) let temporaryOrder = Promise<[String]?>(nil) @@ -680,6 +686,5 @@ public func usernameSetupController(context: AccountContext) -> ViewController { presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } - return controller } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 8c7c01f4e4..e90fbe46c7 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -112,6 +112,10 @@ public enum PresentationResourceKey: Int32 { case chatBubbleVerticalLineIncomingImage case chatBubbleVerticalLineOutgoingImage + case chatBubbleArrowFreeImage + case chatBubbleArrowIncomingImage + case chatBubbleArrowOutgoingImage + case chatBubbleCheckBubbleFullImage case chatBubbleBubblePartialImage case checkBubbleMediaFullImage diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 036ea6c492..ec200e13fb 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -122,6 +122,28 @@ public struct PresentationResourcesChat { }) } + public static func chatBubbleArrowImage(color: UIColor) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/HeaderArrow"), color: color) + } + + public static func chatBubbleArrowFreeImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleArrowFreeImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/HeaderArrow"), color: UIColor(white: 1.0, alpha: 0.3)) + }) + } + + public static func chatBubbleArrowIncomingImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleArrowIncomingImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/HeaderArrow"), color: theme.chat.message.incoming.accentTextColor.withAlphaComponent(0.3)) + }) + } + + public static func chatBubbleArrowOutgoingImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleArrowOutgoingImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/HeaderArrow"), color: theme.chat.message.outgoing.accentTextColor.withAlphaComponent(0.3)) + }) + } + public static func chatBubbleConsumableContentIncomingIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatBubbleConsumableContentIncomingIcon.rawValue, { theme in return generateFilledCircleImage(diameter: 4.0, color: theme.chat.message.incoming.accentTextColor) diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index a1d8482277..db68f8935d 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -224,20 +224,7 @@ public final class EmojiStatusComponent: Component { iconImage = nil } case let .topic(title, color, realSize): - func generateTopicColors(_ color: Int32) -> ([UInt32], [UInt32]) { - return ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]) - } - - let topicColors: [Int32: ([UInt32], [UInt32])] = [ - 0x6FB9F0: ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]), - 0xFFD67E: ([0xFFD67E, 0xFC8601], [0xDA9400, 0xFA5F00]), - 0xCB86DB: ([0xCB86DB, 0x9338AF], [0x812E98, 0x6F2B87]), - 0x8EEE98: ([0x8EEE98, 0x02B504], [0x02A01B, 0x009716]), - 0xFF93B2: ([0xFF93B2, 0xE23264], [0xFC447A, 0xC80C46]), - 0xFB6F5F: ([0xFB6F5F, 0xD72615], [0xDC1908, 0xB61506]) - ] - let colors = topicColors[color] ?? generateTopicColors(color) - + let colors = topicIconColors(for: color) if let image = generateTopicIcon(title: title, backgroundColors: colors.0.map(UIColor.init(rgb:)), strokeColors: colors.1.map(UIColor.init(rgb:)), size: realSize) { iconImage = image } else { @@ -575,3 +562,16 @@ public final class EmojiStatusComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +public func topicIconColors(for color: Int32) -> ([UInt32], [UInt32]) { + let topicColors: [Int32: ([UInt32], [UInt32])] = [ + 0x6FB9F0: ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]), + 0xFFD67E: ([0xFFD67E, 0xFC8601], [0xDA9400, 0xFA5F00]), + 0xCB86DB: ([0xCB86DB, 0x9338AF], [0x812E98, 0x6F2B87]), + 0x8EEE98: ([0x8EEE98, 0x02B504], [0x02A01B, 0x009716]), + 0xFF93B2: ([0xFF93B2, 0xE23264], [0xFC447A, 0xC80C46]), + 0xFB6F5F: ([0xFB6F5F, 0xD72615], [0xDC1908, 0xB61506]) + ] + + return topicColors[color] ?? ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]) +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 15f0ac595b..07b6ba95ab 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -2944,7 +2944,7 @@ public final class EmojiPagerContentComponent: Component { image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) } case let .topic(title, color): - let colors = self.getTopicColors(color) + let colors = topicIconColors(for: color) if let image = generateTopicIcon(backgroundColors: colors.0.map { UIColor(rgb: $0) }, strokeColors: colors.1.map { UIColor(rgb: $0) }, title: title) { let imageSize = image.size//.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0)) image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) @@ -2993,19 +2993,6 @@ public final class EmojiPagerContentComponent: Component { return nullAction } - private func getTopicColors(_ color: Int32) -> ([UInt32], [UInt32]) { - let topicColors: [Int32: ([UInt32], [UInt32])] = [ - 0x6FB9F0: ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]), - 0xFFD67E: ([0xFFD67E, 0xFC8601], [0xDA9400, 0xFA5F00]), - 0xCB86DB: ([0xCB86DB, 0x9338AF], [0x812E98, 0x6F2B87]), - 0x8EEE98: ([0x8EEE98, 0x02B504], [0x02A01B, 0x009716]), - 0xFF93B2: ([0xFF93B2, 0xE23264], [0xFC447A, 0xC80C46]), - 0xFB6F5F: ([0xFB6F5F, 0xD72615], [0xDC1908, 0xB61506]) - ] - - return topicColors[color] ?? ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]) - } - func update(content: ItemContent) { if self.content != content { if case let .icon(icon) = content, case let .topic(title, color) = icon { @@ -3014,7 +3001,7 @@ public final class EmojiPagerContentComponent: Component { UIGraphicsPushContext(context) - let colors = self.getTopicColors(color) + let colors = topicIconColors(for: color) if let image = generateTopicIcon(backgroundColors: colors.0.map { UIColor(rgb: $0) }, strokeColors: colors.1.map { UIColor(rgb: $0) }, title: title) { let imageSize = image.size image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 327dec4e13..b8349516ac 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -430,8 +430,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { self.title = "" self.fileId = 0 - let colors: [Int32] = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98,0xFF93B2, 0xFB6F5F] - self.iconColor = colors.randomElement() ?? 0x0 + self.iconColor = ForumCreateTopicScreen.iconColors.randomElement() ?? 0x0 case let .edit(info): self.title = info.title self.fileId = info.icon ?? 0 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Speed.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Speed.imageset/Contents.json new file mode 100644 index 0000000000..c2c112f2ac --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Speed.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "iOS Speed.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Speed.imageset/iOS Speed.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Speed.imageset/iOS Speed.pdf new file mode 100644 index 0000000000..a3975add35 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Speed.imageset/iOS Speed.pdf @@ -0,0 +1,150 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.005127 2.995117 cm +0.000000 0.000000 0.000000 scn +1.330000 9.000000 m +1.330000 13.236024 4.763976 16.670000 9.000000 16.670000 c +13.236024 16.670000 16.670000 13.236024 16.670000 9.000000 c +16.670000 4.763976 13.236024 1.330000 9.000000 1.330000 c +4.763976 1.330000 1.330000 4.763976 1.330000 9.000000 c +h +9.000000 18.000000 m +4.029438 18.000000 0.000000 13.970562 0.000000 9.000000 c +0.000000 4.029437 4.029438 -0.000002 9.000000 -0.000002 c +13.970563 -0.000002 18.000002 4.029437 18.000002 9.000000 c +18.000002 13.970562 13.970563 18.000000 9.000000 18.000000 c +h +6.012677 4.763371 m +5.846505 4.591045 5.641757 4.504883 5.398434 4.504883 c +5.161045 4.504883 4.956297 4.591045 4.784191 4.763371 c +4.612084 4.941640 4.526031 5.149619 4.526031 5.387310 c +4.526031 5.625002 4.612084 5.830010 4.784191 6.002336 c +4.956297 6.174662 5.161045 6.260825 5.398434 6.260825 c +5.641757 6.260825 5.846505 6.174662 6.012677 6.002336 c +6.184784 5.830010 6.270837 5.625002 6.270837 5.387310 c +6.270837 5.149619 6.184784 4.941640 6.012677 4.763371 c +h +4.490422 8.355477 m +4.318315 8.183151 4.110600 8.096988 3.867277 8.096988 c +3.629888 8.096988 3.425140 8.183151 3.253033 8.355477 c +3.080926 8.527802 2.994873 8.732811 2.994873 8.970503 c +2.994873 9.214136 3.080926 9.419145 3.253033 9.585527 c +3.425140 9.757854 3.629888 9.844017 3.867277 9.844017 c +4.110600 9.844017 4.318315 9.757854 4.490422 9.585527 c +4.662529 9.419145 4.748582 9.214136 4.748582 8.970503 c +4.748582 8.732811 4.662529 8.527802 4.490422 8.355477 c +h +5.994873 11.938669 m +5.828701 11.766342 5.623953 11.680180 5.380630 11.680180 c +5.143241 11.680180 4.938493 11.766342 4.766387 11.938669 c +4.594280 12.110994 4.508226 12.316004 4.508226 12.553694 c +4.508226 12.797327 4.594280 13.002337 4.766387 13.168720 c +4.938493 13.341045 5.143241 13.427209 5.380630 13.427209 c +5.623953 13.427209 5.828701 13.341045 5.994873 13.168720 c +6.166980 13.002337 6.253033 12.797327 6.253033 12.553694 c +6.253033 12.316004 6.166980 12.110994 5.994873 11.938669 c +h +9.609117 13.507429 m +9.437010 13.335104 9.232262 13.248940 8.994873 13.248940 c +8.751550 13.248940 8.543835 13.335104 8.371728 13.507429 c +8.205556 13.679756 8.122470 13.884764 8.122470 14.122455 c +8.122470 14.366088 8.205556 14.574068 8.371728 14.746394 c +8.543835 14.918720 8.751550 15.004883 8.994873 15.004883 c +9.232262 15.004883 9.437010 14.918720 9.609117 14.746394 c +9.781223 14.574068 9.867277 14.366088 9.867277 14.122455 c +9.867277 13.884764 9.781223 13.679756 9.609117 13.507429 c +h +14.736713 8.355477 m +14.564607 8.183151 14.359859 8.096988 14.122470 8.096988 c +13.879147 8.096988 13.671432 8.183151 13.499325 8.355477 c +13.333152 8.527802 13.250066 8.732811 13.250066 8.970503 c +13.250066 9.214136 13.333152 9.419145 13.499325 9.585527 c +13.671432 9.757854 13.879147 9.844017 14.122470 9.844017 c +14.359859 9.844017 14.564607 9.757854 14.736713 9.585527 c +14.908820 9.419145 14.994873 9.214136 14.994873 8.970503 c +14.994873 8.732811 14.908820 8.527802 14.736713 8.355477 c +h +13.205556 4.763371 m +13.033449 4.591045 12.828701 4.504883 12.591312 4.504883 c +12.353924 4.504883 12.149176 4.591045 11.977069 4.763371 c +11.804962 4.941640 11.718909 5.149619 11.718909 5.387310 c +11.718909 5.625002 11.804962 5.830010 11.977069 6.002336 c +12.149176 6.174662 12.353924 6.260825 12.591312 6.260825 c +12.828701 6.260825 13.033449 6.174662 13.205556 6.002336 c +13.377663 5.830010 13.463717 5.625002 13.463717 5.387310 c +13.463717 5.149619 13.377663 4.941640 13.205556 4.763371 c +h +8.362825 7.169993 m +8.083894 7.217531 7.822766 7.360146 7.579443 7.597836 c +7.342054 7.841470 7.196653 8.102930 7.143241 8.382217 c +7.095763 8.667446 7.134339 8.943762 7.258968 9.211164 c +7.383597 9.484509 7.585378 9.719229 7.864309 9.915324 c +12.769354 13.355902 l +12.917722 13.456921 13.057188 13.483660 13.187752 13.436122 c +13.318316 13.394526 13.407336 13.308363 13.454814 13.177633 c +13.502292 13.046904 13.472618 12.907259 13.365793 12.758703 c +9.885081 7.891979 l +9.683301 7.612693 9.448879 7.410655 9.181817 7.285868 c +8.914755 7.161079 8.641757 7.122455 8.362825 7.169993 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 4148 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000004238 00000 n +0000004261 00000 n +0000004434 00000 n +0000004508 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +4567 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/HeaderArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/HeaderArrow.imageset/Contents.json new file mode 100644 index 0000000000..7d40464891 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/HeaderArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/HeaderArrow.imageset/arrow.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/HeaderArrow.imageset/arrow.pdf new file mode 100644 index 0000000000..9189e9a34c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/HeaderArrow.imageset/arrow.pdf @@ -0,0 +1,92 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.000000 -0.459961 cm +0.000000 0.000000 0.000000 scn +0.470226 9.930187 m +0.210527 10.189886 -0.210527 10.189886 -0.470226 9.930187 c +-0.729925 9.670488 -0.729925 9.249434 -0.470226 8.989735 c +0.470226 9.930187 l +h +4.000000 5.459961 m +4.470226 4.989735 l +4.729925 5.249434 4.729925 5.670488 4.470226 5.930187 c +4.000000 5.459961 l +h +-0.470226 1.930187 m +-0.729925 1.670488 -0.729925 1.249434 -0.470226 0.989735 c +-0.210527 0.730036 0.210527 0.730036 0.470226 0.989735 c +-0.470226 1.930187 l +h +-0.470226 8.989735 m +3.529774 4.989735 l +4.470226 5.930187 l +0.470226 9.930187 l +-0.470226 8.989735 l +h +3.529774 5.930187 m +-0.470226 1.930187 l +0.470226 0.989735 l +4.470226 4.989735 l +3.529774 5.930187 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 772 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 8.000000 10.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000862 00000 n +0000000884 00000 n +0000001056 00000 n +0000001130 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1189 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index bd7eb167ed..78819a748f 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1820,6 +1820,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self?.navigateToMessage(from: fromId, to: .id(id, nil), forceInCurrentChat: fromId.peerId == id.peerId) }, navigateToMessageStandalone: { [weak self] id in self?.navigateToMessage(from: nil, to: .id(id, nil), forceInCurrentChat: false) + }, navigateToThreadMessage: { [weak self] peerId, threadId, messageId in + if let context = self?.context, let navigationController = self?.effectiveNavigationController { + let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, keepStack: .always).start() + } }, tapMessage: nil, clickThroughMessage: { [weak self] in self?.chatDisplayNode.dismissInput() }, toggleMessagesSelection: { [weak self] ids, value in diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 26fed4917d..e67a55c530 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -70,6 +70,7 @@ public final class ChatControllerInteraction { let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void let navigateToMessage: (MessageId, MessageId) -> Void let navigateToMessageStandalone: (MessageId) -> Void + let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void let tapMessage: ((Message) -> Void)? let clickThroughMessage: () -> Void let toggleMessagesSelection: ([MessageId], Bool) -> Void @@ -178,6 +179,7 @@ public final class ChatControllerInteraction { openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, navigateToMessageStandalone: @escaping (MessageId) -> Void, + navigateToThreadMessage: @escaping (PeerId, Int64, MessageId?) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, @@ -269,6 +271,7 @@ public final class ChatControllerInteraction { self.openMessageContextActions = openMessageContextActions self.navigateToMessage = navigateToMessage self.navigateToMessageStandalone = navigateToMessageStandalone + self.navigateToThreadMessage = navigateToThreadMessage self.tapMessage = tapMessage self.clickThroughMessage = clickThroughMessage self.toggleMessagesSelection = toggleMessagesSelection diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index ea8f5b1c54..12944a3aa2 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -614,7 +614,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let readCounters = context.engine.data.get(TelegramEngine.EngineData.Item.Messages.PeerReadCounters(id: messages[0].id.peerId)) - let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], InfoSummaryData, AppConfiguration, Bool, Int32, AvailableReactions?, TranslationSettings, LoggingSettings, NotificationSoundList?), NoError> = combineLatest( + let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], InfoSummaryData, AppConfiguration, Bool, Int32, AvailableReactions?, TranslationSettings, LoggingSettings, NotificationSoundList?, EnginePeer?), NoError> = combineLatest( loadLimits, loadStickerSaveStatusSignal, loadResourceStatusSignal, @@ -626,9 +626,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState ApplicationSpecificNotice.getMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager), context.engine.stickers.availableReactions(), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings, SharedDataKeys.loggingSettings]), - context.engine.peers.notificationSoundList() |> take(1) + context.engine.peers.notificationSoundList() |> take(1), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) ) - |> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, infoSummaryData, readCounters, messageViewsPrivacyTips, availableReactions, sharedData, notificationSoundList -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], InfoSummaryData, AppConfiguration, Bool, Int32, AvailableReactions?, TranslationSettings, LoggingSettings, NotificationSoundList?) in + |> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, infoSummaryData, readCounters, messageViewsPrivacyTips, availableReactions, sharedData, notificationSoundList, accountPeer -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], InfoSummaryData, AppConfiguration, Bool, Int32, AvailableReactions?, TranslationSettings, LoggingSettings, NotificationSoundList?, EnginePeer?) in let (limitsConfiguration, appConfig) = limitsAndAppConfig var canEdit = false if !isAction { @@ -652,12 +653,14 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState loggingSettings = LoggingSettings.defaultSettings } - return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, infoSummaryData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings, loggingSettings, notificationSoundList) + return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, infoSummaryData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings, loggingSettings, notificationSoundList, accountPeer) } return dataSignal |> deliverOnMainQueue - |> map { data, updatingMessageMedia, infoSummaryData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings, loggingSettings, notificationSoundList -> ContextController.Items in + |> map { data, updatingMessageMedia, infoSummaryData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings, loggingSettings, notificationSoundList, accountPeer -> ContextController.Items in + let isPremium = accountPeer?.isPremium ?? false + var actions: [ContextMenuItem] = [] var isPinnedMessages = false @@ -794,7 +797,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string)) } else { let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file)) - |> deliverOnMainQueue).start(completed: { + |> deliverOnMainQueue).start(completed: { controllerInteraction.displayUndo(.notificationSoundAdded(title: presentationData.strings.Notifications_UploadSuccess_Title, text: presentationData.strings.Notifications_SaveSuccess_Text, action: { controllerInteraction.navigationController()?.pushViewController(notificationsAndSoundsController(context: context, exceptionsList: nil)) })) @@ -937,6 +940,53 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } + var isDownloading = false + let resourceAvailable: Bool + if let resourceStatus = data.resourceStatus { + if case .Local = resourceStatus { + resourceAvailable = true + } else { + resourceAvailable = false + } + if case .Fetching = resourceStatus { + isDownloading = true + } + } else { + resourceAvailable = false + } + + + if !isPremium && isDownloading { + var isLargeFile = false + for media in message.media { + if let file = media as? TelegramMediaFile { + if let size = file.size, size >= 300 * 1024 * 1024 { + isLargeFile = true + } + break + } + } + if isLargeFile { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_IncreaseSpeed, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Speed"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + let context = context + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumDemoScreen(context: context, subject: .fasterDownload, action: { + let controller = PremiumIntroScreen(context: context, source: .fasterDownload) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + controllerInteraction.navigationController()?.pushViewController(controller) + + f(.dismissWithoutContent) + }))) + actions.append(.separator) + } + } + var isReplyThreadHead = false if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation { isReplyThreadHead = messages[0].id == replyThreadMessage.effectiveTopId @@ -970,12 +1020,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }))) } - let resourceAvailable: Bool - if let resourceStatus = data.resourceStatus, case .Local = resourceStatus { - resourceAvailable = true - } else { - resourceAvailable = false - } + var messageText: String = "" for message in messages { diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index e08be944a7..8510f5b4f0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -58,8 +58,8 @@ extension SlotMachineAnimationNode: GenericAnimatedStickerNode { class ChatMessageShareButton: HighlightableButtonNode { private var backgroundContent: WallpaperBubbleBackgroundNode? - private let backgroundNode: NavigationBackgroundNode + private let iconNode: ASImageNode private var iconOffset = CGPoint() @@ -242,7 +242,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var viaBotNode: TextNode? private let dateAndStatusNode: ChatMessageDateAndStatusNode + private var threadInfoNode: ChatMessageThreadInfoNode? private var replyInfoNode: ChatMessageReplyInfoNode? + private var replyBackgroundContent: WallpaperBubbleBackgroundNode? private var replyBackgroundNode: NavigationBackgroundNode? private var forwardInfoNode: ChatMessageForwardInfoNode? @@ -472,6 +474,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.updateVisibility() self.haptic?.enabled = self.visibilityStatus == true + self.threadInfoNode?.visibility = self.visibilityStatus == true self.replyInfoNode?.visibility = self.visibilityStatus == true } } @@ -789,6 +792,30 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { backgroundNode.update(rect: CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize, transition: .immediate) } + if let threadInfoNode = self.threadInfoNode { + var threadInfoNodeFrame = threadInfoNode.frame + threadInfoNodeFrame.origin.x += rect.minX + threadInfoNodeFrame.origin.y += rect.minY + + threadInfoNode.updateAbsoluteRect(threadInfoNodeFrame, within: containerSize) + } + + if let shareButtonNode = self.shareButtonNode { + var shareButtonNodeFrame = shareButtonNode.frame + shareButtonNodeFrame.origin.x += rect.minX + shareButtonNodeFrame.origin.y += rect.minY + + shareButtonNode.updateAbsoluteRect(shareButtonNodeFrame, within: containerSize) + } + + if let actionButtonsNode = self.actionButtonsNode { + var actionButtonsNodeFrame = actionButtonsNode.frame + actionButtonsNodeFrame.origin.x += rect.minX + actionButtonsNodeFrame.origin.y += rect.minY + + actionButtonsNode.updateAbsoluteRect(actionButtonsNodeFrame, within: containerSize) + } + if let reactionButtonsNode = self.reactionButtonsNode { var reactionButtonsNodeFrame = reactionButtonsNode.frame reactionButtonsNodeFrame.origin.x += rect.minX @@ -796,6 +823,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { reactionButtonsNode.update(rect: rect, within: containerSize, transition: .immediate) } + + if let replyBackgroundContent = self.replyBackgroundContent { + var replyBackgroundContentFrame = replyBackgroundContent.frame + replyBackgroundContentFrame.origin.x += rect.minX + replyBackgroundContentFrame.origin.y += rect.minY + + replyBackgroundContent.update(rect: rect, within: containerSize, transition: .immediate) + } } } @@ -855,6 +890,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) let viaBotLayout = TextNode.asyncLayout(self.viaBotNode) + let makeThreadInfoLayout = ChatMessageThreadInfoNode.asyncLayout(self.threadInfoNode) let makeReplyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let currentShareButtonNode = self.shareButtonNode let currentForwardInfo = self.appliedForwardInfo @@ -1124,11 +1160,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) var viaBotApply: (TextNodeLayout, () -> TextNode)? + var threadInfoApply: (CGSize, (Bool) -> ChatMessageThreadInfoNode)? var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? var needsReplyBackground = false var replyMarkup: ReplyMarkupMessageAttribute? - let availableContentWidth = min(120.0, max(60.0, params.width - params.leftInset - params.rightInset - max(imageSize.width, 160.0) - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left)) var ignoreForward = false @@ -1166,8 +1202,30 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { + var hasReply = true + if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId { - } else { + hasReply = false + } else if let threadId = item.message.threadId, Int64(replyMessage.id.id) == threadId, let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum) { + hasReply = false + } + + if case .peer = item.chatLocation, replyMessage.threadId != nil { + threadInfoApply = makeThreadInfoLayout(ChatMessageThreadInfoNode.Arguments( + presentationData: item.presentationData, + strings: item.presentationData.strings, + context: item.context, + controllerInteraction: item.controllerInteraction, + type: .standalone, + message: replyMessage, + parentMessage: item.message, + constrainedSize: CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer + )) + } + + if hasReply { replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments( presentationData: item.presentationData, strings: item.presentationData.strings, @@ -1425,9 +1483,41 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.replyBackgroundNode = replyBackgroundNode strongSelf.contextSourceNode.contentNode.addSubnode(replyBackgroundNode) } + + if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { + if strongSelf.replyBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + strongSelf.replyBackgroundContent = backgroundContent + strongSelf.insertSubnode(backgroundContent, at: 0) + } + } else { + strongSelf.replyBackgroundContent?.removeFromSupernode() + strongSelf.replyBackgroundContent = nil + } } else if let replyBackgroundNode = strongSelf.replyBackgroundNode { strongSelf.replyBackgroundNode = nil replyBackgroundNode.removeFromSupernode() + + if let replyBackgroundContent = strongSelf.replyBackgroundContent { + replyBackgroundContent.removeFromSupernode() + strongSelf.replyBackgroundContent = nil + } + } + + var headersOffset: CGFloat = 0.0 + if let (threadInfoSize, threadInfoApply) = threadInfoApply { + let threadInfoNode = threadInfoApply(synchronousLoads) + if strongSelf.threadInfoNode == nil { + strongSelf.threadInfoNode = threadInfoNode + strongSelf.contextSourceNode.contentNode.addSubnode(threadInfoNode) + } + let threadInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 6.0) : (params.width - params.rightInset - threadInfoSize.width - layoutConstants.bubble.edgeInset - 8.0)), y: 8.0), size: threadInfoSize) + threadInfoNode.frame = threadInfoFrame + + headersOffset += threadInfoSize.height + 10.0 + } else if let replyInfoNode = strongSelf.replyInfoNode { + replyInfoNode.removeFromSupernode() + strongSelf.replyInfoNode = nil } var messageInfoSize = CGSize() @@ -1447,7 +1537,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.viaBotNode = viaBotNode strongSelf.contextSourceNode.contentNode.addSubnode(viaBotNode) } - let viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: 8.0), size: viaBotLayout.size) + let viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0), size: viaBotLayout.size) viaBotNode.frame = viaBotFrame messageInfoSize = CGSize(width: messageInfoSize.width, height: viaBotLayout.size.height) @@ -1466,7 +1556,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { forwardInfoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } - let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 8.0)), y: 8.0 + messageInfoSize.height), size: forwardInfoSize) + let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 8.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: forwardInfoSize) forwardInfoNode.frame = forwardInfoFrame messageInfoSize = CGSize(width: messageInfoSize.width, height: messageInfoSize.height + forwardInfoSize.height - 1.0) @@ -1490,7 +1580,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.replyInfoNode = replyInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: 8.0 + messageInfoSize.height), size: replyInfoSize) + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: replyInfoSize) replyInfoNode.frame = replyInfoFrame messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: messageInfoSize.height + replyInfoSize.height) @@ -1500,13 +1590,30 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if let replyBackgroundNode = strongSelf.replyBackgroundNode { - replyBackgroundNode.frame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)) - 4.0, y: 6.0), size: CGSize(width: messageInfoSize.width + 8.0, height: messageInfoSize.height + 5.0)) + replyBackgroundNode.frame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)) - 4.0, y: headersOffset + 6.0), size: CGSize(width: messageInfoSize.width + 8.0, height: messageInfoSize.height + 5.0)) let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: cornerRadius, transition: .immediate) + + if let backgroundContent = strongSelf.replyBackgroundContent { + let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 + + replyBackgroundNode.isHidden = true + backgroundContent.cornerRadius = cornerRadius + backgroundContent.frame = replyBackgroundNode.frame + if let (rect, containerSize) = strongSelf.absoluteRect { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } else { + replyBackgroundNode.isHidden = false + } } let panelsAlpha: CGFloat = item.controllerInteraction.selectionState == nil ? 1.0 : 0.0 + strongSelf.threadInfoNode?.alpha = panelsAlpha strongSelf.replyInfoNode?.alpha = panelsAlpha strongSelf.viaBotNode?.alpha = panelsAlpha strongSelf.forwardInfoNode?.alpha = panelsAlpha diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 54286842d9..511567ca66 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -510,6 +510,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var forwardInfoReferenceNode: ASDisplayNode? { return self.forwardInfoNode } + + private var threadInfoNode: ChatMessageThreadInfoNode? private var replyInfoNode: ChatMessageReplyInfoNode? private var contentContainersWrapperNode: ASDisplayNode @@ -547,6 +549,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode contentNode.visibility = mapVisibility(self.visibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) } + if let threadInfoNode = self.threadInfoNode { + threadInfoNode.visibility = self.visibility != .none + } + if let replyInfoNode = self.replyInfoNode { replyInfoNode.visibility = self.visibility != .none } @@ -932,6 +938,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } } + if let threadInfoNode = strongSelf.threadInfoNode, threadInfoNode.frame.contains(point) { + if let _ = threadInfoNode.hitTest(strongSelf.view.convert(point, to: threadInfoNode.view), with: nil) { + return .fail + } + } if let replyInfoNode = strongSelf.replyInfoNode, replyInfoNode.frame.contains(point) { return .waitForSingleTap } @@ -1054,6 +1065,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let authorNameLayout = TextNode.asyncLayout(self.nameNode) let adminBadgeLayout = TextNode.asyncLayout(self.adminBadgeNode) + let threadInfoLayout = ChatMessageThreadInfoNode.asyncLayout(self.threadInfoNode) let forwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) @@ -1076,6 +1088,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode currentContentClassesPropertiesAndLayouts: currentContentClassesPropertiesAndLayouts, authorNameLayout: authorNameLayout, adminBadgeLayout: adminBadgeLayout, + threadInfoLayout: threadInfoLayout, forwardInfoLayout: forwardInfoLayout, replyInfoLayout: replyInfoLayout, actionButtonsLayout: actionButtonsLayout, @@ -1093,6 +1106,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))], authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), + threadInfoLayout: (ChatMessageThreadInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageThreadInfoNode), forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageReplyInfoNode), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, WallpaperBackgroundNode?, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)), @@ -1834,6 +1848,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var nameNodeOriginY: CGFloat = 0.0 var nameNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil }) var adminNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil }) + + var threadInfoOriginY: CGFloat = 0.0 + var threadInfoSizeApply: (CGSize, (Bool) -> ChatMessageThreadInfoNode?) = (CGSize(), { _ in nil }) var replyInfoOriginY: CGFloat = 0.0 var replyInfoSizeApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode?) = (CGSize(), { _ in nil }) @@ -1945,7 +1962,37 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode headerSize.height += forwardInfoSizeApply.0.height } - if !isInstantVideo, let replyMessage = replyMessage { + var hasReply = replyMessage != nil + if !isInstantVideo, let replyMessage = replyMessage, replyMessage.threadId != nil, case .peer = item.chatLocation { + if let threadId = item.message.threadId, Int64(replyMessage.id.id) == threadId, let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum) { + hasReply = false + } + + if headerSize.height.isZero { + headerSize.height += 14.0 + } else { + headerSize.height += 5.0 + } + let sizeAndApply = threadInfoLayout(ChatMessageThreadInfoNode.Arguments( + presentationData: item.presentationData, + strings: item.presentationData.strings, + context: item.context, + controllerInteraction: item.controllerInteraction, + type: .bubble(incoming: incoming), + message: replyMessage, + parentMessage: item.message, + constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer + )) + threadInfoSizeApply = (sizeAndApply.0, { synchronousLoads in sizeAndApply.1(synchronousLoads) }) + + threadInfoOriginY = headerSize.height + headerSize.width = max(headerSize.width, threadInfoSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right) + headerSize.height += threadInfoSizeApply.0.height + 5.0 + } + + if !isInstantVideo, let replyMessage = replyMessage, hasReply { if headerSize.height.isZero { headerSize.height += 6.0 } else { @@ -2401,6 +2448,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode currentCredibilityIcon: currentCredibilityIcon, adminNodeSizeApply: adminNodeSizeApply, contentUpperRightCorner: contentUpperRightCorner, + threadInfoSizeApply: threadInfoSizeApply, + threadInfoOriginY: threadInfoOriginY, forwardInfoSizeApply: forwardInfoSizeApply, forwardInfoOriginY: forwardInfoOriginY, replyInfoSizeApply: replyInfoSizeApply, @@ -2449,6 +2498,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode currentCredibilityIcon: EmojiStatusComponent.Content?, adminNodeSizeApply: (CGSize, () -> TextNode?), contentUpperRightCorner: CGPoint, + threadInfoSizeApply: (CGSize, (Bool) -> ChatMessageThreadInfoNode?), + threadInfoOriginY: CGFloat, forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode?), forwardInfoOriginY: CGFloat, replyInfoSizeApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode?), @@ -2721,6 +2772,40 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } + if let threadInfoNode = threadInfoSizeApply.1(synchronousLoads) { + strongSelf.threadInfoNode = threadInfoNode + var animateFrame = true + if threadInfoNode.supernode == nil { + strongSelf.clippingNode.addSubnode(threadInfoNode) + animateFrame = false + + threadInfoNode.visibility = strongSelf.visibility != .none + + if animation.isAnimated { + threadInfoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + let previousThreadInfoNodeFrame = threadInfoNode.frame + threadInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + threadInfoOriginY), size: threadInfoSizeApply.0) + if case let .System(duration, _) = animation { + if animateFrame { + threadInfoNode.layer.animateFrame(from: previousThreadInfoNodeFrame, to: threadInfoNode.frame, duration: duration, timingFunction: timingFunction) + } + } + } else { + if animation.isAnimated { + if let threadInfoNode = strongSelf.threadInfoNode { + strongSelf.threadInfoNode = nil + threadInfoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak threadInfoNode] _ in + threadInfoNode?.removeFromSupernode() + }) + } + } else { + strongSelf.threadInfoNode?.removeFromSupernode() + strongSelf.threadInfoNode = nil + } + } + if let replyInfoNode = replyInfoSizeApply.1(synchronousLoads) { strongSelf.replyInfoNode = replyInfoNode var animateFrame = true @@ -3498,6 +3583,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } } + } else if let threadInfoNode = self.threadInfoNode, self.item?.controllerInteraction.tapMessage == nil, threadInfoNode.frame.contains(location) { + if let item = self.item { + for attribute in item.message.attributes { + if let attribute = attribute as? ReplyMessageAttribute, let threadId = attribute.threadMessageId { + return .optionalAction({ + item.controllerInteraction.navigateToThreadMessage(item.message.id.peerId, Int64(clamping: threadId.id), item.message.id) + }) + } + } + } } if let forwardInfoNode = self.forwardInfoNode, forwardInfoNode.frame.contains(location) { if let item = self.item, let forwardInfo = item.message.forwardInfo { @@ -3652,6 +3747,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if let item = self.item, self.backgroundNode.frame.contains(location) { let message = item.message + if let threadInfoNode = self.threadInfoNode, self.item?.controllerInteraction.tapMessage == nil, threadInfoNode.frame.contains(location) { + return .action({}) + } + var tapMessage: Message? = item.content.firstMessage var selectAll = true var hasFiles = false @@ -3773,6 +3872,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return nil } + if let threadInfoNode = self.threadInfoNode, let result = threadInfoNode.hitTest(self.view.convert(point, to: threadInfoNode.view), with: event) { + return result + } + if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) { return shareButtonNode.view } @@ -3803,7 +3906,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return result } } - + return super.hitTest(point, with: event) } diff --git a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift index f64cbc03a9..655c937f3b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift @@ -495,7 +495,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { return } var messageId: MessageId? - if let messageReference = messageReference, case let .message(_, id, _, _, _) = messageReference.content { + if let messageReference = messageReference, case let .message(_, _, id, _, _, _) = messageReference.content { messageId = id } strongSelf.controllerInteraction.openPeerContextMenu(peer, messageId, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture) @@ -641,7 +641,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { @objc func tapGesture(_ recognizer: ListViewTapGestureRecognizer) { if case .ended = recognizer.state { - if self.peerId.namespace == Namespaces.Peer.Empty, case let .message(_, id, _, _, _) = self.messageReference?.content { + if self.peerId.namespace == Namespaces.Peer.Empty, case let .message(_, _, id, _, _, _) = self.messageReference?.content { self.controllerInteraction.displayMessageTooltip(id, self.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, self.avatarNode.frame) } else if let peer = self.peer { if let adMessageId = self.adMessageId { diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index 171c10999c..fa7c78f751 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -112,12 +112,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } } - var (textString, isMedia, isText) = descriptionStringForMessage(contentSettings: arguments.context.currentContentSettings.with { $0 }, message: EngineMessage(arguments.message), strings: arguments.strings, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, dateTimeFormat: arguments.presentationData.dateTimeFormat, accountPeerId: arguments.context.account.peerId) - - if let threadId = arguments.parentMessage.threadId, Int64(arguments.message.id.id) == threadId, let channel = arguments.parentMessage.peers[arguments.parentMessage.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), let threadInfo = arguments.parentMessage.associatedThreadInfo { - titleString = "\(threadInfo.title)" - textString = NSAttributedString() - } + let (textString, isMedia, isText) = descriptionStringForMessage(contentSettings: arguments.context.currentContentSettings.with { $0 }, message: EngineMessage(arguments.message), strings: arguments.strings, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, dateTimeFormat: arguments.presentationData.dateTimeFormat, accountPeerId: arguments.context.account.peerId) let placeholderColor: UIColor = arguments.message.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : arguments.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor let titleColor: UIColor diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index d79c16eb07..43513a990e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -38,7 +38,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private var viaBotNode: TextNode? private let dateAndStatusNode: ChatMessageDateAndStatusNode + private var threadInfoNode: ChatMessageThreadInfoNode? private var replyInfoNode: ChatMessageReplyInfoNode? + private var replyBackgroundContent: WallpaperBubbleBackgroundNode? private var replyBackgroundNode: NavigationBackgroundNode? private var forwardInfoNode: ChatMessageForwardInfoNode? @@ -71,6 +73,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { private var visibilityStatus: Bool? { didSet { if self.visibilityStatus != oldValue { + self.threadInfoNode?.visibility = self.visibilityStatus == true self.replyInfoNode?.visibility = self.visibilityStatus == true } } @@ -277,6 +280,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { backgroundNode.update(rect: CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize, transition: .immediate) } + if let threadInfoNode = self.threadInfoNode { + var threadInfoNodeFrame = threadInfoNode.frame + threadInfoNodeFrame.origin.x += rect.minX + threadInfoNodeFrame.origin.y += rect.minY + + threadInfoNode.updateAbsoluteRect(threadInfoNodeFrame, within: containerSize) + } + if let shareButtonNode = self.shareButtonNode { var shareButtonNodeFrame = shareButtonNode.frame shareButtonNodeFrame.origin.x += rect.minX @@ -300,6 +311,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { reactionButtonsNode.update(rect: rect, within: containerSize, transition: .immediate) } + + if let replyBackgroundContent = self.replyBackgroundContent { + var replyBackgroundContentFrame = replyBackgroundContent.frame + replyBackgroundContentFrame.origin.x += rect.minX + replyBackgroundContentFrame.origin.y += rect.minY + + replyBackgroundContent.update(rect: rect, within: containerSize, transition: .immediate) + } } } @@ -357,6 +376,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) let viaBotLayout = TextNode.asyncLayout(self.viaBotNode) + let makeThreadInfoLayout = ChatMessageThreadInfoNode.asyncLayout(self.threadInfoNode) let makeReplyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let currentShareButtonNode = self.shareButtonNode let currentForwardInfo = self.appliedForwardInfo @@ -567,6 +587,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) var viaBotApply: (TextNodeLayout, () -> TextNode)? + var threadInfoApply: (CGSize, (Bool) -> ChatMessageThreadInfoNode)? var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? var replyMarkup: ReplyMarkupMessageAttribute? @@ -610,8 +631,30 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { + var hasReply = true + if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId { - } else { + hasReply = false + } else if let threadId = item.message.threadId, Int64(replyMessage.id.id) == threadId, let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum) { + hasReply = false + } + + if case .peer = item.chatLocation, replyMessage.threadId != nil { + threadInfoApply = makeThreadInfoLayout(ChatMessageThreadInfoNode.Arguments( + presentationData: item.presentationData, + strings: item.presentationData.strings, + context: item.context, + controllerInteraction: item.controllerInteraction, + type: .standalone, + message: replyMessage, + parentMessage: item.message, + constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), + animationCache: item.controllerInteraction.presentationContext.animationCache, + animationRenderer: item.controllerInteraction.presentationContext.animationRenderer + )) + } + + if hasReply { replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments( presentationData: item.presentationData, strings: item.presentationData.strings, @@ -764,9 +807,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { baseShareButtonFrame.origin.x = dateAndStatusFrame.maxX + 8.0 } + var headersOffset: CGFloat = 0.0 + if let (threadInfoSize, _) = threadInfoApply { + headersOffset += threadInfoSize.height + 10.0 + } + var viaBotFrame: CGRect? if let (viaBotLayout, _) = viaBotApply { - viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 15.0) : (params.width - params.rightInset - viaBotLayout.size.width - layoutConstants.bubble.edgeInset - 14.0)), y: 8.0), size: viaBotLayout.size) + viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 15.0) : (params.width - params.rightInset - viaBotLayout.size.width - layoutConstants.bubble.edgeInset - 14.0)), y: headersOffset + 8.0), size: viaBotLayout.size) } var replyInfoFrame: CGRect? @@ -775,7 +823,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let viaBotFrame = viaBotFrame { viaBotSize = viaBotFrame.size } - let replyInfoFrameValue = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - max(replyInfoSize.width, viaBotSize.width) - layoutConstants.bubble.edgeInset - 10.0)), y: 8.0 + viaBotSize.height), size: replyInfoSize) + let replyInfoFrameValue = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - max(replyInfoSize.width, viaBotSize.width) - layoutConstants.bubble.edgeInset - 10.0)), y: headersOffset + 8.0 + viaBotSize.height), size: replyInfoSize) replyInfoFrame = replyInfoFrameValue if let viaBotFrameValue = viaBotFrame { if replyInfoFrameValue.minX < replyInfoFrameValue.minX { @@ -791,7 +839,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { viaBotSize = viaBotFrame.size } - replyBackgroundFrame = CGRect(origin: CGPoint(x: replyInfoFrame.minX - 4.0, y: replyInfoFrame.minY - viaBotSize.height - 2.0), size: CGSize(width: max(replyInfoFrame.size.width, viaBotSize.width) + 8.0, height: replyInfoFrame.size.height + viaBotSize.height + 5.0)) + replyBackgroundFrame = CGRect(origin: CGPoint(x: replyInfoFrame.minX - 4.0, y: headersOffset + replyInfoFrame.minY - viaBotSize.height - 2.0), size: CGSize(width: max(replyInfoFrame.size.width, viaBotSize.width) + 8.0, height: replyInfoFrame.size.height + viaBotSize.height + 5.0)) } if let replyBackgroundFrameValue = replyBackgroundFrame { @@ -875,9 +923,41 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.replyBackgroundNode = replyBackgroundNode strongSelf.contextSourceNode.contentNode.addSubnode(replyBackgroundNode) } + + if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { + if strongSelf.replyBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + strongSelf.replyBackgroundContent = backgroundContent + strongSelf.insertSubnode(backgroundContent, at: 0) + } + } else { + strongSelf.replyBackgroundContent?.removeFromSupernode() + strongSelf.replyBackgroundContent = nil + } } else if let replyBackgroundNode = strongSelf.replyBackgroundNode { replyBackgroundNode.removeFromSupernode() strongSelf.replyBackgroundNode = nil + + if let replyBackgroundContent = strongSelf.replyBackgroundContent { + replyBackgroundContent.removeFromSupernode() + strongSelf.replyBackgroundContent = nil + } + } + + var headersOffset: CGFloat = 0.0 + if let (threadInfoSize, threadInfoApply) = threadInfoApply { + let threadInfoNode = threadInfoApply(synchronousLoads) + if strongSelf.threadInfoNode == nil { + strongSelf.threadInfoNode = threadInfoNode + strongSelf.contextSourceNode.contentNode.addSubnode(threadInfoNode) + } + let threadInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 6.0) : (params.width - params.rightInset - threadInfoSize.width - layoutConstants.bubble.edgeInset - 8.0)), y: 8.0), size: threadInfoSize) + threadInfoNode.frame = threadInfoFrame + + headersOffset += threadInfoSize.height + 10.0 + } else if let replyInfoNode = strongSelf.replyInfoNode { + replyInfoNode.removeFromSupernode() + strongSelf.replyInfoNode = nil } var messageInfoSize = CGSize() @@ -897,7 +977,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.viaBotNode = viaBotNode strongSelf.contextSourceNode.contentNode.addSubnode(viaBotNode) } - let viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: 8.0), size: viaBotLayout.size) + let viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0), size: viaBotLayout.size) viaBotNode.frame = viaBotFrame messageInfoSize = CGSize(width: messageInfoSize.width, height: viaBotLayout.size.height) @@ -916,7 +996,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { forwardInfoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } - let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 8.0)), y: 8.0 + messageInfoSize.height), size: forwardInfoSize) + let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 8.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: forwardInfoSize) forwardInfoNode.frame = forwardInfoFrame messageInfoSize = CGSize(width: messageInfoSize.width, height: messageInfoSize.height + forwardInfoSize.height - 1.0) @@ -940,7 +1020,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.replyInfoNode = replyInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) } - let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: 8.0 + messageInfoSize.height), size: replyInfoSize) + let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 11.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 9.0)), y: headersOffset + 8.0 + messageInfoSize.height), size: replyInfoSize) replyInfoNode.frame = replyInfoFrame messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: messageInfoSize.height + replyInfoSize.height) @@ -949,15 +1029,31 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.replyInfoNode = nil } - if let replyBackgroundNode = strongSelf.replyBackgroundNode { - replyBackgroundNode.frame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)) - 4.0, y: 6.0), size: CGSize(width: messageInfoSize.width + 8.0, height: messageInfoSize.height + 5.0)) + replyBackgroundNode.frame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - messageInfoSize.width - layoutConstants.bubble.edgeInset - 10.0)) - 4.0, y: headersOffset + 6.0), size: CGSize(width: messageInfoSize.width + 8.0, height: messageInfoSize.height + 5.0)) let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: cornerRadius, transition: .immediate) + + if let backgroundContent = strongSelf.replyBackgroundContent { + let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 + + replyBackgroundNode.isHidden = true + backgroundContent.cornerRadius = cornerRadius + backgroundContent.frame = replyBackgroundNode.frame + if let (rect, containerSize) = strongSelf.absoluteRect { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } else { + replyBackgroundNode.isHidden = false + } } - + let panelsAlpha: CGFloat = item.controllerInteraction.selectionState == nil ? 1.0 : 0.0 + strongSelf.threadInfoNode?.alpha = panelsAlpha strongSelf.replyInfoNode?.alpha = panelsAlpha strongSelf.viaBotNode?.alpha = panelsAlpha strongSelf.forwardInfoNode?.alpha = panelsAlpha diff --git a/submodules/TelegramUI/Sources/ChatMessageThreadInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageThreadInfoNode.swift new file mode 100644 index 0000000000..3763acfcad --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatMessageThreadInfoNode.swift @@ -0,0 +1,505 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Postbox +import Display +import TelegramCore +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import LocalizedPeerData +import PhotoResources +import TelegramStringFormatting +import TextFormat +import InvisibleInkDustNode +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer +import ComponentFlow +import EmojiStatusComponent +import WallpaperBackgroundNode + +private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, UIImage?) { + enum CornerType { + case topLeft + case topRight + case bottomLeft + case bottomRight + } + + func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) { + if radius.isZero { + return + } + context.setFillColor(color.cgColor) + switch type { + case .topLeft: + context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .topRight: + context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomLeft: + context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomRight: + context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + } + } + + func drawConnectingCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) { + context.setFillColor(color.cgColor) + switch type { + case .topLeft: + context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .topRight: + context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomLeft: + context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomRight: + context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + } + } + + if rects.isEmpty { + return (CGPoint(), nil) + } + + var topLeft = rects[0].origin + var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY) + for i in 1 ..< rects.count { + topLeft.x = min(topLeft.x, rects[i].origin.x) + topLeft.y = min(topLeft.y, rects[i].origin.y) + bottomRight.x = max(bottomRight.x, rects[i].maxX) + bottomRight.y = max(bottomRight.y, rects[i].maxY) + } + + topLeft.x -= inset + topLeft.y -= inset + bottomRight.x += inset * 2.0 + bottomRight.y += inset * 2.0 + + return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + + context.setBlendMode(.copy) + + for i in 0 ..< rects.count { + let rect = rects[i].insetBy(dx: -inset, dy: -inset) + context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y)) + } + + for i in 0 ..< rects.count { + let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + + var previous: CGRect? + if i != 0 { + previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + } + + var next: CGRect? + if i != rects.count - 1 { + next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + } + + if let previous = previous { + if previous.contains(rect.topLeft) { + if abs(rect.topLeft.x - previous.minX) >= innerRadius { + var radius = innerRadius + if let next = next { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius) + } + if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) { + if abs(rect.topRight.x - previous.maxX) >= innerRadius { + var radius = innerRadius + if let next = next { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius) + drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius) + } + + if let next = next { + if next.contains(rect.bottomLeft) { + if abs(rect.bottomRight.x - next.maxX) >= innerRadius { + var radius = innerRadius + if let previous = previous { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius) + } + if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) { + if abs(rect.bottomRight.x - next.maxX) >= innerRadius { + var radius = innerRadius + if let previous = previous { + radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) + } + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius) + drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius) + } + } + })) + +} + +enum ChatMessageThreadInfoType { + case bubble(incoming: Bool) + case standalone +} + +class ChatMessageThreadInfoNode: ASDisplayNode { + class Arguments { + let presentationData: ChatPresentationData + let strings: PresentationStrings + let context: AccountContext + let controllerInteraction: ChatControllerInteraction + let type: ChatMessageThreadInfoType + let message: Message + let parentMessage: Message + let constrainedSize: CGSize + let animationCache: AnimationCache? + let animationRenderer: MultiAnimationRenderer? + + init( + presentationData: ChatPresentationData, + strings: PresentationStrings, + context: AccountContext, + controllerInteraction: ChatControllerInteraction, + type: ChatMessageThreadInfoType, + message: Message, + parentMessage: Message, + constrainedSize: CGSize, + animationCache: AnimationCache?, + animationRenderer: MultiAnimationRenderer? + ) { + self.presentationData = presentationData + self.strings = strings + self.context = context + self.controllerInteraction = controllerInteraction + self.type = type + self.message = message + self.parentMessage = parentMessage + self.constrainedSize = constrainedSize + self.animationCache = animationCache + self.animationRenderer = animationRenderer + } + } + + var visibility: Bool = false { + didSet { + if self.visibility != oldValue { + self.textNode?.visibilityRect = self.visibility ? CGRect.infinite : nil + + if let titleTopicIconView = self.titleTopicIconView, let titleTopicIconComponent = self.titleTopicIconComponent { + let _ = titleTopicIconView.update( + transition: .immediate, + component: AnyComponent(titleTopicIconComponent.withVisibleForAnimations(self.visibility)), + environment: {}, + containerSize: titleTopicIconView.bounds.size + ) + } + } + } + } + + private var backgroundContent: WallpaperBubbleBackgroundNode? + private var backgroundNode: NavigationBackgroundNode? + + private let contentNode: HighlightTrackingButtonNode + private let contentBackgroundNode: ASImageNode + private var textNode: TextNodeWithEntities? + private let arrowNode: ASImageNode + + private var titleTopicIconView: ComponentHostView? + private var titleTopicIconComponent: EmojiStatusComponent? + + private var lineRects: [CGRect] = [] + + private var pressed = { } + + private var absolutePosition: (CGRect, CGSize)? + + override init() { + self.contentNode = HighlightTrackingButtonNode() + + self.contentBackgroundNode = ASImageNode() + self.contentBackgroundNode.alpha = 0.1 + self.contentBackgroundNode.displaysAsynchronously = false + self.contentBackgroundNode.displayWithoutProcessing = true + self.contentBackgroundNode.isLayerBacked = true + self.contentBackgroundNode.isUserInteractionEnabled = false + + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + self.arrowNode.displayWithoutProcessing = true + self.arrowNode.isLayerBacked = true + self.arrowNode.isUserInteractionEnabled = false + + super.init() + + self.contentNode.isUserInteractionEnabled = true + + self.addSubnode(self.contentNode) + self.contentNode.addSubnode(self.contentBackgroundNode) + self.contentNode.addSubnode(self.arrowNode) + + self.contentNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.contentNode.layer.animateScale(from: 1.0, to: 0.96, duration: 0.15, removeOnCompletion: false) + + strongSelf.contentBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.contentBackgroundNode.alpha = 0.2 + } else if let presentationLayer = strongSelf.contentNode.layer.presentation() { + strongSelf.contentNode.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false) + + strongSelf.contentBackgroundNode.alpha = 0.1 + strongSelf.contentBackgroundNode.layer.animateAlpha(from: 0.2, to: 0.1, duration: 0.2) + } + } + } + + self.contentNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc private func buttonPressed() { + self.pressed() + } + + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absolutePosition = (rect, containerSize) + if let backgroundContent = self.backgroundContent { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } + + class func asyncLayout(_ maybeNode: ChatMessageThreadInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageThreadInfoNode) { + let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode) + + return { arguments in + let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) + let textFont = Font.medium(fontSize) + + var topicTitle = "" + var topicIconId: Int64? + var topicIconColor: Int32 = 0 + if let _ = arguments.parentMessage.threadId, let channel = arguments.parentMessage.peers[arguments.parentMessage.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), let threadInfo = arguments.parentMessage.associatedThreadInfo { + + topicTitle = threadInfo.title + topicIconId = threadInfo.icon + topicIconColor = threadInfo.iconColor + } + + let backgroundColor: UIColor + let textColor: UIColor + let arrowIcon: UIImage? + switch arguments.type { + case let .bubble(incoming): + if topicIconId == nil, topicIconColor != 0, incoming { + let colors = topicIconColors(for: topicIconColor) + backgroundColor = UIColor(rgb: colors.0.last ?? 0x000000) + textColor = UIColor(rgb: colors.1.first ?? 0x000000) + arrowIcon = PresentationResourcesChat.chatBubbleArrowImage(color: textColor) + } else { + backgroundColor = (incoming ? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor) + textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor + arrowIcon = incoming ? PresentationResourcesChat.chatBubbleArrowIncomingImage(arguments.presentationData.theme.theme) : PresentationResourcesChat.chatBubbleArrowOutgoingImage(arguments.presentationData.theme.theme) + } + case .standalone: + textColor = .white + backgroundColor = .white + arrowIcon = PresentationResourcesChat.chatBubbleArrowFreeImage(arguments.presentationData.theme.theme) + } + + let placeholderColor: UIColor = arguments.message.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : arguments.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor + + let text = NSAttributedString(string: topicTitle, font: textFont, textColor: textColor) + + let lineInset: CGFloat = 7.0 + let iconSize = CGSize(width: 22.0, height: 22.0) + let insets = UIEdgeInsets(top: 2.0, left: 4.0, bottom: 2.0, right: 4.0) + let spacing: CGFloat = 4.0 + + let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: arguments.constrainedSize.width - insets.left - insets.right - iconSize.width - spacing, height: arguments.constrainedSize.height), alignment: .natural, cutout: nil, insets: .zero)) + + var lineRects = textLayout.linesRects().map { rect in + return CGRect(origin: rect.origin.offsetBy(dx: insets.left, dy: 0.0), size: CGSize(width: rect.width + iconSize.width + spacing + 3.0, height: rect.size.height)) + } + let lastRect = lineRects[lineRects.count - 1] + lineRects[lineRects.count - 1] = CGRect(origin: lastRect.origin, size: CGSize(width: lastRect.width + 11.0, height: lastRect.height)) + + let size = CGSize(width: insets.left + iconSize.width + spacing + textLayout.size.width + insets.right + lineInset * 2.0, height: insets.top + textLayout.size.height + insets.bottom) + + return (size, { attemptSynchronous in + let node: ChatMessageThreadInfoNode + if let maybeNode = maybeNode { + node = maybeNode + } else { + node = ChatMessageThreadInfoNode() + } + + node.pressed = { + if let threadId = arguments.message.threadId { + arguments.controllerInteraction.navigateToThreadMessage(arguments.parentMessage.id.peerId, threadId, arguments.parentMessage.id) + } + } + + if node.lineRects != lineRects { + let (offset, image) = generateRectsImage(color: backgroundColor, rects: lineRects, inset: 5.0, outerRadius: 13.0, innerRadius: 8.0) + if let image = image { + if case .standalone = arguments.type { + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -3.0), size: CGSize(width: size.width + 5.0, height: size.height + 10.0)) + + if arguments.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { + if node.backgroundContent == nil, let backgroundContent = arguments.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + backgroundContent.isUserInteractionEnabled = false + node.backgroundContent = backgroundContent + node.contentNode.insertSubnode(backgroundContent, at: 0) + + let backgroundMask = UIImageView(image: image) + backgroundContent.view.mask = backgroundMask + } + + if let backgroundContent = node.backgroundContent { + backgroundContent.view.mask?.bounds = CGRect(origin: .zero, size: image.size) + (backgroundContent.view.mask as? UIImageView)?.image = image + + backgroundContent.frame = backgroundFrame + if let (rect, containerSize) = node.absolutePosition { + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } + } else { + node.backgroundContent?.removeFromSupernode() + node.backgroundContent = nil + + let backgroundNode: NavigationBackgroundNode + if let current = node.backgroundNode { + backgroundNode = current + } else { + backgroundNode = NavigationBackgroundNode(color: .clear) + backgroundNode.isUserInteractionEnabled = false + node.backgroundNode = backgroundNode + node.contentNode.insertSubnode(backgroundNode, at: 0) + + let backgroundMask = UIImageView(image: image) + backgroundNode.view.mask = backgroundMask + } + + backgroundNode.view.mask?.bounds = CGRect(origin: .zero, size: image.size) + (backgroundNode.view.mask as? UIImageView)?.image = image + + backgroundNode.frame = backgroundFrame + backgroundNode.update(size: backgroundNode.bounds.size, cornerRadius: 0.0, transition: .immediate) + backgroundNode.updateColor(color: selectDateFillStaticColor(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper), transition: .immediate) + } + } else { + node.contentBackgroundNode.frame = CGRect(origin: offset.offsetBy(dx: 0.0, dy: -11.0), size: image.size) + node.contentBackgroundNode.image = image + } + } + } + + node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview + + var textArguments: TextNodeWithEntities.Arguments? + if let cache = arguments.animationCache, let renderer = arguments.animationRenderer { + textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: placeholderColor, attemptSynchronous: attemptSynchronous) + } + let textNode = textApply(textArguments) + textNode.visibilityRect = node.visibility ? CGRect.infinite : nil + + if node.textNode == nil { + textNode.textNode.isUserInteractionEnabled = false + node.textNode = textNode + node.contentNode.addSubnode(textNode.textNode) + } + + let titleTopicIconView: ComponentHostView + if let current = node.titleTopicIconView { + titleTopicIconView = current + } else { + titleTopicIconView = ComponentHostView() + node.titleTopicIconView = titleTopicIconView + node.contentNode.view.addSubview(titleTopicIconView) + } + + let titleTopicIconContent: EmojiStatusComponent.Content + if let fileId = topicIconId, fileId != 0 { + titleTopicIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 36.0, height: 36.0), placeholderColor: arguments.presentationData.theme.theme.list.mediaPlaceholderColor, themeColor: arguments.presentationData.theme.theme.list.itemAccentColor, loopMode: .count(1)) + } else { + titleTopicIconContent = .topic(title: String(topicTitle.prefix(1)), color: topicIconColor, size: CGSize(width: 22.0, height: 22.0)) + } + + if let animationCache = arguments.animationCache, let animationRenderer = arguments.animationRenderer { + let titleTopicIconComponent = EmojiStatusComponent( + context: arguments.context, + animationCache: animationCache, + animationRenderer: animationRenderer, + content: titleTopicIconContent, + isVisibleForAnimations: node.visibility, + action: nil + ) + node.titleTopicIconComponent = titleTopicIconComponent + + let iconSize = titleTopicIconView.update( + transition: .immediate, + component: AnyComponent(titleTopicIconComponent), + environment: {}, + containerSize: CGSize(width: 22.0, height: 22.0) + ) + titleTopicIconView.frame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: iconSize) + } + + let textFrame = CGRect(origin: CGPoint(x: iconSize.width + 2.0 + insets.left, y: insets.top), size: textLayout.size) + textNode.textNode.frame = textFrame + + if let arrowIcon = arrowIcon, let lastRect = lineRects.last { + node.arrowNode.image = arrowIcon + node.arrowNode.frame = CGRect(origin: CGPoint(x: lastRect.maxX - arrowIcon.size.width - 1.0, y: floorToScreenPixels(lastRect.midY - arrowIcon.size.height / 2.0) - 11.0 + UIScreenPixel), size: arrowIcon.size) + } + + node.contentNode.frame = CGRect(origin: CGPoint(), size: size) + + return node + }) + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 5aef700fb2..16fd355b29 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -263,6 +263,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in + }, navigateToThreadMessage: { _, _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in self?.openUrl(url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index c00caf0884..892a074980 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -112,6 +112,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in + }, navigateToThreadMessage: { _, _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect, _, _ in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendEmoji: { _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 2e7429f1ef..2aa1b1602e 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -76,6 +76,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in + }, navigateToThreadMessage: { _, _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 51ffef6c3a..d08f36a4af 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -2431,6 +2431,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }) }, navigateToMessage: { fromId, id in }, navigateToMessageStandalone: { _ in + }, navigateToThreadMessage: { _, _, _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { [weak self] ids, value in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Sources/PrefetchManager.swift b/submodules/TelegramUI/Sources/PrefetchManager.swift index a017c0d106..b0d2f47c75 100644 --- a/submodules/TelegramUI/Sources/PrefetchManager.swift +++ b/submodules/TelegramUI/Sources/PrefetchManager.swift @@ -173,16 +173,16 @@ private final class PrefetchManagerInnerImpl { if case .full = automaticDownload { if let image = media as? TelegramMediaImage { - context.fetchDisposable.set(messageMediaImageInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), image: image, resource: resource, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil).start()) + context.fetchDisposable.set(messageMediaImageInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), image: image, resource: resource, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil).start()) } else if let _ = media as? TelegramMediaWebFile { //strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).start()) } else if let file = media as? TelegramMediaFile { - let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), file: file, userInitiated: false, priority: priority) + let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), file: file, userInitiated: false, priority: priority) context.fetchDisposable.set(fetchSignal.start()) } } else if case .prefetch = automaticDownload, mediaItem.media.peer.id.namespace != Namespaces.Peer.SecretChat { if let file = media as? TelegramMediaFile, let _ = file.size { - context.fetchDisposable.set(preloadVideoResource(postbox: self.account.postbox, resourceReference: FileMediaReference.message(message: MessageReference(peer: mediaItem.media.peer, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), media: file).resourceReference(file.resource), duration: 4.0).start()) + context.fetchDisposable.set(preloadVideoResource(postbox: self.account.postbox, resourceReference: FileMediaReference.message(message: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), media: file).resourceReference(file.resource), duration: 4.0).start()) } } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 0a7e5a3c94..e08bd3935c 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1293,6 +1293,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in + }, navigateToThreadMessage: { _, _, _ in }, tapMessage: { message in tapMessage?(message) }, clickThroughMessage: { @@ -1523,6 +1524,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .emojiStatus(peerId, fileId, file, packTitle) case .voiceToText: mappedSource = .voiceToText + case .fasterDownload: + mappedSource = .fasterDownload } return PremiumIntroScreen(context: context, source: mappedSource) }