diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 6224c2704a..f07b8239b3 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -1425,15 +1425,24 @@ private final class NotificationServiceHandler { } } } + + let wasDisplayed = stateManager.postbox.transaction { transaction -> Bool in + if let messageId { + return _internal_getMessageNotificationWasDisplayed(transaction: transaction, id: messageId) + } else { + return false + } + } Logger.shared.log("NotificationService \(episode)", "Will fetch media") let _ = (combineLatest(queue: queue, fetchMediaSignal |> timeout(10.0, queue: queue, alternate: .single(nil)), fetchNotificationSoundSignal - |> timeout(10.0, queue: queue, alternate: .single(nil)) + |> timeout(10.0, queue: queue, alternate: .single(nil)), + wasDisplayed ) - |> deliverOn(queue)).start(next: { mediaData, notificationSoundData in + |> deliverOn(queue)).start(next: { mediaData, notificationSoundData, wasDisplayed in guard let strongSelf = self, let stateManager = strongSelf.stateManager else { completed() return @@ -1527,6 +1536,15 @@ private final class NotificationServiceHandler { Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)") + if wasDisplayed { + content = NotificationContent(isLockedMessage: nil) + Logger.shared.log("NotificationService \(episode)", "Was already displayed, skipping content") + } else if let messageId { + let _ = (stateManager.postbox.transaction { transaction -> Void in + _internal_setMessageNotificationWasDisplayed(transaction: transaction, id: messageId) + }).start() + } + updateCurrentContent(content) completed() diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 84c38c6da3..945130e464 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -985,6 +985,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private var cachedChatListText: (String, String)? private var cachedChatListSearchResult: CachedChatListSearchResult? + private var cachedChatListQuoteSearchResult: CachedChatListSearchResult? private var cachedCustomTextEntities: CachedCustomTextEntities? var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, ListViewItemLayoutParams, countersSize: CGFloat)? @@ -1585,6 +1586,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let currentItem = self.layoutParams?.0 let currentChatListText = self.cachedChatListText let currentChatListSearchResult = self.cachedChatListSearchResult + let currentChatListQuoteSearchResult = self.cachedChatListQuoteSearchResult let currentCustomTextEntities = self.cachedCustomTextEntities return { item, params, first, last, firstWithHeader, nextIsPinned in @@ -1869,6 +1871,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var chatListText: (String, String)? var chatListSearchResult: CachedChatListSearchResult? + var chatListQuoteSearchResult: CachedChatListSearchResult? var customTextEntities: CachedCustomTextEntities? let contentImageSide: CGFloat = max(10.0, min(20.0, floor(item.presentationData.fontSize.baseDisplaySize * 18.0 / 17.0))) @@ -2016,15 +2019,45 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { composedString = NSMutableAttributedString(attributedString: messageString) } + var composedReplyString: NSMutableAttributedString? if let searchQuery = item.interaction.searchTextHighightState { + var quoteText: String? + for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute { + if let quote = attribute.quote { + quoteText = quote.text + } + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + if let quote = attribute.quote { + quoteText = quote.text + } + } + } + if let quoteText { + let quoteString = foldLineBreaks(stringWithAppliedEntities(quoteText, entities: [], baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: italicTextFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil)) + composedReplyString = NSMutableAttributedString(attributedString: quoteString) + } + if let cached = currentChatListSearchResult, cached.matches(text: composedString.string, searchQuery: searchQuery) { chatListSearchResult = cached } else { let (ranges, text) = findSubstringRanges(in: composedString.string, query: searchQuery) chatListSearchResult = CachedChatListSearchResult(text: text, searchQuery: searchQuery, resultRanges: ranges) } + + if let composedReplyString { + if let cached = currentChatListQuoteSearchResult, cached.matches(text: composedReplyString.string, searchQuery: searchQuery) { + chatListQuoteSearchResult = cached + } else { + let (ranges, text) = findSubstringRanges(in: composedReplyString.string, query: searchQuery) + chatListQuoteSearchResult = CachedChatListSearchResult(text: text, searchQuery: searchQuery, resultRanges: ranges) + } + } else { + chatListQuoteSearchResult = nil + } } else { chatListSearchResult = nil + chatListQuoteSearchResult = nil } if let chatListSearchResult = chatListSearchResult, let firstRange = chatListSearchResult.resultRanges.first { @@ -2053,6 +2086,34 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { composedString = composedString.attributedSubstring(from: NSMakeRange(leftOrigin, composedString.length - leftOrigin)).mutableCopy() as! NSMutableAttributedString composedString.insert(NSAttributedString(string: "\u{2026}", attributes: [NSAttributedString.Key.font: textFont, NSAttributedString.Key.foregroundColor: theme.messageTextColor]), at: 0) } + } else if var composedReplyString, let chatListQuoteSearchResult, let firstRange = chatListQuoteSearchResult.resultRanges.first { + for range in chatListQuoteSearchResult.resultRanges { + let stringRange = NSRange(range, in: chatListQuoteSearchResult.text) + if stringRange.location >= 0 && stringRange.location + stringRange.length <= composedReplyString.length { + var stringRange = stringRange + if stringRange.location > composedReplyString.length { + continue + } else if stringRange.location + stringRange.length > composedReplyString.length { + stringRange.length = composedReplyString.length - stringRange.location + } + composedReplyString.addAttribute(.foregroundColor, value: theme.messageHighlightedTextColor, range: stringRange) + } + } + + let firstRangeOrigin = chatListQuoteSearchResult.text.distance(from: chatListQuoteSearchResult.text.startIndex, to: firstRange.lowerBound) + if firstRangeOrigin > 24 { + var leftOrigin: Int = 0 + (composedReplyString.string as NSString).enumerateSubstrings(in: NSMakeRange(0, firstRangeOrigin), options: [.byWords, .reverse]) { (str, range1, _, _) in + let distanceFromEnd = firstRangeOrigin - range1.location + if (distanceFromEnd > 12 || range1.location == 0) && leftOrigin == 0 { + leftOrigin = range1.location + } + } + composedReplyString = composedReplyString.attributedSubstring(from: NSMakeRange(leftOrigin, composedReplyString.length - leftOrigin)).mutableCopy() as! NSMutableAttributedString + composedReplyString.insert(NSAttributedString(string: "\u{2026}", attributes: [NSAttributedString.Key.font: textFont, NSAttributedString.Key.foregroundColor: theme.messageTextColor]), at: 0) + } + + composedString = composedReplyString } attributedText = composedString @@ -2712,6 +2773,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.currentItemHeight = itemHeight strongSelf.cachedChatListText = chatListText strongSelf.cachedChatListSearchResult = chatListSearchResult + strongSelf.cachedChatListQuoteSearchResult = chatListQuoteSearchResult strongSelf.cachedCustomTextEntities = customTextEntities strongSelf.onlineIsVoiceChat = onlineIsVoiceChat diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift index 817b89d626..4f43901702 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -738,14 +738,21 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, var textFrame = self.textFieldFrame textFrame.origin = CGPoint(x: 13.0, y: 6.0 - UIScreenPixel) textFrame.size.height = self.textInputView.contentSize.height - textFrame.size.width -= self.textInputView.textContainerInset.right + if let textInputView = self.textInputView as? ChatInputTextView { + textFrame.size.width -= textInputView.defaultTextContainerInset.right + } else { + textFrame.size.width -= self.textInputView.textContainerInset.right + } if self.textInputView.isRTL { textFrame.origin.x -= messageOriginDelta } self.fromMessageTextNode.frame = textFrame + self.fromMessageTextNode.updateLayout(size: textFrame.size) + self.toMessageTextNode.frame = textFrame + self.toMessageTextNode.updateLayout(size: textFrame.size) } @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index b8827a10e0..8922a6dd6e 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -738,7 +738,11 @@ public extension ContainedViewLayoutTransition { case .immediate: layer.removeAnimation(forKey: "position") layer.removeAnimation(forKey: "bounds") - layer.frame = frame + if let view = layer.delegate as? UIView { + view.frame = frame + } else { + layer.frame = frame + } if let completion = completion { completion(true) } diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 242ae9f314..74e3b57260 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -1915,7 +1915,7 @@ public func chatWebpageSnippetPhotoData(account: Account, userLocation: MediaRes } } -public func chatWebpageSnippetFile(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation, automaticFetch: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func chatWebpageSnippetFile(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation, automaticFetch: Bool = true, placeholderColor: UIColor? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal = chatWebpageSnippetFileData(account: account, userLocation: userLocation, mediaReference: mediaReference, resource: representation.resource, automaticFetch: automaticFetch) return signal |> map { fullSizeData in diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 08fc704269..3c308396b0 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -112,6 +112,7 @@ public struct Namespaces { public static let displayedStoryNotifications: Int8 = 28 public static let storySendAsPeerIds: Int8 = 29 public static let cachedChannelBoosts: Int8 = 31 + public static let displayedMessageNotifications: Int8 = 32 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 9ae4f860e0..839ae2ee1f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1993,6 +1993,20 @@ public func _internal_setStoryNotificationWasDisplayed(transaction: Transaction, transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedStoryNotifications, key: key), entry: CodableEntry(data: Data())) } +public func _internal_getMessageNotificationWasDisplayed(transaction: Transaction, id: MessageId) -> Bool { + let key = ValueBoxKey(length: 8 + 4) + key.setInt64(0, value: id.peerId.toInt64()) + key.setInt32(8, value: id.id) + return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedMessageNotifications, key: key)) != nil +} + +public func _internal_setMessageNotificationWasDisplayed(transaction: Transaction, id: MessageId) { + let key = ValueBoxKey(length: 8 + 4) + key.setInt64(0, value: id.peerId.toInt64()) + key.setInt32(8, value: id.id) + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedMessageNotifications, key: key), entry: CodableEntry(data: Data())) +} + func _internal_updateStoryViewsForMyReaction(isChannel: Bool, views: Stories.Item.Views?, previousReaction: MessageReaction.Reaction?, reaction: MessageReaction.Reaction?) -> Stories.Item.Views? { if !isChannel { return views diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 8534780507..812bae542c 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -184,6 +184,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case chatReplyOptionsTip = 50 case displayStoryInteractionGuide = 51 case dismissedPremiumAppIconsBadge = 52 + case replyQuoteTextSelectionTip = 53 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -449,6 +450,10 @@ private struct ApplicationSpecificNoticeKeys { static func dismissedPremiumAppIconsBadge() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedPremiumAppIconsBadge.key) } + + static func replyQuoteTextSelectionTip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.replyQuoteTextSelectionTip.key) + } } public struct ApplicationSpecificNotice { @@ -924,6 +929,30 @@ public struct ApplicationSpecificNotice { } } } + + public static func getReplyQuoteTextSelectionTips(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.replyQuoteTextSelectionTip())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementReplyQuoteTextSelectionTips(accountManager: AccountManager, count: Int32 = 1) -> Signal { + return accountManager.transaction { transaction -> Void in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.replyQuoteTextSelectionTip())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + currentValue += count + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.replyQuoteTextSelectionTip(), entry) + } + } + } public static func getMessageViewsPrivacyTips(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Int32 in diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index fb65888d27..62ba4b7d20 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1488,8 +1488,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } + var replyBackgroundFrame: CGRect? if let (replyInfoSize, replyInfoApply) = replyInfoApply { 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) + replyBackgroundFrame = replyInfoFrame let replyInfoNode = replyInfoApply(replyInfoFrame.size, synchronousLoads, animation) if strongSelf.replyInfoNode == nil { @@ -1504,8 +1506,8 @@ public class ChatMessageAnimatedStickerItemNode: 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: headersOffset + 6.0), size: CGSize(width: messageInfoSize.width + 8.0, height: messageInfoSize.height + 5.0)) + if let replyBackgroundNode = strongSelf.replyBackgroundNode, let replyBackgroundFrame { + replyBackgroundNode.frame = replyBackgroundFrame let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: cornerRadius, transition: .immediate) @@ -1974,6 +1976,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return .optionalAction({ item.controllerInteraction.navigateToStory(item.message, attribute.storyId) }) + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + return .action({ + item.controllerInteraction.attemptedNavigationToPrivateQuote(attribute.peerId.flatMap { item.message.peers[$0] }) + }) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 7e2804813a..d1e46c4999 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -67,7 +67,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { private var actionButtonSeparator: SimpleLayer? public let statusNode: ChatMessageDateAndStatusNode - private var inlineMediaValue: TelegramMediaImage? + private var inlineMediaValue: Media? //private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge? private var linkHighlightingNode: LinkHighlightingNode? @@ -300,7 +300,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { var maxWidth: CGFloat = .greatestFiniteMagnitude let contentMediaContinueLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))? - let inlineMediaAndSize: (TelegramMediaImage, CGSize)? + let inlineMediaAndSize: (Media, CGSize)? if let contentMediaValue { if contentMediaInline { @@ -308,6 +308,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { if let image = contentMediaValue as? TelegramMediaImage { inlineMediaAndSize = (image, CGSize(width: 54.0, height: 54.0)) + } else if let file = contentMediaValue as? TelegramMediaFile, !file.previewRepresentations.isEmpty { + inlineMediaAndSize = (file, CGSize(width: 54.0, height: 54.0)) } else { inlineMediaAndSize = nil } @@ -843,14 +845,14 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { if let current = self.backgroundView { backgroundView = current animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil) + backgroundView.update(size: backgroundFrame.size, primaryColor: mainColor, secondaryColor: secondaryColor, pattern: nil, animation: animation) } else { backgroundView = MessageInlineBlockBackgroundView() self.backgroundView = backgroundView backgroundView.frame = backgroundFrame self.transformContainer.view.insertSubview(backgroundView, at: 0) + backgroundView.update(size: backgroundFrame.size, primaryColor: mainColor, secondaryColor: secondaryColor, pattern: nil, animation: .None) } - - backgroundView.update(size: backgroundFrame.size, primaryColor: mainColor, secondaryColor: secondaryColor, pattern: nil, animation: animation) } else { if let backgroundView = self.backgroundView { self.backgroundView = nil @@ -892,17 +894,28 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } self.inlineMediaValue = inlineMediaValue - if updateMedia { - let updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: inlineMediaValue), placeholderColor: mainColor.withMultipliedAlpha(0.1)) - inlineMedia.setSignal(updateInlineImageSignal) - } - var fittedImageSize = inlineMediaSize - if let dimensions = inlineMediaValue.representations.last?.dimensions.cgSize { - fittedImageSize = dimensions.aspectFilled(inlineMediaSize) + if let image = inlineMediaValue as? TelegramMediaImage { + if let dimensions = image.representations.last?.dimensions.cgSize { + fittedImageSize = dimensions.aspectFilled(inlineMediaSize) + } + } else if let file = inlineMediaValue as? TelegramMediaFile { + if let dimensions = file.dimensions?.cgSize { + fittedImageSize = dimensions.aspectFilled(inlineMediaSize) + } } - inlineMedia.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: fittedImageSize, boundingSize: inlineMediaSize, intrinsicInsets: UIEdgeInsets()))() + if updateMedia { + if let image = inlineMediaValue as? TelegramMediaImage { + let updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), placeholderColor: mainColor.withMultipliedAlpha(0.1)) + inlineMedia.setSignal(updateInlineImageSignal) + } else if let file = inlineMediaValue as? TelegramMediaFile, let representation = file.previewRepresentations.last { + let updateInlineImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file), representation: representation) + inlineMedia.setSignal(updateInlineImageSignal) + } + } + + inlineMedia.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: fittedImageSize, boundingSize: inlineMediaSize, intrinsicInsets: UIEdgeInsets(), emptyColor: mainColor.withMultipliedAlpha(0.1)))() } else { if let inlineMedia = self.inlineMedia { self.inlineMedia = nil @@ -2133,7 +2146,11 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { return ChatMessageBubbleContentTapAction(content: .ignore) } - return self.defaultContentAction() + if let backgroundView = self.backgroundView, backgroundView.frame.contains(point) { + return self.defaultContentAction() + } else { + return .init(content: .none) + } } public func updateTouchesAtPoint(_ point: CGPoint?) { @@ -2184,7 +2201,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { var isHighlighted = false if rects == nil, let point { if let actionButton = self.actionButton, actionButton.frame.contains(point) { - } else { + } else if let backgroundView = self.backgroundView, backgroundView.frame.contains(point) { isHighlighted = true } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index c0613b3751..863866a4bc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -3957,6 +3957,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return .action({ item.controllerInteraction.navigateToStory(item.message, attribute.storyId) }) + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + return .action({ + item.controllerInteraction.attemptedNavigationToPrivateQuote(attribute.peerId.flatMap { item.message.peers[$0] }) + }) } } } @@ -4161,6 +4165,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return .action({ item.controllerInteraction.navigateToStory(item.message, attribute.storyId) }) + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + return .action({ + item.controllerInteraction.attemptedNavigationToPrivateQuote(attribute.peerId.flatMap { item.message.peers[$0] }) + }) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index 4bf1ad17c1..b558eae069 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -761,8 +761,10 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco } } + var replyBackgroundFrame: CGRect? if let (replyInfoSize, replyInfoApply) = replyInfoApply { 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) + replyBackgroundFrame = replyInfoFrame let replyInfoNode = replyInfoApply(replyInfoFrame.size, synchronousLoads, animation) if strongSelf.replyInfoNode == nil { @@ -777,8 +779,8 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco 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)) + if let replyBackgroundNode = strongSelf.replyBackgroundNode, let replyBackgroundFrame { + replyBackgroundNode.frame = replyBackgroundFrame let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: cornerRadius, transition: .immediate) @@ -963,6 +965,10 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco return .optionalAction({ item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text)) }) + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + return .action({ + item.controllerInteraction.attemptedNavigationToPrivateQuote(attribute.peerId.flatMap { item.message.peers[$0] }) + }) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 0942824d8d..eefebccc34 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -999,8 +999,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } + var replyBackgroundFrame: CGRect? if let (replyInfoSize, replyInfoApply) = replyInfoApply { let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (displayVideoFrame.maxX - width + 5.0) : (width - messageInfoSize.width - bubbleEdgeInset - 9.0 + 10.0)), y: 8.0 + messageInfoSize.height), size: replyInfoSize) + replyBackgroundFrame = replyInfoFrame let replyInfoNode = replyInfoApply(replyInfoFrame.size, false, animation) if strongSelf.replyInfoNode == nil { @@ -1015,8 +1017,8 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { strongSelf.replyInfoNode = nil } - if let replyBackgroundNode = strongSelf.replyBackgroundNode { - let replyBackgroundFrame = CGRect(origin: CGPoint(x: (!incoming ? (displayVideoFrame.maxX - width + 4.0) : (width - messageInfoSize.width - bubbleEdgeInset)) - 4.0, y: 6.0), size: CGSize(width: messageInfoSize.width + 8.0, height: messageInfoSize.height + 5.0)) + if let replyBackgroundNode = strongSelf.replyBackgroundNode, let replyBackgroundFrame { + let replyBackgroundFrame = replyBackgroundFrame animation.animator.updateFrame(layer: replyBackgroundNode.layer, frame: replyBackgroundFrame, completion: nil) let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 @@ -1294,6 +1296,9 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } else if let attribute = attribute as? ReplyStoryAttribute { item.controllerInteraction.navigateToStory(item.message, attribute.storyId) return + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + item.controllerInteraction.attemptedNavigationToPrivateQuote(attribute.peerId.flatMap { item.message.peers[$0] }) + return } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift index e1356d27ce..e2d8837293 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift @@ -101,7 +101,6 @@ public enum ChatMessageMerge: Int32 { public protocol ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { func updateSelectionState(animated: Bool) - func updateSublayerTransformOffset(layer: CALayer, offset: CGPoint) } public protocol ChatMessageItem: ListViewItem { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index 17821e75d8..1f9dc25472 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -436,7 +436,7 @@ private let avatarFont = avatarPlaceholderFont(size: 16.0) private let maxVideoLoopCount = 3 -public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode { +public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, ChatMessageAvatarHeaderNode { private let context: AccountContext private var presentationData: ChatPresentationData private let controllerInteraction: ChatControllerInteraction diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index acfa010dda..4b1eac6b92 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -215,6 +215,9 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { case .standalone: let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) titleColor = serviceColor.primaryText + if dashSecondaryColor != nil { + secondaryColor = .clear + } mainColor = serviceMessageColorComponents(chatTheme: arguments.presentationData.theme.theme.chat, wallpaper: arguments.presentationData.theme.wallpaper).primaryText dustColor = titleColor @@ -457,7 +460,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { imageDimensions = representation.dimensions.cgSize } break - } else if let file = media as? TelegramMediaFile, file.isVideo && !file.isVideoSticker { + } else if let file = media as? TelegramMediaFile, !file.isVideoSticker { updatedMediaReference = .message(message: MessageReference(message), media: file) if let dimensions = file.dimensions { @@ -527,14 +530,12 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { var textCutoutWidth: CGFloat = 0.0 if arguments.quote != nil || arguments.replyForward?.quote != nil { additionalTitleWidth += 10.0 - if case .bubble = arguments.type { - maxTitleNumberOfLines = 2 - maxTextNumberOfLines = 5 - if imageTextInset != 0.0 { - adjustedConstrainedTextSize.width += imageTextInset - textCutout = TextNodeCutout(topLeft: CGSize(width: imageTextInset + 6.0, height: 10.0)) - textCutoutWidth = imageTextInset + 6.0 - } + maxTitleNumberOfLines = 2 + maxTextNumberOfLines = 5 + if imageTextInset != 0.0 { + adjustedConstrainedTextSize.width += imageTextInset + textCutout = TextNodeCutout(topLeft: CGSize(width: imageTextInset + 6.0, height: 10.0)) + textCutoutWidth = imageTextInset + 6.0 } } @@ -723,10 +724,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { node.contentNode.view.insertSubview(node.backgroundView, at: 0) } - var backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: realSize.width, height: realSize.height + 2.0)) - if case .standalone = arguments.type { - backgroundFrame.size.height -= 1.0 - } + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: realSize.width, height: realSize.height)) node.backgroundView.frame = backgroundFrame @@ -748,7 +746,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { pattern: pattern, animation: animation ) - + if arguments.quote != nil || arguments.replyForward?.quote != nil { let quoteIconView: UIImageView if let current = node.quoteIconView { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 2400e00a06..27b8b28c82 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -1068,8 +1068,10 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } + var replyBackgroundFrame: CGRect? if let (replyInfoSize, replyInfoApply) = replyInfoApply { 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) + replyBackgroundFrame = replyInfoFrame let replyInfoNode = replyInfoApply(replyInfoFrame.size, synchronousLoads, animation) if strongSelf.replyInfoNode == nil { @@ -1084,8 +1086,8 @@ public 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: headersOffset + 6.0), size: CGSize(width: messageInfoSize.width + 8.0, height: messageInfoSize.height + 5.0)) + if let replyBackgroundNode = strongSelf.replyBackgroundNode, let replyBackgroundFrame { + replyBackgroundNode.frame = replyBackgroundFrame let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0 replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: cornerRadius, transition: .immediate) @@ -1341,6 +1343,10 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { return .optionalAction({ item.controllerInteraction.navigateToStory(item.message, attribute.storyId) }) + } else if let attribute = attribute as? QuotedReplyMessageAttribute { + return .optionalAction({ + item.controllerInteraction.attemptedNavigationToPrivateQuote(attribute.peerId.flatMap { item.message.peers[$0] }) + }) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index a357a92bbc..755577b0cf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -1108,13 +1108,18 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let enableCopy = !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected() textSelectionNode.enableCopy = enableCopy - let enableQuote = true + var enableQuote = !item.message.text.isEmpty var enableOtherActions = true if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .reply = info { enableOtherActions = false } else if item.controllerInteraction.canSetupReply(item.message) == .reply { enableOtherActions = false } + + if !item.controllerInteraction.canSendMessages() && !enableCopy { + enableQuote = false + } + textSelectionNode.enableQuote = enableQuote textSelectionNode.enableTranslate = enableOtherActions textSelectionNode.enableShare = enableOtherActions diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 40988aefb5..6d2678f8ac 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -519,6 +519,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, setupReply: { _ in }, canSetupReply: { _ in return .none + }, canSendMessages: { + return false }, navigateToFirstDateMessage: { _, _ in }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in @@ -572,6 +574,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, dismissTextInput: { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in + }, attemptedNavigationToPrivateQuote: { _ in }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode)) self.controllerInteraction = controllerInteraction diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 85db04b490..26162320b4 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -155,6 +155,7 @@ public final class ChatControllerInteraction { public let openSearch: () -> Void public let setupReply: (MessageId) -> Void public let canSetupReply: (Message) -> ChatControllerInteractionSwipeAction + public let canSendMessages: () -> Bool public let navigateToFirstDateMessage: (Int32, Bool) -> Void public let requestRedeliveryOfFailedMessages: (MessageId) -> Void public let addContact: (String) -> Void @@ -204,6 +205,7 @@ public final class ChatControllerInteraction { public let dismissTextInput: () -> Void public let scrollToMessageId: (MessageIndex) -> Void public let navigateToStory: (Message, StoryId) -> Void + public let attemptedNavigationToPrivateQuote: (Peer?) -> Void public var canPlayMedia: Bool = false public var hiddenMedia: [MessageId: [Media]] = [:] @@ -271,6 +273,7 @@ public final class ChatControllerInteraction { openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> ChatControllerInteractionSwipeAction, + canSendMessages: @escaping () -> Bool, navigateToFirstDateMessage: @escaping(Int32, Bool) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, @@ -319,6 +322,7 @@ public final class ChatControllerInteraction { dismissTextInput: @escaping () -> Void, scrollToMessageId: @escaping (MessageIndex) -> Void, navigateToStory: @escaping (Message, StoryId) -> Void, + attemptedNavigationToPrivateQuote: @escaping (Peer?) -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings, @@ -369,6 +373,7 @@ public final class ChatControllerInteraction { self.openSearch = openSearch self.setupReply = setupReply self.canSetupReply = canSetupReply + self.canSendMessages = canSendMessages self.navigateToFirstDateMessage = navigateToFirstDateMessage self.requestRedeliveryOfFailedMessages = requestRedeliveryOfFailedMessages self.addContact = addContact @@ -417,6 +422,7 @@ public final class ChatControllerInteraction { self.dismissTextInput = dismissTextInput self.scrollToMessageId = scrollToMessageId self.navigateToStory = navigateToStory + self.attemptedNavigationToPrivateQuote = attemptedNavigationToPrivateQuote self.automaticMediaDownloadSettings = automaticMediaDownloadSettings diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index f093d8c65b..89206fe64b 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -15,6 +15,7 @@ import ChatMessageTextBubbleContentNode import TextFormat import ChatMessageItemView import ChatMessageBubbleItemNode +import TelegramNotices private enum OptionsId: Hashable { case reply @@ -286,12 +287,16 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch return .complete() } - let items = selfController.context.account.postbox.messagesAtIds([replySubject.messageId]) + let items = combineLatest(queue: .mainQueue(), + selfController.context.account.postbox.messagesAtIds([replySubject.messageId]), + ApplicationSpecificNotice.getReplyQuoteTextSelectionTips(accountManager: selfController.context.sharedContext.accountManager) + ) |> deliverOnMainQueue - |> map { [weak selfController, weak chatController] messages -> [ContextMenuItem] in + |> map { [weak selfController, weak chatController] messages, quoteTextSelectionTips -> ContextController.Items in guard let selfController, let chatController else { - return [] + return ContextController.Items(content: .list([])) } + var items: [ContextMenuItem] = [] if replySubject.quote != nil { @@ -398,18 +403,21 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch }))) } - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Reply in Another Chat", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) }, action: { [weak selfController] c, f in - f(.default) - - guard let selfController else { - return - } - guard let replySubject = selfController.presentationInterfaceState.interfaceState.replyMessageSubject else { - return - } - moveReplyMessageToAnotherChat(selfController: selfController, replySubject: replySubject) - }))) + if selfController.presentationInterfaceState.copyProtectionEnabled || messages.first?.isCopyProtected() == true { + } else { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Reply in Another Chat", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) }, action: { [weak selfController] c, f in + f(.default) + + guard let selfController else { + return + } + guard let replySubject = selfController.presentationInterfaceState.interfaceState.replyMessageSubject else { + return + } + moveReplyMessageToAnotherChat(selfController: selfController, replySubject: replySubject) + }))) + } items.append(.separator) @@ -441,14 +449,17 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch }))) } - return items + var tip: ContextController.Tip? + if quoteTextSelectionTips <= 3, let message = messages.first, !message.text.isEmpty { + tip = .quoteSelection + } + + return ContextController.Items(content: .list(items), tip: tip) } - var tip: ContextController.Tip? - if "".isEmpty { - tip = .quoteSelection - } - return items |> map { ContextController.Items(content: .list($0), tip: tip) } + let _ = ApplicationSpecificNotice.incrementReplyQuoteTextSelectionTips(accountManager: selfController.context.sharedContext.accountManager).startStandalone() + + return items } private func chatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?, selectionState: Promise) -> ContextController.Source? { diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift new file mode 100644 index 0000000000..50639205c6 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift @@ -0,0 +1,91 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import ContextUI +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramNotices +import ChatSendMessageActionUI +import AccountContext + +func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, node: ASDisplayNode, gesture: ContextGesture) { + guard let peerId = selfController.chatLocation.peerId, let textInputView = selfController.chatDisplayNode.textInputView(), let layout = selfController.validLayout else { + return + } + let previousSupportedOrientations = selfController.supportedOrientations + if layout.size.width > layout.size.height { + selfController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .landscape) + } else { + selfController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + } + + let _ = ApplicationSpecificNotice.incrementChatMessageOptionsTip(accountManager: selfController.context.sharedContext.accountManager, count: 4).startStandalone() + + var hasEntityKeyboard = false + if case .media = selfController.presentationInterfaceState.inputMode { + hasEntityKeyboard = true + } + + let _ = (selfController.context.account.viewTracker.peerView(peerId) + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView in + guard let selfController, let peer = peerViewMainPeer(peerView) else { + return + } + var sendWhenOnlineAvailable = false + if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if currentTime > until { + sendWhenOnlineAvailable = true + } + } + if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { + sendWhenOnlineAvailable = false + } + + if sendWhenOnlineAvailable { + let _ = ApplicationSpecificNotice.incrementSendWhenOnlineTip(accountManager: selfController.context.sharedContext.accountManager, count: 4).startStandalone() + } + + let controller = ChatSendMessageActionSheetController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, peerId: selfController.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: selfController.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputView: textInputView, canSendWhenOnline: sendWhenOnlineAvailable, completion: { [weak selfController] in + guard let selfController else { + return + } + selfController.supportedOrientations = previousSupportedOrientations + }, sendMessage: { [weak selfController] mode in + guard let selfController else { + return + } + switch mode { + case .generic: + selfController.controllerInteraction?.sendCurrentMessage(false) + case .silently: + selfController.controllerInteraction?.sendCurrentMessage(true) + case .whenOnline: + selfController.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleWhenOnlineTimestamp) { [weak selfController] in + guard let selfController else { + return + } + selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: selfController.presentationInterfaceState.subject != .scheduledMessages, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } + }) + selfController.openScheduledMessages() + } + } + }, schedule: { [weak selfController] in + guard let selfController else { + return + } + selfController.controllerInteraction?.scheduleCurrentMessage() + }) + controller.emojiViewProvider = selfController.chatDisplayNode.textInputPanelNode?.emojiViewProvider + selfController.sendMessageActionsController = controller + if layout.isNonExclusive { + selfController.present(controller, in: .window(.root)) + } else { + selfController.presentInGlobalOverlay(controller, with: nil) + } + }) +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index e92b475d05..d63d5a74dd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3293,7 +3293,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }, openSearch: { }, setupReply: { [weak self] messageId in - self?.interfaceInteraction?.setupReplyMessage(messageId, { _, _ in }) + self?.interfaceInteraction?.setupReplyMessage(messageId, { _, f in f() }) }, canSetupReply: { [weak self] message in if !message.flags.contains(.Incoming) { if !message.flags.intersection([.Failed, .Sending, .Unsent]).isEmpty { @@ -3316,6 +3316,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } return .none + }, canSendMessages: { [weak self] in + guard let self else { + return false + } + return canSendMessagesToChat(self.presentationInterfaceState) }, navigateToFirstDateMessage: { [weak self] timestamp, alreadyThere in guard let strongSelf = self else { return @@ -4899,6 +4904,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) self.push(storyContainerScreen) }) + }, attemptedNavigationToPrivateQuote: { [weak self] peer in + guard let self else { + return + } + //TODO:localize + let text: String + if let peer = peer as? TelegramChannel { + if case .broadcast = peer.info { + text = "This quote is from a private channel" + } else { + text = "This quote is from a private group" + } + } else if peer is TelegramGroup { + text = "This quote is from a private group" + } else { + text = "This quote is from a private chat" + } + self.controllerInteraction?.displayUndo(.info(title: nil, text: text, timeout: nil, customUndoText: nil)) }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode)) controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency @@ -10436,78 +10459,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.window?.presentInGlobalOverlay(slowmodeTooltipController) }, displaySendMessageOptions: { [weak self] node, gesture in - if let strongSelf = self, let peerId = strongSelf.chatLocation.peerId, let textInputView = strongSelf.chatDisplayNode.textInputView(), let layout = strongSelf.validLayout { - let previousSupportedOrientations = strongSelf.supportedOrientations - if layout.size.width > layout.size.height { - strongSelf.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .landscape) - } else { - strongSelf.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - } - - let _ = ApplicationSpecificNotice.incrementChatMessageOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).startStandalone() - - var hasEntityKeyboard = false - if case .media = strongSelf.presentationInterfaceState.inputMode { - hasEntityKeyboard = true - } - - let _ = (strongSelf.context.account.viewTracker.peerView(peerId) - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in - guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else { - return - } - var sendWhenOnlineAvailable = false - if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status { - let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if currentTime > until { - sendWhenOnlineAvailable = true - } - } - if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { - sendWhenOnlineAvailable = false - } - - if sendWhenOnlineAvailable { - let _ = ApplicationSpecificNotice.incrementSendWhenOnlineTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).startStandalone() - } - - let controller = ChatSendMessageActionSheetController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputView: textInputView, canSendWhenOnline: sendWhenOnlineAvailable, completion: { [weak self] in - if let strongSelf = self { - strongSelf.supportedOrientations = previousSupportedOrientations - } - }, sendMessage: { [weak self] mode in - if let strongSelf = self { - switch mode { - case .generic: - strongSelf.controllerInteraction?.sendCurrentMessage(false) - case .silently: - strongSelf.controllerInteraction?.sendCurrentMessage(true) - case .whenOnline: - strongSelf.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleWhenOnlineTimestamp) { [weak self] in - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: strongSelf.presentationInterfaceState.subject != .scheduledMessages, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } - }) - strongSelf.openScheduledMessages() - } - } - } - } - }, schedule: { [weak self] in - if let strongSelf = self { - strongSelf.controllerInteraction?.scheduleCurrentMessage() - } - }) - controller.emojiViewProvider = strongSelf.chatDisplayNode.textInputPanelNode?.emojiViewProvider - strongSelf.sendMessageActionsController = controller - if layout.isNonExclusive { - strongSelf.present(controller, in: .window(.root)) - } else { - strongSelf.presentInGlobalOverlay(controller, with: nil) - } - }) + guard let self else { + return } + chatMessageDisplaySendMessageOptions(selfController: self, node: node, gesture: gesture) }, openScheduledMessages: { [weak self] in if let strongSelf = self { strongSelf.openScheduledMessages() diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index be2b92ec2d..b41a6cfe56 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -649,6 +649,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } + if !canSendMessagesToChat(chatPresentationInterfaceState) && (chatPresentationInterfaceState.copyProtectionEnabled || message.isCopyProtected()) { + canReply = false + } + for media in messages[0].media { if let story = media as? TelegramMediaStory { if let story = message.associatedStories[story.storyId], story.data.isEmpty { diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 3f80155d6b..0f00dc700b 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -123,6 +123,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, setupReply: { _ in }, canSetupReply: { _ in return .none + }, canSendMessages: { + return false }, navigateToFirstDateMessage: { _, _ in }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in @@ -173,6 +175,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, dismissTextInput: { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in + }, attemptedNavigationToPrivateQuote: { _ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.dimNode = ASDisplayNode() diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 8c581220f8..ca0435aff4 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -2874,6 +2874,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, setupReply: { _ in }, canSetupReply: { _ in return .none + }, canSendMessages: { + return false }, navigateToFirstDateMessage: { _, _ in }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in @@ -2924,6 +2926,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, dismissTextInput: { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in + }, attemptedNavigationToPrivateQuote: { _ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 30011d92ac..cd3c01dafe 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1512,6 +1512,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return .none + }, canSendMessages: { + return false }, navigateToFirstDateMessage: { _, _ in }, requestRedeliveryOfFailedMessages: { _ in }, addContact: { _ in @@ -1562,6 +1564,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, dismissTextInput: { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in + }, attemptedNavigationToPrivateQuote: { _ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode))