[WIP] Quotes

This commit is contained in:
Ali 2023-10-11 00:40:43 +04:00
parent b17138d501
commit 627009f32e
56 changed files with 1137 additions and 287 deletions

View File

@ -517,21 +517,42 @@ public enum ChatControllerSubject: Equatable {
case id(EngineMessage.Id)
case timestamp(Int32)
}
public struct ReplyOptions: Equatable {
public var hasQuote: Bool
public init(hasQuote: Bool) {
self.hasQuote = hasQuote
}
}
public struct ForwardOptions: Equatable {
public let hideNames: Bool
public let hideCaptions: Bool
public var hideNames: Bool
public var hideCaptions: Bool
public init(hideNames: Bool, hideCaptions: Bool) {
public var replyOptions: ReplyOptions?
public init(hideNames: Bool, hideCaptions: Bool, replyOptions: ReplyOptions?) {
self.hideNames = hideNames
self.hideCaptions = hideCaptions
self.replyOptions = replyOptions
}
}
public struct MessageOptionsInfo: Equatable {
public enum Kind {
public struct ReplyQuote: Equatable {
public let messageId: EngineMessage.Id
public let text: String
public init(messageId: EngineMessage.Id, text: String) {
self.messageId = messageId
self.text = text
}
}
public enum Kind: Equatable {
case forward
case reply
case reply(initialQuote: ReplyQuote?)
}
public let kind: Kind
@ -541,7 +562,15 @@ public enum ChatControllerSubject: Equatable {
}
}
case message(id: MessageSubject, highlight: Bool, timecode: Double?)
public struct MessageHighlight: Equatable {
public var quote: String?
public init(quote: String? = nil) {
self.quote = quote
}
}
case message(id: MessageSubject, highlight: MessageHighlight?, timecode: Double?)
case scheduledMessages
case pinnedMessages(id: EngineMessage.Id?)
case messageOptions(peerIds: [EnginePeer.Id], ids: [EngineMessage.Id], info: MessageOptionsInfo, options: Signal<ForwardOptions, NoError>)

View File

@ -1150,7 +1150,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if case let .channel(channel) = actualPeer, channel.flags.contains(.isForum), let threadId {
let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: peer.id, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, keepStack: .never).startStandalone()
} else {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeer), subject: .message(id: .id(messageId), highlight: true, timecode: nil), purposefulAction: {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), purposefulAction: {
if deactivateOnAction {
self?.deactivateSearch(animated: false)
}
@ -1399,7 +1399,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else {
var subject: ChatControllerSubject?
if case let .search(messageId) = source, let id = messageId {
subject = .message(id: .id(id), highlight: false, timecode: nil)
subject = .message(id: .id(id), highlight: nil, timecode: nil)
}
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.id), subject: subject, botStart: nil, mode: .standard(previewing: true))
chatController.canReadHistory.set(false)

View File

@ -403,6 +403,18 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
self.targetSelectionIndex = 1
}
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
case .quoteSelection:
//TODO:localize
var rawText = "Hold on a word, then move cursor to select more| text to quote."
if let range = rawText.range(of: "|") {
rawText.removeSubrange(range)
self.text = rawText
self.targetSelectionIndex = NSRange(range, in: rawText).lowerBound
} else {
self.text = rawText
self.targetSelectionIndex = 1
}
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
case .messageViewsPrivacy:
self.text = self.presentationData.strings.ChatContextMenu_MessageViewsPrivacyTip
self.targetSelectionIndex = nil

View File

@ -537,6 +537,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.contentReady.set(.single(true))
case let .controller(source):
self.contentReady.set(source.controller.ready.get())
//TODO:
//self.contentReady.set(.single(true))
}
self.initializeContent()
@ -764,51 +766,64 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
)
self.presentationNode = presentationNode
self.addSubnode(presentationNode)
/*let takenViewInfo = source.takeView()
if let takenViewInfo = takenViewInfo, let parentSupernode = takenViewInfo.contentContainingNode.supernode {
self.contentContainerNode.contentNode = .extracted(node: takenViewInfo.contentContainingNode, keepInPlace: source.keepInPlace)
if source.keepInPlace || takenViewInfo.maskView != nil {
self.clippingNode.view.mask = takenViewInfo.maskView
self.clippingNode.addSubnode(self.contentContainerNode)
} else {
self.scrollNode.addSubnode(self.contentContainerNode)
}
let contentParentNode = takenViewInfo.contentContainingNode
takenViewInfo.contentContainingNode.layoutUpdated = { [weak contentParentNode, weak self] size in
guard let strongSelf = self, let contentParentNode = contentParentNode, let parentSupernode = contentParentNode.supernode else {
return
}
if strongSelf.isAnimatingOut {
return
}
strongSelf.originalProjectedContentViewFrame = (convertFrame(contentParentNode.frame, from: parentSupernode.view, to: strongSelf.view), convertFrame(contentParentNode.contentRect, from: contentParentNode.view, to: strongSelf.view))
if let validLayout = strongSelf.validLayout {
strongSelf.updateLayout(layout: validLayout, transition: .animated(duration: 0.2 * animationDurationFactor, curve: .easeInOut), previousActionsContainerNode: nil)
}
}
self.contentAreaInScreenSpace = takenViewInfo.contentAreaInScreenSpace
self.contentContainerNode.addSubnode(takenViewInfo.contentContainingNode.contentNode)
takenViewInfo.contentContainingNode.isExtractedToContextPreview = true
takenViewInfo.contentContainingNode.isExtractedToContextPreviewUpdated?(true)
self.originalProjectedContentViewFrame = (convertFrame(takenViewInfo.contentContainingNode.frame, from: parentSupernode.view, to: self.view), convertFrame(takenViewInfo.contentContainingNode.contentRect, from: takenViewInfo.contentContainingNode.view, to: self.view))
}*/
case let .controller(source):
let transitionInfo = source.transitionInfo()
if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() {
let contentParentNode = ContextControllerContentNode(sourceView: sourceView, controller: source.controller, tapped: { [weak self] in
self?.attemptTransitionControllerIntoNavigation()
})
self.contentContainerNode.contentNode = .controller(contentParentNode)
self.scrollNode.addSubnode(self.contentContainerNode)
self.contentContainerNode.clipsToBounds = true
self.contentContainerNode.cornerRadius = 14.0
self.contentContainerNode.addSubnode(contentParentNode)
let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view)
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
if "".isEmpty {
let transitionInfo = source.transitionInfo()
if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() {
let contentParentNode = ContextControllerContentNode(sourceView: sourceView, controller: source.controller, tapped: { [weak self] in
self?.attemptTransitionControllerIntoNavigation()
})
self.contentContainerNode.contentNode = .controller(contentParentNode)
self.scrollNode.addSubnode(self.contentContainerNode)
self.contentContainerNode.clipsToBounds = true
self.contentContainerNode.cornerRadius = 14.0
self.contentContainerNode.addSubnode(contentParentNode)
let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view)
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
}
} else {
let presentationNode = ContextControllerExtractedPresentationNode(
getController: { [weak self] in
return self?.getController()
},
requestUpdate: { [weak self] transition in
guard let strongSelf = self else {
return
}
if let validLayout = strongSelf.validLayout {
strongSelf.updateLayout(
layout: validLayout,
transition: transition,
previousActionsContainerNode: nil
)
}
},
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
guard let strongSelf = self else {
return
}
if let controller = strongSelf.getController() {
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
}
},
requestDismiss: { [weak self] result in
guard let strongSelf = self else {
return
}
strongSelf.dismissedForCancel?()
strongSelf.beginDismiss(result)
},
requestAnimateOut: { [weak self] result, completion in
guard let strongSelf = self else {
return
}
strongSelf.animateOut(result: result, completion: completion)
},
source: .controller(source)
)
self.presentationNode = presentationNode
self.addSubnode(presentationNode)
}
}
}
@ -2407,6 +2422,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
public enum Tip: Equatable {
case textSelection
case quoteSelection
case messageViewsPrivacy
case messageCopyProtection(isChannel: Bool)
case animatedEmoji(text: String?, arguments: TextNodeWithEntities.Arguments?, file: TelegramMediaFile?, action: (() -> Void)?)
@ -2420,6 +2436,12 @@ public final class ContextController: ViewController, StandalonePresentableContr
} else {
return false
}
case .quoteSelection:
if case .quoteSelection = rhs {
return true
} else {
return false
}
case .messageViewsPrivacy:
if case .messageViewsPrivacy = rhs {
return true

View File

@ -115,9 +115,10 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
case location(ContextLocationContentSource)
case reference(ContextReferenceContentSource)
case extracted(ContextExtractedContentSource)
case controller(ContextControllerContentSource)
}
private final class ContentNode: ASDisplayNode {
private final class ItemContentNode: ASDisplayNode {
let offsetContainerNode: ASDisplayNode
var containingItem: ContextControllerTakeViewInfo.ContainingItem
@ -160,6 +161,28 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
}
private final class ControllerContentNode: ASDisplayNode {
let controller: ViewController
init(controller: ViewController) {
self.controller = controller
super.init()
self.addSubnode(self.controller.displayNode)
}
func update(presentationData: PresentationData, size: CGSize, transition: ContainedViewLayoutTransition) {
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
return self.view
}
}
private final class AnimatingOutState {
var currentContentScreenFrame: CGRect
@ -170,6 +193,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
}
private let _ready = Promise<Bool>()
private var didSetReady: Bool = false
var ready: Signal<Bool, NoError> {
return self._ready.get()
}
private let getController: () -> ContextControllerProtocol?
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
private let requestUpdateOverlayWantsToBeBelowKeyboard: (ContainedViewLayoutTransition) -> Void
@ -187,7 +216,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
private var reactionContextNode: ReactionContextNode?
private var reactionContextNodeIsAnimatingOut: Bool = false
private var contentNode: ContentNode?
private var itemContentNode: ItemContentNode?
private var controllerContentNode: ControllerContentNode?
private let contentRectDebugNode: ASDisplayNode
private var actionsContainerNode: ASDisplayNode
@ -310,7 +340,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
}
if case let .extracted(source) = self.source, !source.ignoreContentTouches, let contentNode = self.contentNode {
if case let .extracted(source) = self.source, !source.ignoreContentTouches, let contentNode = self.itemContentNode {
let contentPoint = self.view.convert(point, to: contentNode.containingItem.contentView)
if let result = contentNode.containingItem.customHitTest?(contentPoint) {
return result
@ -321,6 +351,10 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
return contentNode.containingItem.contentView
}
}
} else if case .controller = self.source, let contentNode = self.controllerContentNode {
let contentPoint = self.view.convert(point, to: contentNode.view)
let _ = contentPoint
//TODO:
}
return self.scrollNode.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event)
@ -457,7 +491,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
switch self.source {
case .location, .reference:
return nil
case .extracted:
case .extracted, .controller:
return self.actionsStackNode.view.convert(CGPoint(), to: self.view).y
}
}
@ -487,7 +521,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
let topInset: CGFloat = layout.insets(options: .statusBar).top + 8.0
let bottomInset: CGFloat = 10.0
let contentNode: ContentNode?
let itemContentNode: ItemContentNode?
let controllerContentNode: ControllerContentNode?
var contentTransition = transition
if self.strings !== presentationData.strings {
@ -505,7 +540,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
transition: .immediate
)
actionsEdgeInset = 16.0
case .extracted:
case .extracted, .controller:
self.backgroundNode.updateColor(
color: presentationData.theme.contextMenu.dimColor,
enableBlur: true,
@ -524,25 +559,36 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
transition.updateFrame(view: self.scroller, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true)
}
if let current = self.contentNode {
contentNode = current
if let current = self.itemContentNode {
itemContentNode = current
} else {
switch self.source {
case .location, .reference:
contentNode = nil
case .location, .reference, .controller:
itemContentNode = nil
case let .extracted(source):
guard let takeInfo = source.takeView() else {
return
}
let contentNodeValue = ContentNode(containingItem: takeInfo.containingItem)
let contentNodeValue = ItemContentNode(containingItem: takeInfo.containingItem)
contentNodeValue.animateClippingFromContentAreaInScreenSpace = takeInfo.contentAreaInScreenSpace
self.scrollNode.insertSubnode(contentNodeValue, aboveSubnode: self.actionsContainerNode)
self.contentNode = contentNodeValue
contentNode = contentNodeValue
self.itemContentNode = contentNodeValue
itemContentNode = contentNodeValue
contentTransition = .immediate
}
}
if let current = self.controllerContentNode {
controllerContentNode = current
} else {
switch self.source {
case let .controller(source):
controllerContentNode = ControllerContentNode(controller: source.controller)
case .location, .reference, .extracted:
controllerContentNode = nil
}
}
var animateReactionsIn = false
var contentTopInset: CGFloat = topInset
var removedReactionContextNode: ReactionContextNode?
@ -629,7 +675,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
removedReactionContextNode = reactionContextNode
}
if let contentNode = contentNode {
if let contentNode = itemContentNode {
switch stateTransition {
case .animateIn, .animateOut:
contentNode.storedGlobalFrame = convertFrame(contentNode.containingItem.contentRect, from: contentNode.containingItem.view, to: self.view)
@ -660,7 +706,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
return
}
case .extracted:
if let contentNode = contentNode {
if let contentNode = itemContentNode {
contentParentGlobalFrame = convertFrame(contentNode.containingItem.view.bounds, from: contentNode.containingItem.view, to: self.view)
let contentRectGlobalFrame = CGRect(origin: CGPoint(x: contentNode.containingItem.contentRect.minX, y: (contentNode.storedGlobalFrame?.maxY ?? 0.0) - contentNode.containingItem.contentRect.height), size: contentNode.containingItem.contentRect.size)
@ -671,6 +717,15 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
} else {
return
}
case .controller:
//TODO
if let contentNode = controllerContentNode {
let _ = contentNode
contentRect = CGRect(origin: CGPoint(x: layout.size.width * 0.5 - 100.0, y: layout.size.height * 0.5 - 100.0), size: CGSize(width: 200.0, height: 200.0))
contentParentGlobalFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height))
} else {
return
}
}
let keepInPlace: Bool
@ -682,17 +737,29 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
case let .extracted(source):
keepInPlace = source.keepInPlace
actionsHorizontalAlignment = source.actionsHorizontalAlignment
case .controller:
//TODO:
keepInPlace = false
actionsHorizontalAlignment = .default
}
var defaultScrollY: CGFloat = 0.0
if self.animatingOutState == nil {
if let contentNode = contentNode {
if let contentNode = itemContentNode {
contentNode.update(
presentationData: presentationData,
size: contentNode.containingItem.view.bounds.size,
transition: contentTransition
)
}
if let contentNode = controllerContentNode {
//TODO
contentNode.update(
presentationData: presentationData,
size: CGSize(),
transition: contentTransition
)
}
let actionsConstrainedHeight: CGFloat
if let actionsPositionLock = self.actionsStackNode.topPositionLock {
@ -709,7 +776,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
switch self.source {
case .location, .reference:
actionsStackPresentation = .inline
case .extracted:
case .extracted, .controller:
actionsStackPresentation = .modal
}
@ -841,6 +908,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
case .extracted:
actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.maxX - actionsSideInset - actionsSize.width - 1.0
case .controller:
//TODO:
actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.maxX - actionsSideInset - actionsSize.width - 1.0
}
}
}
@ -871,13 +941,21 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
transition.updateFrame(node: self.actionsStackNode, frame: CGRect(origin: CGPoint(x: 0.0, y: combinedActionsFrame.height - actionsSize.height), size: actionsSize), beginWithCurrentState: true)
transition.updateFrame(node: self.additionalActionsStackNode, frame: CGRect(origin: .zero, size: additionalActionsSize), beginWithCurrentState: true)
if let contentNode = contentNode {
if let contentNode = itemContentNode {
var contentFrame = CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX, y: contentRect.minY - contentNode.containingItem.contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentNode.containingItem.view.bounds.size)
if case let .extracted(extracted) = self.source, extracted.centerVertically, contentFrame.midX > layout.size.width / 2.0 {
contentFrame.origin.x = layout.size.width - contentFrame.maxX
}
contentTransition.updateFrame(node: contentNode, frame: contentFrame, beginWithCurrentState: true)
}
if let contentNode = controllerContentNode {
//TODO:
var contentFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: CGSize(width: 200.0, height: 200.0))
if case let .extracted(extracted) = self.source, extracted.centerVertically, contentFrame.midX > layout.size.width / 2.0 {
contentFrame.origin.x = layout.size.width - contentFrame.maxX
}
contentTransition.updateFrame(node: contentNode, frame: contentFrame, beginWithCurrentState: true)
}
let contentHeight: CGFloat
if self.actionsStackNode.topPositionLock != nil || self.currentReactionsPositionLock != nil {
@ -918,7 +996,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
switch stateTransition {
case .animateIn:
if let contentNode = contentNode {
if let contentNode = itemContentNode {
contentNode.takeContainingNode()
}
@ -931,7 +1009,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
let animationInContentYDistance: CGFloat
let currentContentScreenFrame: CGRect
if let contentNode = contentNode {
if let contentNode = itemContentNode {
if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace {
self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(x: 0.0, y: animateClippingFromContentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: animateClippingFromContentAreaInScreenSpace.height)), to: CGRect(origin: CGPoint(), size: layout.size), duration: 0.2)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: animateClippingFromContentAreaInScreenSpace.minY, to: 0.0, duration: 0.2)
@ -997,7 +1075,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
let actionsVerticalTransitionDirection: CGFloat
if let contentNode = contentNode {
if let contentNode = itemContentNode {
if contentNode.frame.minY < self.actionsContainerNode.frame.minY {
actionsVerticalTransitionDirection = -1.0
} else {
@ -1039,13 +1117,13 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.actionsStackNode.animateIn()
if let contentNode = contentNode {
if let contentNode = itemContentNode {
contentNode.containingItem.isExtractedToContextPreview = true
contentNode.containingItem.isExtractedToContextPreviewUpdated?(true)
contentNode.containingItem.willUpdateIsExtractedToContextPreview?(true, transition)
contentNode.containingItem.layoutUpdated = { [weak self] _, animation in
guard let strongSelf = self, let _ = strongSelf.contentNode else {
guard let strongSelf = self else {
return
}
@ -1114,11 +1192,22 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
}
if let contentNode = contentNode {
if let contentNode = itemContentNode {
currentContentScreenFrame = convertFrame(contentNode.containingItem.contentRect, from: contentNode.containingItem.view, to: self.view)
} else {
return
}
case let .controller(source):
if let putBackInfo = source.transitionInfo() {
let _ = putBackInfo
self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
//TODO:
currentContentScreenFrame = CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0))
} else {
return
}
}
self.animatingOutState = AnimatingOutState(
@ -1134,13 +1223,13 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
animationInContentYDistance = currentContentLocalFrame.minY - currentContentScreenFrame.minY
case .dismissWithoutContent:
animationInContentYDistance = 0.0
if let contentNode = contentNode {
if let contentNode = itemContentNode {
contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false)
}
}
let actionsVerticalTransitionDirection: CGFloat
if let contentNode = contentNode {
if let contentNode = itemContentNode {
if contentNode.frame.minY < self.actionsContainerNode.frame.minY {
actionsVerticalTransitionDirection = -1.0
} else {
@ -1154,9 +1243,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
}
let completeWithActionStack = contentNode == nil
let completeWithActionStack = itemContentNode == nil && controllerContentNode == nil
if let contentNode = contentNode {
if let contentNode = itemContentNode {
contentNode.containingItem.willUpdateIsExtractedToContextPreview?(false, transition)
var animationInContentXDistance: CGFloat = 0.0
@ -1192,7 +1281,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
contentNode.containingItem.isExtractedToContextPreview = false
contentNode.containingItem.isExtractedToContextPreviewUpdated?(false)
if let strongSelf = self, let contentNode = strongSelf.contentNode {
if let strongSelf = self, let contentNode = strongSelf.itemContentNode {
switch contentNode.containingItem {
case let .node(containingNode):
containingNode.addSubnode(containingNode.contentNode)
@ -1206,6 +1295,11 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
)
}
if let controllerContentNode {
let _ = controllerContentNode
//TODO
completion()
}
self.actionsContainerNode.layer.animateAlpha(from: self.actionsContainerNode.alpha, to: 0.0, duration: duration, removeOnCompletion: false)
self.actionsContainerNode.layer.animate(

View File

@ -14,6 +14,8 @@ enum ContextControllerPresentationNodeStateTransition {
}
protocol ContextControllerPresentationNode: ASDisplayNode {
var ready: Signal<Bool, NoError> { get }
func replaceItems(items: ContextController.Items, animated: Bool)
func pushItems(items: ContextController.Items)
func popItems()

View File

@ -34,6 +34,8 @@ public final class ContextMenuController: ViewController, KeyShortcutResponder,
public var centerHorizontally = false
public var dismissed: (() -> Void)?
public var dismissOnTap: ((UIView, CGPoint) -> Bool)?
public init(actions: [ContextMenuAction], catchTapsOutside: Bool = false, hasHapticFeedback: Bool = false, blurred: Bool = false) {
self.actions = actions
self.catchTapsOutside = catchTapsOutside
@ -55,6 +57,11 @@ public final class ContextMenuController: ViewController, KeyShortcutResponder,
self?.contextMenuNode.animateOut(bounce: (self?.presentationArguments as? ContextMenuControllerPresentationArguments)?.bounce ?? true, completion: {
self?.presentingViewController?.dismiss(animated: false)
})
}, dismissOnTap: { [weak self] view, point in
guard let self, let dismissOnTap = self.dismissOnTap else {
return false
}
return dismissOnTap(view, point)
}, catchTapsOutside: self.catchTapsOutside, hasHapticFeedback: self.hasHapticFeedback, blurred: self.blurred)
self.displayNodeDidLoad()
}

View File

@ -130,6 +130,7 @@ private final class ContextMenuContentScrollNode: ASDisplayNode {
final class ContextMenuNode: ASDisplayNode {
private let actions: [ContextMenuAction]
private let dismiss: () -> Void
private let dismissOnTap: (UIView, CGPoint) -> Bool
private let containerNode: ContextMenuContainerNode
private let scrollNode: ContextMenuContentScrollNode
@ -145,9 +146,10 @@ final class ContextMenuNode: ASDisplayNode {
private let feedback: HapticFeedback?
init(actions: [ContextMenuAction], dismiss: @escaping () -> Void, catchTapsOutside: Bool, hasHapticFeedback: Bool = false, blurred: Bool = false) {
init(actions: [ContextMenuAction], dismiss: @escaping () -> Void, dismissOnTap: @escaping (UIView, CGPoint) -> Bool, catchTapsOutside: Bool, hasHapticFeedback: Bool = false, blurred: Bool = false) {
self.actions = actions
self.dismiss = dismiss
self.dismissOnTap = dismissOnTap
self.catchTapsOutside = catchTapsOutside
self.containerNode = ContextMenuContainerNode(blurred: blurred)
@ -259,6 +261,14 @@ final class ContextMenuNode: ASDisplayNode {
}
if event.type == .touches || eventIsPresses {
if !self.containerNode.frame.contains(point) {
if self.dismissOnTap(self.view, point) {
self.dismiss()
if self.catchTapsOutside {
return self.view
} else {
return nil
}
}
if !self.dismissedByTouchOutside {
self.dismissedByTouchOutside = true
self.dismiss()

View File

@ -516,7 +516,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
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)))
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil)))
Queue.mainQueue().after(0.3) {
strongSelf.completeCustomDismiss()

View File

@ -2522,7 +2522,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
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)))
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil)))
Queue.mainQueue().after(0.3) {
strongSelf.completeCustomDismiss()

View File

@ -67,7 +67,7 @@ public final class HashtagSearchController: TelegramBaseController {
if let strongSelf = self {
strongSelf.openMessageFromSearchDisposable.set((strongSelf.context.engine.peers.ensurePeerIsLocallyAvailable(peer: peer) |> deliverOnMainQueue).start(next: { actualPeer in
if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeer), subject: message.id.peerId == actualPeer.id ? .message(id: .id(message.id), highlight: true, timecode: nil) : nil, keepStack: .always))
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeer), subject: message.id.peerId == actualPeer.id ? .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) : nil, keepStack: .always))
}
}))
strongSelf.controllerNode.listNode.clearHighlightAnimated(true)

View File

@ -977,7 +977,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
}
if let navigationController = controller?.navigationController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: true, timecode: nil)))
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil)))
}
})
})

View File

@ -266,7 +266,7 @@ public func messageStatsController(context: AccountContext, messageId: EngineMes
return
}
if let navigationController = controller?.navigationController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: true, timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil))
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {}, peekData: nil))
}
})
}

View File

@ -92,6 +92,7 @@ private var declaredEncodables: Void = {
declareEncodable(InlineBotMessageAttribute.self, f: { InlineBotMessageAttribute(decoder: $0) })
declareEncodable(TextEntitiesMessageAttribute.self, f: { TextEntitiesMessageAttribute(decoder: $0) })
declareEncodable(ReplyMessageAttribute.self, f: { ReplyMessageAttribute(decoder: $0) })
declareEncodable(QuotedReplyMessageAttribute.self, f: { QuotedReplyMessageAttribute(decoder: $0) })
declareEncodable(ReplyStoryAttribute.self, f: { ReplyStoryAttribute(decoder: $0) })
declareEncodable(ReplyThreadMessageAttribute.self, f: { ReplyThreadMessageAttribute(decoder: $0) })
declareEncodable(ReactionsMessageAttribute.self, f: { ReactionsMessageAttribute(decoder: $0) })

View File

@ -568,7 +568,6 @@ extension StoreMessage {
var threadMessageId: MessageId?
switch replyTo {
case let .messageReplyHeader(flags, replyToMsgId, replyToPeerId, replyHeader, replyToTopId, quoteText, quoteEntities):
let _ = replyHeader
let isForumTopic = (flags & (1 << 3)) != 0
var quote: EngineMessageReplyQuote?
@ -608,6 +607,8 @@ extension StoreMessage {
}
}
attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote))
} else if let replyHeader = replyHeader {
attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote))
}
case let .messageReplyStoryHeader(userId, storyId):
attributes.append(ReplyStoryAttribute(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: storyId)))
@ -841,8 +842,6 @@ extension StoreMessage {
var threadMessageId: MessageId?
switch replyTo {
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyToTopId, quoteText, quoteEntities):
let _ = replyHeader
var quote: EngineMessageReplyQuote?
if let quoteText = quoteText {
quote = EngineMessageReplyQuote(text: quoteText, entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []))
@ -868,6 +867,8 @@ extension StoreMessage {
break
}
attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote))
} else if let replyHeader = replyHeader {
attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote))
}
case let .messageReplyStoryHeader(userId, storyId):
attributes.append(ReplyStoryAttribute(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: storyId)))

View File

@ -369,7 +369,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
}
switch message {
case let .message(_, attributes, _, _, replyToMessageId, _, _, _, _):
if let replyToMessageId = replyToMessageId, replyToMessageId.messageId.peerId != peerId, let replyMessage = transaction.getMessage(replyToMessageId.messageId) {
if let replyToMessageId = replyToMessageId, (replyToMessageId.messageId.peerId != peerId && peerId.namespace == Namespaces.Peer.SecretChat), let replyMessage = transaction.getMessage(replyToMessageId.messageId) {
var canBeForwarded = true
if replyMessage.id.namespace != Namespaces.Message.Cloud {
canBeForwarded = false
@ -503,12 +503,17 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: peerAutoremoveTimeout, countdownBeginTime: nil))
}
if let replyToMessageId = replyToMessageId, replyToMessageId.messageId.peerId == peerId {
if let replyToMessageId = replyToMessageId {
var threadMessageId: MessageId?
var quote = replyToMessageId.quote
if let replyMessage = transaction.getMessage(replyToMessageId.messageId) {
threadMessageId = replyMessage.effectiveReplyThreadMessageId
if quote == nil, replyToMessageId.messageId.peerId != peerId {
let nsText = replyMessage.text as NSString
quote = EngineMessageReplyQuote(text: replyMessage.text, entities: messageTextEntitiesInRange(entities: replyMessage.textEntitiesAttribute?.entities ?? [], range: NSRange(location: 0, length: nsText.length), onlyQuoteable: true))
}
}
attributes.append(ReplyMessageAttribute(messageId: replyToMessageId.messageId, threadMessageId: threadMessageId, quote: replyToMessageId.quote))
attributes.append(ReplyMessageAttribute(messageId: replyToMessageId.messageId, threadMessageId: threadMessageId, quote: quote))
}
if let replyToStoryId = replyToStoryId {
attributes.append(ReplyStoryAttribute(storyId: replyToStoryId))

View File

@ -779,6 +779,7 @@ public final class PendingMessageManager {
var hideSendersNames = false
var hideCaptions = false
var replyMessageId: Int32?
var replyPeerId: PeerId?
var replyQuote: EngineMessageReplyQuote?
var replyToStoryId: StoryId?
var scheduleTime: Int32?
@ -789,6 +790,9 @@ public final class PendingMessageManager {
for attribute in messages[0].0.attributes {
if let replyAttribute = attribute as? ReplyMessageAttribute {
replyMessageId = replyAttribute.messageId.id
if peerId != replyAttribute.messageId.peerId {
replyPeerId = replyAttribute.messageId.peerId
}
replyQuote = replyAttribute.quote
} else if let attribute = attribute as? ReplyStoryAttribute {
replyToStoryId = attribute.storyId
@ -934,6 +938,14 @@ public final class PendingMessageManager {
replyFlags |= 1 << 0
}
var replyToPeerId: Api.InputPeer?
if let replyPeerId = replyPeerId {
replyToPeerId = transaction.getPeer(replyPeerId).flatMap(apiInputPeer)
}
if replyToPeerId != nil {
replyFlags |= 1 << 1
}
var quoteText: String?
var quoteEntities: [Api.MessageEntity]?
if let replyQuote = replyQuote {
@ -956,7 +968,7 @@ public final class PendingMessageManager {
}
}
replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: quoteText, quoteEntities: quoteEntities)
replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities)
} else if let replyToStoryId = replyToStoryId {
if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) {
flags |= 1 << 0
@ -1133,6 +1145,7 @@ public final class PendingMessageManager {
var forwardSourceInfoAttribute: ForwardSourceInfoAttribute?
var messageEntities: [Api.MessageEntity]?
var replyMessageId: Int32?
var replyPeerId: PeerId?
var replyQuote: EngineMessageReplyQuote?
var replyToStoryId: StoryId?
var scheduleTime: Int32?
@ -1144,6 +1157,9 @@ public final class PendingMessageManager {
for attribute in message.attributes {
if let replyAttribute = attribute as? ReplyMessageAttribute {
replyMessageId = replyAttribute.messageId.id
if peer.id != replyAttribute.messageId.peerId {
replyPeerId = replyAttribute.messageId.peerId
}
replyQuote = replyAttribute.quote
} else if let attribute = attribute as? ReplyStoryAttribute {
replyToStoryId = attribute.storyId
@ -1206,6 +1222,14 @@ public final class PendingMessageManager {
replyFlags |= 1 << 0
}
var replyToPeerId: Api.InputPeer?
if let replyPeerId = replyPeerId {
replyToPeerId = transaction.getPeer(replyPeerId).flatMap(apiInputPeer)
}
if replyToPeerId != nil {
replyFlags |= 1 << 1
}
var quoteText: String?
var quoteEntities: [Api.MessageEntity]?
if let replyQuote = replyQuote {
@ -1228,7 +1252,7 @@ public final class PendingMessageManager {
}
}
replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: quoteText, quoteEntities: quoteEntities)
replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities)
} else if let replyToStoryId = replyToStoryId {
if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) {
flags |= 1 << 0
@ -1251,6 +1275,14 @@ public final class PendingMessageManager {
replyFlags |= 1 << 0
}
var replyToPeerId: Api.InputPeer?
if let replyPeerId = replyPeerId {
replyToPeerId = transaction.getPeer(replyPeerId).flatMap(apiInputPeer)
}
if replyToPeerId != nil {
replyFlags |= 1 << 1
}
var quoteText: String?
var quoteEntities: [Api.MessageEntity]?
if let replyQuote = replyQuote {
@ -1273,7 +1305,7 @@ public final class PendingMessageManager {
}
}
replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: quoteText, quoteEntities: quoteEntities)
replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities)
} else if let replyToStoryId = replyToStoryId {
if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) {
flags |= 1 << 0
@ -1310,6 +1342,14 @@ public final class PendingMessageManager {
replyFlags |= 1 << 0
}
var replyToPeerId: Api.InputPeer?
if let replyPeerId = replyPeerId {
replyToPeerId = transaction.getPeer(replyPeerId).flatMap(apiInputPeer)
}
if replyToPeerId != nil {
replyFlags |= 1 << 1
}
var quoteText: String?
var quoteEntities: [Api.MessageEntity]?
if let replyQuote = replyQuote {
@ -1332,7 +1372,7 @@ public final class PendingMessageManager {
}
}
replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: nil, quoteText: quoteText, quoteEntities: quoteEntities)
replyTo = .inputReplyToMessage(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: message.threadId.flatMap(Int32.init(clamping:)), replyToPeerId: replyToPeerId, quoteText: quoteText, quoteEntities: quoteEntities)
} else if let replyToStoryId = replyToStoryId {
if let inputUser = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputUser) {
flags |= 1 << 0

View File

@ -1,5 +1,6 @@
import Foundation
import Postbox
import TelegramApi
public class ReplyMessageAttribute: MessageAttribute {
public let messageId: MessageId
@ -46,6 +47,57 @@ public class ReplyMessageAttribute: MessageAttribute {
}
}
public class QuotedReplyMessageAttribute: MessageAttribute {
public let peerId: PeerId?
public let authorName: String?
public let quote: EngineMessageReplyQuote?
public var associatedMessageIds: [MessageId] {
return []
}
public init(peerId: PeerId?, authorName: String?, quote: EngineMessageReplyQuote?) {
self.peerId = peerId
self.authorName = authorName
self.quote = quote
}
required public init(decoder: PostboxDecoder) {
self.peerId = decoder.decodeOptionalInt64ForKey("p").flatMap(PeerId.init)
self.authorName = decoder.decodeOptionalStringForKey("a")
self.quote = decoder.decodeCodable(EngineMessageReplyQuote.self, forKey: "qu")
}
public func encode(_ encoder: PostboxEncoder) {
if let peerId = self.peerId {
encoder.encodeInt64(peerId.toInt64(), forKey: "p")
} else {
encoder.encodeNil(forKey: "p")
}
if let authorName = self.authorName {
encoder.encodeString(authorName, forKey: "a")
} else {
encoder.encodeNil(forKey: "a")
}
if let quote = self.quote {
encoder.encodeCodable(quote, forKey: "qu")
} else {
encoder.encodeNil(forKey: "qu")
}
}
}
extension QuotedReplyMessageAttribute {
convenience init(apiHeader: Api.MessageFwdHeader, quote: EngineMessageReplyQuote?) {
switch apiHeader {
case let .messageFwdHeader(_, fromId, fromName, _, _, _, _, _, _):
self.init(peerId: fromId?.peerId, authorName: fromName, quote: quote)
}
}
}
public class ReplyStoryAttribute: MessageAttribute {
public let storyId: StoryId

View File

@ -1,3 +1,4 @@
import Foundation
import Postbox
public enum MessageTextEntityType: Equatable {
@ -315,3 +316,22 @@ public class TextEntitiesMessageAttribute: MessageAttribute, Equatable {
return lhs.entities == rhs.entities
}
}
public func messageTextEntitiesInRange(entities: [MessageTextEntity], range: NSRange, onlyQuoteable: Bool) -> [MessageTextEntity] {
let range: Range<Int> = range.lowerBound ..< range.upperBound
var result: [MessageTextEntity] = []
loop: for entity in entities {
if onlyQuoteable {
switch entity.type {
case .Bold, .Italic, .Strikethrough, .Underline, .Spoiler, .CustomEmoji:
break
default:
continue loop
}
}
if entity.range.overlaps(range) {
result.append(MessageTextEntity(range: entity.range.clamped(to: range), type: entity.type))
}
}
return result
}

View File

@ -546,8 +546,8 @@ public final class ChatInputTextView: ChatInputTextViewImpl, NSLayoutManagerDele
boundingRect.origin.y += self.defaultTextContainerInset.top
boundingRect.origin.x -= 9.0
boundingRect.size.width += 9.0
boundingRect.origin.x -= 4.0
boundingRect.size.width += 4.0
boundingRect.size.width += 18.0
boundingRect.size.width = min(boundingRect.size.width, self.bounds.width - 18.0)

View File

@ -51,6 +51,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
public let context: AccountContext
public let type: ChatMessageReplyInfoType
public let message: Message?
public let replyForward: QuotedReplyMessageAttribute?
public let quote: EngineMessageReplyQuote?
public let story: StoryId?
public let parentMessage: Message
@ -65,6 +66,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
context: AccountContext,
type: ChatMessageReplyInfoType,
message: Message?,
replyForward: QuotedReplyMessageAttribute?,
quote: EngineMessageReplyQuote?,
story: StoryId?,
parentMessage: Message,
@ -78,6 +80,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
self.context = context
self.type = type
self.message = message
self.replyForward = replyForward
self.quote = quote
self.story = story
self.parentMessage = parentMessage
@ -150,10 +153,24 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
}
}
if message.id.peerId != arguments.parentMessage.id.peerId {
//TODO:localize
if let peer = message.peers[message.id.peerId], (peer is TelegramChannel || peer is TelegramGroup) {
titleString += " in \(peer.debugDisplayTitle)"
}
}
let (textStringValue, isMediaValue, isTextValue) = descriptionStringForMessage(contentSettings: arguments.context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: arguments.strings, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, dateTimeFormat: arguments.presentationData.dateTimeFormat, accountPeerId: arguments.context.account.peerId)
textString = textStringValue
isMedia = isMediaValue
isText = isTextValue
} else if let replyForward = arguments.replyForward {
titleString = replyForward.authorName ?? " "
//TODO:localize
textString = NSAttributedString(string: replyForward.quote?.text ?? "Message")
isMedia = false
isText = true
} else if let story = arguments.story {
if let authorPeer = arguments.parentMessage.peers[story.peerId] {
titleString = EnginePeer(authorPeer).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
@ -289,10 +306,27 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
}
}
if entities.count > 0 {
messageText = stringWithAppliedEntities(trimToLineCount(text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message)
messageText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message)
} else {
messageText = NSAttributedString(string: text, font: textFont, textColor: textColor)
}
} else if let replyForward = arguments.replyForward, let quote = replyForward.quote {
let entities = quote.entities.filter { entity in
if case .Strikethrough = entity.type {
return true
} else if case .Spoiler = entity.type {
return true
} else if case .CustomEmoji = entity.type {
return true
} else {
return false
}
}
if entities.count > 0 {
messageText = stringWithAppliedEntities(quote.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil)
} else {
messageText = NSAttributedString(string: quote.text, font: textFont, textColor: textColor)
}
} else {
messageText = NSAttributedString(string: textString.string, font: textFont, textColor: textColor)
}
@ -356,13 +390,15 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
var additionalTitleWidth: CGFloat = 0.0
var maxTitleNumberOfLines = 1
var maxTextNumberOfLines = 1
var adjustedConstrainedTextSize = contrainedTextSize
var textCutout: TextNodeCutout?
var textCutoutWidth: CGFloat = 0.0
if arguments.quote != nil {
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
@ -372,15 +408,20 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
}
}
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: contrainedTextSize.width - additionalTitleWidth, height: contrainedTextSize.height), alignment: .natural, cutout: nil, insets: textInsets))
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: maxTitleNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: contrainedTextSize.width - additionalTitleWidth, height: contrainedTextSize.height), alignment: .natural, cutout: nil, insets: textInsets))
if isExpiredStory || isStory {
contrainedTextSize.width -= 26.0
}
if titleLayout.numberOfLines > 1 {
textCutout = nil
}
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: maxTextNumberOfLines, truncationType: .end, constrainedSize: adjustedConstrainedTextSize, alignment: .natural, lineSpacing: 0.07, cutout: textCutout, insets: textInsets))
let imageSide: CGFloat
imageSide = titleLayout.size.height + titleLayout.size.height - 14.0
let titleLineHeight: CGFloat = titleLayout.linesRects().first?.height ?? 12.0
imageSide = titleLineHeight * 2.0
var applyImage: (() -> TransformImageNode)?
if let imageDimensions = imageDimensions {
@ -567,7 +608,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
node.backgroundView.tintColor = mainColor
node.backgroundView.frame = backgroundFrame
if arguments.quote != nil {
if arguments.quote != nil || arguments.replyForward?.quote != nil {
let quoteIconView: UIImageView
if let current = node.quoteIconView {
quoteIconView = current

View File

@ -564,8 +564,17 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.statusNode.pressed = nil
}
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .reply = info.kind {
strongSelf.updateIsExtractedToContextPreview(true)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case let .reply(initialQuote) = info.kind {
if strongSelf.textSelectionNode == nil {
strongSelf.updateIsExtractedToContextPreview(true)
if let initialQuote, item.message.id == initialQuote.messageId, let string = strongSelf.textNode.textNode.cachedLayout?.attributedString {
let nsString = string.string as NSString
let subRange = nsString.range(of: initialQuote.text)
if subRange.location != NSNotFound {
strongSelf.beginTextSelection(range: subRange, displayMenu: false)
}
}
}
}
}
})
@ -772,7 +781,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
override public func updateIsExtractedToContextPreview(_ value: Bool) {
if value {
if self.textSelectionNode == nil, let item = self.item, !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected(), let rootNode = item.controllerInteraction.chatControllerNode() {
if self.textSelectionNode == nil, let item = self.item, let rootNode = item.controllerInteraction.chatControllerNode() {
let selectionColor: UIColor
let knobColor: UIColor
if item.message.effectivelyIncoming(item.context.account.peerId) {
@ -786,7 +795,15 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor), strings: item.presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { [weak self] value in
self?.updateIsTextSelectionActive?(value)
}, present: { [weak self] c, a in
self?.item?.controllerInteraction.presentGlobalOverlayController(c, a)
guard let self, let item = self.item else {
return
}
/*if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .reply = info.kind {
item.controllerInteraction.presentControllerInCurrent(c, a)
} else {*/
item.controllerInteraction.presentGlobalOverlayController(c, a)
//}
}, rootNode: { [weak rootNode] in
return rootNode
}, performAction: { [weak self] text, action in
@ -805,7 +822,22 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
}
textSelectionNode.enableQuote = item.controllerInteraction.canSetupReply(item.message) == .reply
let enableCopy = !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected()
textSelectionNode.enableCopy = enableCopy
var enableQuote = false
var enableOtherActions = true
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .reply = info.kind {
enableQuote = true
enableOtherActions = false
} else if item.controllerInteraction.canSetupReply(item.message) == .reply {
enableQuote = true
enableOtherActions = false
}
textSelectionNode.enableQuote = enableQuote
textSelectionNode.enableTranslate = enableOtherActions
textSelectionNode.enableShare = enableOtherActions
self.textSelectionNode = textSelectionNode
self.addSubnode(textSelectionNode)
self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode.textNode)
@ -859,4 +891,40 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
transition.horizontal.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: -widthDifference, y: 0.0))
}
public func beginTextSelection(range: NSRange?, displayMenu: Bool = true) {
guard let textSelectionNode = self.textSelectionNode else {
return
}
guard let string = self.textNode.textNode.cachedLayout?.attributedString else {
return
}
let nsString = string.string as NSString
let range = range ?? NSRange(location: 0, length: nsString.length)
textSelectionNode.setSelection(range: range, displayMenu: displayMenu)
}
public func getCurrentTextSelection() -> (text: String, entities: [MessageTextEntity])? {
guard let textSelectionNode = self.textSelectionNode else {
return nil
}
guard let range = textSelectionNode.getSelection() else {
return nil
}
guard let item = self.item else {
return nil
}
guard let string = self.textNode.textNode.cachedLayout?.attributedString else {
return nil
}
let nsString = string.string as NSString
let substring = nsString.substring(with: range)
var entities: [MessageTextEntity] = []
if let textEntitiesAttribute = item.message.textEntitiesAttribute {
entities = messageTextEntitiesInRange(entities: textEntitiesAttribute.entities, range: range, onlyQuoteable: true)
}
return (substring, entities)
}
}

View File

@ -21,6 +21,7 @@ import TelegramNotices
public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
private let messageDisposable = MetaDisposable()
public let chatPeerId: EnginePeer.Id
public let messageId: MessageId
public let quote: EngineMessageReplyQuote?
@ -39,9 +40,12 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
public var theme: PresentationTheme
public var strings: PresentationStrings
private var textIsOptions: Bool = false
private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)?
public init(context: AccountContext, messageId: MessageId, quote: EngineMessageReplyQuote?, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) {
public init(context: AccountContext, chatPeerId: EnginePeer.Id, messageId: MessageId, quote: EngineMessageReplyQuote?, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) {
self.chatPeerId = chatPeerId
self.messageId = messageId
self.quote = quote
@ -145,7 +149,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
isMedia = false
}
let textFont = Font.regular(14.0)
let textFont = Font.regular(15.0)
let messageText: NSAttributedString
if isText, let message = message {
let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in
@ -231,14 +235,23 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
}
}
var titleText: String
if let quote = strongSelf.quote {
//TODO:localize
strongSelf.titleNode.attributedText = NSAttributedString(string: "Reply to quote by \(authorName)", font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor)
titleText = "Reply to quote by \(authorName)"
strongSelf.textNode.attributedText = NSAttributedString(string: quote.text, font: textFont, textColor: strongSelf.theme.chat.inputPanel.primaryTextColor)
} else {
strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string, font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor)
titleText = strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string
strongSelf.textNode.attributedText = messageText
}
if strongSelf.messageId.peerId != strongSelf.chatPeerId {
if let peer = message?.peers[strongSelf.messageId.peerId], (peer is TelegramChannel || peer is TelegramGroup) {
titleText += " in \(peer.debugDisplayTitle)"
}
}
strongSelf.titleNode.attributedText = NSAttributedString(string: titleText, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor)
let headerString: String
if let message = message, message.flags.contains(.Incoming), let author = message.author {
@ -277,6 +290,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
} else {
text = "Tap here for forwarding options"
}
strongSelf.textIsOptions = true
strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor)
@ -336,7 +350,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
}
if let text = self.textNode.attributedText?.string {
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor)
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.textIsOptions ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor)
}
self.textNode.spoilerColor = self.theme.chat.inputPanel.secondaryTextColor

View File

@ -362,7 +362,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
let forwardOptions: Signal<ChatControllerSubject.ForwardOptions, NoError>
forwardOptions = strongSelf.presentationInterfaceStatePromise.get()
|> map { state -> ChatControllerSubject.ForwardOptions in
return ChatControllerSubject.ForwardOptions(hideNames: state.interfaceState.forwardOptionsState?.hideNames ?? false, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false)
return ChatControllerSubject.ForwardOptions(hideNames: state.interfaceState.forwardOptionsState?.hideNames ?? false, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false, replyOptions: nil)
}
|> distinctUntilChanged

View File

@ -2570,7 +2570,7 @@ final class StorageUsageScreenComponent: Component {
navigationController: navigationController,
context: component.context,
chatLocation: chatLocation,
subject: .message(id: .id(message.id), highlight: true, timecode: nil),
subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil),
keepStack: .always
))
})
@ -2673,7 +2673,7 @@ final class StorageUsageScreenComponent: Component {
navigationController: navigationController,
context: component.context,
chatLocation: chatLocation,
subject: .message(id: .id(message.id), highlight: true, timecode: nil),
subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil),
keepStack: .always
))
})

View File

@ -4416,7 +4416,7 @@ public final class StoryItemSetContainerComponent: Component {
presentationData: presentationData,
content: .sticker(context: context, file: animation, loop: false, title: nil, text: component.strings.Story_ToastReactionSent, undoText: component.strings.Story_ToastViewInChat, customAction: { [weak self] in
if let messageId = messageIds.first, let self {
self.navigateToPeer(peer: peer, chat: true, subject: messageId.flatMap { .message(id: .id($0), highlight: false, timecode: nil) })
self.navigateToPeer(peer: peer, chat: true, subject: messageId.flatMap { .message(id: .id($0), highlight: nil, timecode: nil) })
}
}),
elevatedLayout: false,

View File

@ -371,7 +371,7 @@ final class StoryItemSetContainerSendMessage {
animateInAsReplacement: false,
action: { [weak view, weak self] action in
if case .undo = action, let messageId {
view?.navigateToPeer(peer: peer, chat: true, subject: isScheduled ? .scheduledMessages : .message(id: .id(messageId), highlight: false, timecode: nil))
view?.navigateToPeer(peer: peer, chat: true, subject: isScheduled ? .scheduledMessages : .message(id: .id(messageId), highlight: nil, timecode: nil))
}
self?.tooltipScreen = nil
view?.updateIsProgressPaused()
@ -2721,7 +2721,7 @@ final class StoryItemSetContainerSendMessage {
return
}
if let navigationController = controller.navigationController as? NavigationController {
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: true, timecode: nil)))
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil)))
}
completion?()
})

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "IconQuote.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3.165C4.53884 3.165 4.165 3.53884 4.165 4C4.165 4.46116 4.53884 4.835 5 4.835C5.46116 4.835 5.835 4.46116 5.835 4C5.835 3.53884 5.46116 3.165 5 3.165ZM7.165 4C7.165 4.9637 6.53535 5.78033 5.665 6.06095V8.5C5.665 8.86727 5.36727 9.165 5 9.165C4.63273 9.165 4.335 8.86727 4.335 8.5V6.06095C3.46465 5.78033 2.835 4.9637 2.835 4C2.835 2.8043 3.8043 1.835 5 1.835C6.1957 1.835 7.165 2.8043 7.165 4ZM8.335 7C8.335 6.63273 8.63273 6.335 9 6.335H19C19.3673 6.335 19.665 6.63273 19.665 7C19.665 7.36727 19.3673 7.665 19 7.665H9C8.63273 7.665 8.335 7.36727 8.335 7ZM19.665 15.5C19.665 15.1327 19.3673 14.835 19 14.835C18.6327 14.835 18.335 15.1327 18.335 15.5V17.939C17.4647 18.2197 16.835 19.0363 16.835 20C16.835 21.1957 17.8043 22.165 19 22.165C20.1957 22.165 21.165 21.1957 21.165 20C21.165 19.0363 20.5353 18.2197 19.665 17.939V15.5ZM4.335 12C4.335 11.6327 4.63273 11.335 5 11.335H19C19.3673 11.335 19.665 11.6327 19.665 12C19.665 12.3673 19.3673 12.665 19 12.665H5C4.63273 12.665 4.335 12.3673 4.335 12ZM5 16.335C4.63273 16.335 4.335 16.6327 4.335 17C4.335 17.3673 4.63273 17.665 5 17.665H15C15.3673 17.665 15.665 17.3673 15.665 17C15.665 16.6327 15.3673 16.335 15 16.335H5ZM18.165 20C18.165 19.5388 18.5388 19.165 19 19.165C19.4612 19.165 19.835 19.5388 19.835 20C19.835 20.4612 19.4612 20.835 19 20.835C18.5388 20.835 18.165 20.4612 18.165 20Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "IconQuoteRemove.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.47023 3.52977C4.21053 3.27008 3.78947 3.27008 3.52977 3.52977C3.27008 3.78947 3.27008 4.21053 3.52977 4.47023L19.5298 20.4702C19.7895 20.7299 20.2105 20.7299 20.4702 20.4702C20.7299 20.2105 20.7299 19.7895 20.4702 19.5298L18.6055 17.665H19C19.3673 17.665 19.665 17.3673 19.665 17C19.665 16.6327 19.3673 16.335 19 16.335H17.2755L13.6055 12.665H19C19.3673 12.665 19.665 12.3673 19.665 12C19.665 11.6327 19.3673 11.335 19 11.335H12.2755L8.60545 7.665H12.5C12.8673 7.665 13.165 7.36727 13.165 7C13.165 6.63273 12.8673 6.335 12.5 6.335H7.27545L4.47023 3.52977ZM4.33502 7C4.33502 6.93205 4.34521 6.86647 4.36415 6.80473L5.22443 7.665H5.00002C4.63275 7.665 4.33502 7.36727 4.33502 7ZM5.00002 11.335H8.89443L10.2244 12.665H5.00002C4.63275 12.665 4.33502 12.3673 4.33502 12C4.33502 11.6327 4.63275 11.335 5.00002 11.335ZM5.00002 16.335H13.8944L15.2244 17.665H5.00002C4.63275 17.665 4.33502 17.3673 4.33502 17C4.33502 16.6327 4.63275 16.335 5.00002 16.335ZM20.6406 5.16992C20.3281 4.85742 19.9492 4.70117 19.5039 4.70117C19.2578 4.70117 19.0313 4.75391 18.8242 4.85937C18.6172 4.96484 18.4531 5.11328 18.3321 5.30469C18.2071 5.5 18.1446 5.72852 18.1446 5.99023C18.1446 6.36523 18.2598 6.66602 18.4903 6.89258C18.7168 7.11914 19.0039 7.23242 19.3516 7.23242C19.5938 7.23242 19.7969 7.16797 19.961 7.03906C20.0355 6.98226 20.0992 6.91214 20.152 6.82871C20.1095 7.02421 20.042 7.2037 19.9492 7.36719C19.8008 7.62891 19.6016 7.83594 19.3516 7.98828C19.1016 8.14062 18.8145 8.22461 18.4903 8.24023C18.3809 8.24414 18.2891 8.2832 18.2149 8.35742C18.1406 8.43164 18.1035 8.52344 18.1035 8.63281C18.1035 8.76562 18.1524 8.86719 18.25 8.9375C18.3477 9.00781 18.4688 9.04297 18.6133 9.04297C19.0547 9.04297 19.4649 8.92773 19.8438 8.69727C20.2188 8.4707 20.5235 8.16016 20.7578 7.76562C20.9883 7.37109 21.1035 6.92773 21.1035 6.43555C21.1035 5.9082 20.9492 5.48633 20.6406 5.16992ZM17.0547 5.16992C16.7422 4.85742 16.3633 4.70117 15.918 4.70117C15.6719 4.70117 15.4453 4.75391 15.2383 4.85937C15.0313 4.96484 14.8653 5.11328 14.7403 5.30469C14.6153 5.5 14.5528 5.72852 14.5528 5.99023C14.5528 6.36523 14.668 6.66602 14.8985 6.89258C15.125 7.11914 15.4141 7.23242 15.7656 7.23242C16.0039 7.23242 16.2071 7.16797 16.375 7.03906C16.4496 6.98226 16.5133 6.91214 16.566 6.82871C16.5236 7.02421 16.456 7.2037 16.3633 7.36719C16.2149 7.63281 16.0156 7.83984 15.7656 7.98828C15.5156 8.14062 15.2305 8.22461 14.9102 8.24023C14.793 8.24414 14.6973 8.2832 14.6231 8.35742C14.5489 8.43164 14.5117 8.52344 14.5117 8.63281C14.5117 8.76562 14.5606 8.86719 14.6582 8.9375C14.7559 9.00781 14.8789 9.04297 15.0274 9.04297C15.4649 9.04297 15.8731 8.92773 16.252 8.69727C16.6309 8.4707 16.9375 8.16016 17.1719 7.76562C17.4024 7.37109 17.5176 6.92773 17.5176 6.43555C17.5176 5.9082 17.3633 5.48633 17.0547 5.16992Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "IconQuoteSelected.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.6406 5.16992C20.3281 4.85742 19.9492 4.70117 19.5039 4.70117C19.2578 4.70117 19.0313 4.75391 18.8242 4.85938C18.6172 4.96484 18.4531 5.11328 18.332 5.30469C18.207 5.5 18.1445 5.72852 18.1445 5.99023C18.1445 6.36523 18.2598 6.66602 18.4902 6.89258C18.7168 7.11914 19.0039 7.23242 19.3516 7.23242C19.5938 7.23242 19.7969 7.16797 19.9609 7.03906C20.0355 6.98226 20.0992 6.91214 20.1519 6.82871C20.1095 7.02421 20.0419 7.2037 19.9492 7.36719C19.8008 7.62891 19.6016 7.83594 19.3516 7.98828C19.1016 8.14062 18.8145 8.22461 18.4902 8.24023C18.3809 8.24414 18.2891 8.2832 18.2148 8.35742C18.1406 8.43164 18.1035 8.52344 18.1035 8.63281C18.1035 8.76562 18.1523 8.86719 18.25 8.9375C18.3477 9.00781 18.4688 9.04297 18.6133 9.04297C19.0547 9.04297 19.4648 8.92773 19.8438 8.69727C20.2188 8.4707 20.5234 8.16016 20.7578 7.76562C20.9883 7.37109 21.1035 6.92773 21.1035 6.43555C21.1035 5.9082 20.9492 5.48633 20.6406 5.16992ZM17.0547 5.16992C16.7422 4.85742 16.3633 4.70117 15.918 4.70117C15.6719 4.70117 15.4453 4.75391 15.2383 4.85938C15.0313 4.96484 14.8652 5.11328 14.7402 5.30469C14.6152 5.5 14.5527 5.72852 14.5527 5.99023C14.5527 6.36523 14.668 6.66602 14.8984 6.89258C15.125 7.11914 15.4141 7.23242 15.7656 7.23242C16.0039 7.23242 16.207 7.16797 16.375 7.03906C16.4496 6.98226 16.5132 6.91214 16.566 6.82871C16.5236 7.02421 16.456 7.2037 16.3633 7.36719C16.2148 7.63281 16.0156 7.83984 15.7656 7.98828C15.5156 8.14062 15.2305 8.22461 14.9102 8.24023C14.793 8.24414 14.6973 8.2832 14.623 8.35742C14.5488 8.43164 14.5117 8.52344 14.5117 8.63281C14.5117 8.76562 14.5605 8.86719 14.6582 8.9375C14.7559 9.00781 14.8789 9.04297 15.0273 9.04297C15.4648 9.04297 15.873 8.92773 16.252 8.69727C16.6309 8.4707 16.9375 8.16016 17.1719 7.76562C17.4023 7.37109 17.5176 6.92773 17.5176 6.43555C17.5176 5.9082 17.3633 5.48633 17.0547 5.16992ZM5 6.335C4.63273 6.335 4.335 6.63273 4.335 7C4.335 7.36727 4.63273 7.665 5 7.665H12.5C12.8673 7.665 13.165 7.36727 13.165 7C13.165 6.63273 12.8673 6.335 12.5 6.335H5ZM5 11.335C4.63273 11.335 4.335 11.6327 4.335 12C4.335 12.3673 4.63273 12.665 5 12.665H19C19.3673 12.665 19.665 12.3673 19.665 12C19.665 11.6327 19.3673 11.335 19 11.335H5ZM4.335 17C4.335 16.6327 4.63273 16.335 5 16.335H19C19.3673 16.335 19.665 16.6327 19.665 17C19.665 17.3673 19.3673 17.665 19 17.665H5C4.63273 17.665 4.335 17.3673 4.335 17Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -785,7 +785,7 @@ final class AuthorizedApplicationContext {
return
}
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf.rootController, context: strongSelf.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: true, timecode: nil)))
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf.rootController, context: strongSelf.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil)))
})
}
@ -904,7 +904,7 @@ final class AuthorizedApplicationContext {
chatLocation = .peer(peer)
}
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) } : nil, activateInput: activateInput ? .text : nil))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) } : nil, activateInput: activateInput ? .text : nil))
})
}
}

View File

@ -9,6 +9,9 @@ import TelegramPresentationData
import AccountContext
import ChatPresentationInterfaceState
import ContextUI
import ChatInterfaceState
import PresentationDataUtils
import ChatMessageTextBubbleContentNode
func presentChatForwardOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) {
guard let peerId = selfController.chatLocation.peerId else {
@ -22,7 +25,7 @@ func presentChatForwardOptions(selfController: ChatControllerImpl, sourceNode: A
if peerId.namespace == Namespaces.Peer.SecretChat {
hideNames = true
}
return ChatControllerSubject.ForwardOptions(hideNames: hideNames, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false)
return ChatControllerSubject.ForwardOptions(hideNames: hideNames, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false, replyOptions: nil)
}
|> distinctUntilChanged
@ -220,6 +223,153 @@ func presentChatForwardOptions(selfController: ChatControllerImpl, sourceNode: A
selfController.presentInGlobalOverlay(contextController)
}
private func generateChatReplyOptionItems(selfController: ChatControllerImpl, chatController: ChatControllerImpl) -> Signal<ContextController.Items, NoError> {
guard let replySubject = selfController.presentationInterfaceState.interfaceState.replyMessageSubject else {
return .complete()
}
let messageIds: [EngineMessage.Id] = [replySubject.messageId]
let messagesCount: Signal<Int, NoError> = .single(1)
let items = combineLatest(selfController.context.account.postbox.messagesAtIds(messageIds), messagesCount)
|> deliverOnMainQueue
|> map { [weak selfController, weak chatController] messages, messagesCount -> [ContextMenuItem] in
guard let selfController, let chatController else {
return []
}
var items: [ContextMenuItem] = []
if replySubject.quote != nil {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Quote Selected Part", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QuoteSelected"), color: theme.contextMenu.primaryColor)
}, action: { [weak selfController, weak chatController] _, f in
guard let selfController, let chatController else {
return
}
var messageItemNode: ChatMessageItemView?
chatController.chatDisplayNode.historyNode.enumerateItemNodes { itemNode in
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == replySubject.messageId {
messageItemNode = itemNode
}
return true
}
var targetContentNode: ChatMessageTextBubbleContentNode?
if let messageItemNode = messageItemNode as? ChatMessageBubbleItemNode {
for contentNode in messageItemNode.contentNodes {
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
targetContentNode = contentNode
break
}
}
}
guard let contentNode = targetContentNode else {
return
}
guard let textSelection = contentNode.getCurrentTextSelection() else {
return
}
selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: EngineMessageReplyQuote(text: textSelection.text, entities: textSelection.entities))).withoutSelectionState() }) })
f(.default)
})))
} else {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Select Specific Quote", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Quote"), color: theme.contextMenu.primaryColor) }, action: { [weak selfController, weak chatController] c, _ in
guard let selfController, let chatController else {
return
}
var messageItemNode: ChatMessageItemView?
chatController.chatDisplayNode.historyNode.enumerateItemNodes { itemNode in
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == replySubject.messageId {
messageItemNode = itemNode
}
return true
}
if let messageItemNode = messageItemNode as? ChatMessageBubbleItemNode {
for contentNode in messageItemNode.contentNodes {
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
contentNode.beginTextSelection(range: nil)
var subItems: [ContextMenuItem] = []
subItems.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { [weak selfController, weak chatController] c, _ in
guard let selfController, let chatController else {
return
}
c.setItems(generateChatReplyOptionItems(selfController: selfController, chatController: chatController), minHeight: nil, previousActionsTransition: .slide(forward: false))
//c.popItems()
})))
subItems.append(.separator)
//TODO:localize
subItems.append(.action(ContextMenuActionItem(text: "Quote Selected Part", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QuoteSelected"), color: theme.contextMenu.primaryColor)
}, action: { [weak selfController, weak contentNode] _, f in
guard let selfController, let contentNode else {
return
}
guard let textSelection = contentNode.getCurrentTextSelection() else {
return
}
selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: EngineMessageReplyQuote(text: textSelection.text, entities: textSelection.entities))).withoutSelectionState() }) })
f(.default)
})))
//c.pushItems(items: .single(ContextController.Items(content: .list(subItems))))
let minHeight = c.getActionsMinHeight()
c.immediateItemsTransitionAnimation = false
c.setItems(.single(ContextController.Items(content: .list(subItems))), minHeight: minHeight, previousActionsTransition: .slide(forward: true))
break
}
}
}
})))
}
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Reply in Another Chat", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), 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 replySubject.quote != nil {
items.append(.action(ContextMenuActionItem(text: "Remove Quote", textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QuoteRemove"), color: theme.contextMenu.destructiveColor) }, action: { [weak selfController] c, f in
f(.default)
guard let selfController else {
return
}
var replySubject = replySubject
replySubject.quote = nil
selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) })
})))
}
return items
}
var tip: ContextController.Tip?
if "".isEmpty {
tip = .quoteSelection
}
return items |> map { ContextController.Items(content: .list($0), tip: tip) }
}
func presentChatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) {
guard let peerId = selfController.chatLocation.peerId else {
@ -229,51 +379,161 @@ func presentChatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASD
return
}
let replyOptionsSubject = Promise<ChatControllerSubject.ForwardOptions>()
replyOptionsSubject.set(.single(ChatControllerSubject.ForwardOptions(hideNames: false, hideCaptions: false, replyOptions: ChatControllerSubject.ReplyOptions(hasQuote: replySubject.quote != nil))))
//let presentationData = selfController.presentationData
let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [replySubject.messageId.peerId], ids: [replySubject.messageId], info: ChatControllerSubject.MessageOptionsInfo(kind: .reply), options: .single(ChatControllerSubject.ForwardOptions(hideNames: false, hideCaptions: false))), botStart: nil, mode: .standard(previewing: true))
var replyQuote: ChatControllerSubject.MessageOptionsInfo.ReplyQuote?
if let quote = replySubject.quote {
replyQuote = ChatControllerSubject.MessageOptionsInfo.ReplyQuote(messageId: replySubject.messageId, text: quote.text)
}
guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [replySubject.messageId.peerId], ids: [replySubject.messageId], info: ChatControllerSubject.MessageOptionsInfo(kind: .reply(initialQuote: replyQuote)), options: replyOptionsSubject.get()), botStart: nil, mode: .standard(previewing: true)) as? ChatControllerImpl else {
return
}
chatController.canReadHistory.set(false)
let messageIds: [EngineMessage.Id] = [replySubject.messageId]
let messagesCount: Signal<Int, NoError> = .single(1)
//let accountPeerId = selfController.context.account.peerId
let items = combineLatest(selfController.context.account.postbox.messagesAtIds(messageIds), messagesCount)
|> deliverOnMainQueue
|> map { [weak selfController] messages, messagesCount -> [ContextMenuItem] in
guard let selfController else {
return []
}
var items: [ContextMenuItem] = []
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Reply in Another Chat", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak selfController] c, f in
selfController?.interfaceInteraction?.forwardCurrentForwardMessages()
f(.default)
})))
return items
}
let items = generateChatReplyOptionItems(selfController: selfController, chatController: chatController)
selfController.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
selfController.canReadHistory.set(false)
let contextController = ContextController(presentationData: selfController.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(content: .list($0)) })
let contextController = ContextController(presentationData: selfController.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), items: items)
contextController.dismissed = { [weak selfController] in
selfController?.canReadHistory.set(true)
}
contextController.dismissedForCancel = { [weak selfController, weak chatController] in
guard let selfController else {
return
}
if let selectedMessageIds = (chatController as? ChatControllerImpl)?.selectedMessageIds {
var forwardMessageIds = selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? []
forwardMessageIds = forwardMessageIds.filter { selectedMessageIds.contains($0) }
selfController.updateChatPresentationInterfaceState(interactive: false, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds) }) })
}
contextController.dismissedForCancel = {
}
contextController.immediateItemsTransitionAnimation = true
selfController.presentInGlobalOverlay(contextController)
chatController.performTextSelectionAction = { [weak selfController, weak contextController] message, canCopy, text, action in
guard let selfController, let contextController else {
return
}
contextController.dismiss()
selfController.controllerInteraction?.performTextSelectionAction(message, canCopy, text, action)
}
}
func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubject: ChatInterfaceState.ReplyMessageSubject) {
let _ = selfController.presentVoiceMessageDiscardAlert(action: { [weak selfController] in
guard let selfController else {
return
}
let filter: ChatListNodePeersFilter = [.onlyWriteable, .includeSavedMessages, .excludeDisabled, .doNotSearchMessages]
var attemptSelectionImpl: ((EnginePeer) -> Void)?
let controller = selfController.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(
context: selfController.context,
updatedPresentationData: selfController.updatedPresentationData,
filter: filter,
hasFilters: true,
title: "Reply in...", //TODO:localize
attemptSelection: { peer, _ in
attemptSelectionImpl?(peer)
},
multipleSelection: false,
forwardedMessageIds: [],
selectForumThreads: true
))
let context = selfController.context
attemptSelectionImpl = { [weak selfController, weak controller] peer in
guard let selfController, let controller = controller else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
controller.present(textAlertController(context: context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: presentationData.strings.Forward_ErrorDisabledForChat, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
controller.peerSelected = { [weak selfController, weak controller] peer, threadId in
guard let selfController, let strongController = controller else {
return
}
let peerId = peer.id
//let accountPeerId = selfController.context.account.peerId
var isPinnedMessages = false
if case .pinnedMessages = selfController.presentationInterfaceState.subject {
isPinnedMessages = true
}
if case .peer(peerId) = selfController.chatLocation, selfController.parentController == nil, !isPinnedMessages {
selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) })
selfController.updateItemNodesSearchTextHighlightStates()
selfController.searchResultsController = nil
strongController.dismiss()
} else {
if let navigationController = selfController.navigationController as? NavigationController {
for controller in navigationController.viewControllers {
if let maybeChat = controller as? ChatControllerImpl {
if case .peer(peerId) = maybeChat.chatLocation {
var isChatPinnedMessages = false
if case .pinnedMessages = maybeChat.presentationInterfaceState.subject {
isChatPinnedMessages = true
}
if !isChatPinnedMessages {
maybeChat.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }) })
selfController.dismiss()
strongController.dismiss()
return
}
}
}
}
}
let _ = (ChatInterfaceState.update(engine: selfController.context.engine, peerId: peerId, threadId: threadId, { currentState in
return currentState.withUpdatedReplyMessageSubject(replySubject)
})
|> deliverOnMainQueue).startStandalone(completed: { [weak selfController] in
guard let selfController else {
return
}
let proceed: (ChatController) -> Void = { [weak selfController] chatController in
guard let selfController else {
return
}
selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil).withoutSelectionState() }) })
let navigationController: NavigationController?
if let parentController = selfController.parentController {
navigationController = (parentController.navigationController as? NavigationController)
} else {
navigationController = selfController.effectiveNavigationController
}
if let navigationController = navigationController {
var viewControllers = navigationController.viewControllers
if threadId != nil {
viewControllers.insert(chatController, at: viewControllers.count - 2)
} else {
viewControllers.insert(chatController, at: viewControllers.count - 1)
}
navigationController.setViewControllers(viewControllers, animated: false)
selfController.controllerNavigationDisposable.set((chatController.ready.get()
|> SwiftSignalKit.filter { $0 }
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak navigationController] _ in
viewControllers.removeAll(where: { $0 is PeerSelectionController })
navigationController?.setViewControllers(viewControllers, animated: true)
}))
}
}
if let threadId = threadId {
let _ = (selfController.context.sharedContext.chatControllerForForumThread(context: selfController.context, peerId: peerId, threadId: threadId)
|> deliverOnMainQueue).startStandalone(next: { chatController in
proceed(chatController)
})
} else {
proceed(ChatControllerImpl(context: selfController.context, chatLocation: .peer(id: peerId)))
}
})
}
}
selfController.chatDisplayNode.dismissInput()
selfController.effectiveNavigationController?.pushViewController(controller)
})
}

View File

@ -105,6 +105,7 @@ import PeerSelectionController
import SaveToCameraRoll
import ChatMessageDateAndStatusNode
import ReplyAccessoryPanelNode
import TextSelectionNode
public enum ChatControllerPeekActions {
case standard
@ -543,6 +544,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var avatarNode: ChatAvatarNavigationNode?
var storyStats: PeerStoryStats?
var performTextSelectionAction: ((Message?, Bool, NSAttributedString, TextSelectionAction) -> Void)?
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = []) {
let _ = ChatControllerCount.modify { value in
return value + 1
@ -3860,6 +3863,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else {
return
}
if let performTextSelectionAction = strongSelf.performTextSelectionAction {
performTextSelectionAction(message, canCopy, text, action)
return
}
switch action {
case .copy:
storeAttributedTextInPasteboard(text)
@ -4575,7 +4584,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else {
var subject: ChatControllerSubject?
if let messageId = messageId {
subject = .message(id: .id(messageId), highlight: true, timecode: nil)
subject = .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil)
}
navigationData = .chat(textInputState: nil, subject: subject, peekData: nil)
}
@ -5200,8 +5209,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
|> distinctUntilChanged
} else if case let .messageOptions(peerIds, messageIds, info, options) = subject {
let _ = info
displayedCountSignal = self.presentationInterfaceStatePromise.get()
|> map { state -> Int? in
if let selectionState = state.interfaceState.selectionState {
@ -5216,73 +5223,80 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|> take(1)
let presentationData = self.presentationData
subtitleTextSignal = combineLatest(peers, options, displayedCountSignal)
|> map { peersView, options, count in
let peers = peersView.peers.values
if !peers.isEmpty {
if peers.count == 1, let peer = peers.first {
if let peer = peer as? TelegramUser {
let displayName = EnginePeer(peer).compactDisplayTitle
if count == 1 {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_UserMessageForwardHidden(displayName).string
switch info.kind {
case .forward:
subtitleTextSignal = combineLatest(peers, options, displayedCountSignal)
|> map { peersView, options, count in
let peers = peersView.peers.values
if !peers.isEmpty {
if peers.count == 1, let peer = peers.first {
if let peer = peer as? TelegramUser {
let displayName = EnginePeer(peer).compactDisplayTitle
if count == 1 {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_UserMessageForwardHidden(displayName).string
} else {
return presentationData.strings.Conversation_ForwardOptions_UserMessageForwardVisible(displayName).string
}
} else {
return presentationData.strings.Conversation_ForwardOptions_UserMessageForwardVisible(displayName).string
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_UserMessagesForwardHidden(displayName).string
} else {
return presentationData.strings.Conversation_ForwardOptions_UserMessagesForwardVisible(displayName).string
}
}
} else if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if count == 1 {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_ChannelMessageForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_ChannelMessageForwardVisible
}
} else {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_ChannelMessagesForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_ChannelMessagesForwardVisible
}
}
} else {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_UserMessagesForwardHidden(displayName).string
if count == 1 {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_GroupMessageForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_GroupMessageForwardVisible
}
} else {
return presentationData.strings.Conversation_ForwardOptions_UserMessagesForwardVisible(displayName).string
}
}
} else if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if count == 1 {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_ChannelMessageForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_ChannelMessageForwardVisible
}
} else {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_ChannelMessagesForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_ChannelMessagesForwardVisible
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_GroupMessagesForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_GroupMessagesForwardVisible
}
}
}
} else {
if count == 1 {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_GroupMessageForwardHidden
return presentationData.strings.Conversation_ForwardOptions_RecipientsMessageForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_GroupMessageForwardVisible
return presentationData.strings.Conversation_ForwardOptions_RecipientsMessageForwardVisible
}
} else {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_GroupMessagesForwardHidden
return presentationData.strings.Conversation_ForwardOptions_RecipientsMessagesForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_GroupMessagesForwardVisible
return presentationData.strings.Conversation_ForwardOptions_RecipientsMessagesForwardVisible
}
}
}
} else {
if count == 1 {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_RecipientsMessageForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_RecipientsMessageForwardVisible
}
} else {
if options.hideNames {
return presentationData.strings.Conversation_ForwardOptions_RecipientsMessagesForwardHidden
} else {
return presentationData.strings.Conversation_ForwardOptions_RecipientsMessagesForwardVisible
}
}
return nil
}
} else {
return nil
}
case .reply:
//TODO:localize
subtitleTextSignal = .single("You can select a specific part to quote")
}
}
@ -5305,8 +5319,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if let peer = peerViewMainPeer(peerView) {
if case .messageOptions = presentationInterfaceState.subject {
if displayedCount == 1 {
if case let .messageOptions(_, _, info, _) = presentationInterfaceState.subject {
if case let .reply(initialQuote) = info.kind {
//TODO:localize
if initialQuote != nil {
strongSelf.chatTitleView?.titleContent = .custom("Reply to Quote", subtitleText, false)
} else {
strongSelf.chatTitleView?.titleContent = .custom("Reply to Message", subtitleText, false)
}
} else if displayedCount == 1 {
strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitleSingle, subtitleText, false)
} else {
strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitle(Int32(displayedCount ?? 1)), subtitleText, false)
@ -10508,7 +10529,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if let navigationController = strongSelf.effectiveNavigationController {
let subject: ChatControllerSubject? = sourceMessageId.flatMap { ChatControllerSubject.message(id: .id($0), highlight: true, timecode: nil) }
let subject: ChatControllerSubject? = sourceMessageId.flatMap { ChatControllerSubject.message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) }
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadResult), subject: subject, keepStack: .always))
}
}, activatePinnedListPreview: { [weak self] node, gesture in
@ -11902,6 +11923,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition)
}
override public func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? {
switch self.presentationInterfaceState.mode {
case let .standard(previewing):
if previewing {
if let subject = self.subject, case let .messageOptions(_, _, info, _) = subject, case .reply = info.kind {
return self.chatDisplayNode.preferredContentSizeForLayout(layout)
}
}
default:
break
}
return nil
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.suspendNavigationBarLayout = true
super.containerLayoutUpdated(layout, transition: transition)
@ -16412,7 +16447,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})))
}
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: false, timecode: nil), botStart: nil, mode: .standard(previewing: true))
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .message(id: .timestamp(timestamp), highlight: nil, timecode: nil), botStart: nil, mode: .standard(previewing: true))
chatController.canReadHistory.set(false)
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
@ -16545,9 +16580,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let subject: ChatControllerSubject?
if let atMessageId = atMessageId {
subject = .message(id: .id(atMessageId), highlight: true, timecode: nil)
subject = .message(id: .id(atMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil)
} else if let index = result.scrollToLowerBoundMessage {
subject = .message(id: .id(index.id), highlight: false, timecode: nil)
subject = .message(id: .id(index.id), highlight: nil, timecode: nil)
} else {
subject = nil
}
@ -16628,7 +16663,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else {
navigateToLocation = .peer(peer)
}
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: navigateToLocation, subject: .message(id: .id(messageId), highlight: true, timecode: nil), keepStack: .always))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: navigateToLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always))
})
} else if case let .peer(peerId) = self.chatLocation, let messageId = messageLocation.messageId, (messageId.peerId != peerId && !forceInCurrentChat) || (isScheduledMessages && messageId.id != 0 && !Namespaces.Message.allScheduled.contains(messageId.namespace)) {
let _ = (self.context.engine.data.get(
@ -16645,7 +16680,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false))
}
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: true, timecode: nil), keepStack: .always))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always))
}
})
} else if forceInCurrentChat {
@ -16833,7 +16868,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if let navigationController = strongSelf.effectiveNavigationController {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) }))
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) }))
}
})
completion?()
@ -16851,7 +16886,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
if let navigationController = self.effectiveNavigationController {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) }))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: messageLocation.messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) }))
}
completion?()
})

View File

@ -445,37 +445,42 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
var messageText = message.text
var messageMedia = message.media
var hasDice = false
if hideNames {
for media in message.media {
if options.hideCaptions {
if media is TelegramMediaImage || media is TelegramMediaFile {
messageText = ""
break
if case .forward = info.kind {
if hideNames {
for media in message.media {
if options.hideCaptions {
if media is TelegramMediaImage || media is TelegramMediaFile {
messageText = ""
break
}
}
if let poll = media as? TelegramMediaPoll {
var updatedMedia = message.media.filter { !($0 is TelegramMediaPoll) }
updatedMedia.append(TelegramMediaPoll(pollId: poll.pollId, publicity: poll.publicity, kind: poll.kind, text: poll.text, options: poll.options, correctAnswers: poll.correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil), isClosed: false, deadlineTimeout: nil))
messageMedia = updatedMedia
}
if let _ = media as? TelegramMediaDice {
hasDice = true
}
}
if let poll = media as? TelegramMediaPoll {
var updatedMedia = message.media.filter { !($0 is TelegramMediaPoll) }
updatedMedia.append(TelegramMediaPoll(pollId: poll.pollId, publicity: poll.publicity, kind: poll.kind, text: poll.text, options: poll.options, correctAnswers: poll.correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil), isClosed: false, deadlineTimeout: nil))
messageMedia = updatedMedia
}
if let _ = media as? TelegramMediaDice {
hasDice = true
}
}
var forwardInfo: MessageForwardInfo?
if let existingForwardInfo = message.forwardInfo {
forwardInfo = MessageForwardInfo(author: existingForwardInfo.author, source: existingForwardInfo.source, sourceMessageId: nil, date: 0, authorSignature: nil, psaType: nil, flags: [])
}
else {
forwardInfo = MessageForwardInfo(author: message.author, source: nil, sourceMessageId: nil, date: 0, authorSignature: nil, psaType: nil, flags: [])
}
if hideNames && !hasDice {
forwardInfo = nil
}
return message.withUpdatedFlags(flags).withUpdatedText(messageText).withUpdatedMedia(messageMedia).withUpdatedTimestamp(Int32(context.account.network.context.globalTime())).withUpdatedAttributes(attributes).withUpdatedAuthor(accountPeer).withUpdatedForwardInfo(forwardInfo)
} else {
return message
}
var forwardInfo: MessageForwardInfo?
if let existingForwardInfo = message.forwardInfo {
forwardInfo = MessageForwardInfo(author: existingForwardInfo.author, source: existingForwardInfo.source, sourceMessageId: nil, date: 0, authorSignature: nil, psaType: nil, flags: [])
}
else {
forwardInfo = MessageForwardInfo(author: message.author, source: nil, sourceMessageId: nil, date: 0, authorSignature: nil, psaType: nil, flags: [])
}
if hideNames && !hasDice {
forwardInfo = nil
}
return message.withUpdatedFlags(flags).withUpdatedText(messageText).withUpdatedMedia(messageMedia).withUpdatedTimestamp(Int32(context.account.network.context.globalTime())).withUpdatedAttributes(attributes).withUpdatedAuthor(accountPeer).withUpdatedForwardInfo(forwardInfo)
}
return (messages, Int32(messages.count), false)
@ -891,6 +896,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
}
func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? {
var height = self.historyNode.scroller.contentSize.height
height += 3.0
height = min(height, layout.size.height)
return CGSize(width: layout.size.width, height: height)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition protoTransition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets, CGFloat, Bool, @escaping () -> Void) -> Void, updateExtraNavigationBarBackgroundHeight: (CGFloat, CGFloat, ContainedViewLayoutTransition) -> Void) {
let transition: ContainedViewLayoutTransition
if let _ = self.scheduledAnimateInAsOverlayFromNode {

View File

@ -814,7 +814,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
initialSearchLocation = .index(MessageIndex.absoluteUpperBound())
}
}
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: initialSearchLocation, count: historyMessageCount, highlight: highlight), id: 0)
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: initialSearchLocation, count: historyMessageCount, highlight: highlight != nil), id: 0)
} else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: historyMessageCount, highlight: true), id: 0)
} else {
@ -1372,7 +1372,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
initialSearchLocation = .index(.absoluteUpperBound())
}
}
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: initialSearchLocation, count: historyMessageCount, highlight: highlight), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: initialSearchLocation, count: historyMessageCount, highlight: highlight != nil), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
} else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId {
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: historyMessageCount, highlight: true), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
} else if var chatHistoryLocation = strongSelf.chatHistoryLocationValue {

View File

@ -73,10 +73,12 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
replyPanelNode.interfaceInteraction = interfaceInteraction
replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
return replyPanelNode
} else {
let panelNode = ReplyAccessoryPanelNode(context: context, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer)
} else if let peerId = chatPresentationInterfaceState.chatLocation.peerId {
let panelNode = ReplyAccessoryPanelNode(context: context, chatPeerId: peerId, messageId: replyMessageSubject.messageId, quote: replyMessageSubject.quote, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, dateTimeFormat: chatPresentationInterfaceState.dateTimeFormat, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer)
panelNode.interfaceInteraction = interfaceInteraction
return panelNode
} else {
return nil
}
} else {
return nil

View File

@ -1536,7 +1536,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
guard let peer = messages[0].peers[messages[0].id.peerId] else {
return
}
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messages[0].id), highlight: true, timecode: nil), useExisting: true))
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messages[0].id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), useExisting: true))
})
})))
}

View File

@ -1237,6 +1237,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
var replyMessage: Message?
var replyForward: QuotedReplyMessageAttribute?
var replyQuote: EngineMessageReplyQuote?
var replyStory: StoryId?
for attribute in item.message.attributes {
@ -1265,6 +1266,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
replyMessage = item.message.associatedMessages[replyAttribute.messageId]
}
replyQuote = replyAttribute.quote
} else if let quoteReplyAttribute = attribute as? QuotedReplyMessageAttribute {
replyForward = quoteReplyAttribute
} else if let attribute = attribute as? ReplyStoryAttribute {
replyStory = attribute.storyId
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
@ -1272,7 +1275,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
}
var hasReply = replyMessage != nil || replyStory != nil
var hasReply = replyMessage != nil || replyForward != nil || replyStory != nil
if case let .peer(peerId) = item.chatLocation, (peerId == replyMessage?.id.peerId || item.message.threadId == 1), let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil {
if let threadId = item.message.threadId, let replyMessage = replyMessage, Int64(replyMessage.id.id) == threadId {
hasReply = false
@ -1292,13 +1295,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
))
}
if hasReply, (replyMessage != nil || replyStory != nil) {
if hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil) {
replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments(
presentationData: item.presentationData,
strings: item.presentationData.strings,
context: item.context,
type: .standalone,
message: replyMessage,
replyForward: replyForward,
quote: replyQuote,
story: replyStory,
parentMessage: item.message,

View File

@ -1525,6 +1525,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
var inlineBotNameString: String?
var replyMessage: Message?
var replyForward: QuotedReplyMessageAttribute?
var replyQuote: EngineMessageReplyQuote?
var replyStory: StoryId?
var replyMarkup: ReplyMarkupMessageAttribute?
@ -1543,6 +1544,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
replyMessage = firstMessage.associatedMessages[attribute.messageId]
}
replyQuote = attribute.quote
} else if let attribute = attribute as? QuotedReplyMessageAttribute {
replyForward = attribute
} else if let attribute = attribute as? ReplyStoryAttribute {
replyStory = attribute.storyId
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty && !isPreview {
@ -1778,7 +1781,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
hasForwardLikeContent = true
}
if inlineBotNameString == nil && (ignoreForward || !hasForwardLikeContent) && replyMessage == nil && replyStory == nil {
if inlineBotNameString == nil && (ignoreForward || !hasForwardLikeContent) && replyMessage == nil && replyForward == nil && replyStory == nil {
if let first = contentPropertiesAndLayouts.first, first.1.hidesSimpleAuthorHeader && !ignoreNameHiding {
if let author = firstMessage.author as? TelegramChannel, case .group = author.info, author.id == firstMessage.id.peerId, !incoming {
} else {
@ -1848,7 +1851,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
if firstMessage.media.contains(where: { $0 is TelegramMediaStory }) {
displayHeader = true
}
if replyMessage != nil || replyStory != nil {
if replyMessage != nil || replyForward != nil || replyStory != nil {
displayHeader = true
}
if !displayHeader, case .peer = item.chatLocation, let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil {
@ -2115,7 +2118,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
}
}
var hasReply = replyMessage != nil || replyStory != nil
var hasReply = replyMessage != nil || replyForward != nil || replyStory != nil
if !isInstantVideo, case let .peer(peerId) = item.chatLocation, (peerId == replyMessage?.id.peerId || item.message.threadId == 1), let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil {
if let threadId = item.message.threadId, let replyMessage = replyMessage, Int64(replyMessage.id.id) == threadId {
hasReply = false
@ -2147,7 +2150,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
}
}
if !isInstantVideo, hasReply, (replyMessage != nil || replyStory != nil) {
if !isInstantVideo, hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil) {
if headerSize.height.isZero {
headerSize.height += 10.0
} else {
@ -2159,6 +2162,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
context: item.context,
type: .bubble(incoming: incoming),
message: replyMessage,
replyForward: replyForward,
quote: replyQuote,
story: replyStory,
parentMessage: item.message,

View File

@ -444,6 +444,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
}
var replyMessage: Message?
var replyForward: QuotedReplyMessageAttribute?
var replyQuote: EngineMessageReplyQuote?
var replyStory: StoryId?
for attribute in item.message.attributes {
@ -488,6 +489,8 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
replyMessage = item.message.associatedMessages[replyAttribute.messageId]
}
replyQuote = replyAttribute.quote
} else if let attribute = attribute as? QuotedReplyMessageAttribute {
replyForward = attribute
} else if let attribute = attribute as? ReplyStoryAttribute {
replyStory = attribute.storyId
} else if let _ = attribute as? InlineBotMessageAttribute {
@ -496,13 +499,14 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
}
}
if replyMessage != nil || replyStory != nil {
if replyMessage != nil || replyForward != nil || replyStory != nil {
replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments(
presentationData: item.presentationData,
strings: item.presentationData.strings,
context: item.context,
type: .standalone,
message: replyMessage,
replyForward: replyForward,
quote: replyQuote,
story: replyStory,
parentMessage: item.message,

View File

@ -317,6 +317,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
if !ignoreHeaders {
var replyMessage: Message?
var replyForward: QuotedReplyMessageAttribute?
var replyQuote: EngineMessageReplyQuote?
var replyStory: StoryId?
@ -348,12 +349,14 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
replyMessage = item.message.associatedMessages[replyAttribute.messageId]
}
replyQuote = replyAttribute.quote
} else if let attribute = attribute as? QuotedReplyMessageAttribute {
replyForward = attribute
} else if let attribute = attribute as? ReplyStoryAttribute {
replyStory = attribute.storyId
}
}
if replyMessage != nil || replyStory != nil {
if replyMessage != nil || replyForward != nil || replyStory != nil {
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyMessage?.id {
} else {
replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments(
@ -362,6 +365,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
context: item.context,
type: .standalone,
message: replyMessage,
replyForward: replyForward,
quote: replyQuote,
story: replyStory,
parentMessage: item.message,

View File

@ -509,7 +509,13 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
let nodeLayout = node.asyncLayout()
let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem)
let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !self.disableDate)
var disableDate = self.disableDate
if let subject = self.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .reply = info.kind {
disableDate = true
}
let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !disableDate)
node.contentSize = layout.contentSize
node.insets = layout.insets

View File

@ -639,6 +639,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
var replyMessage: Message?
var replyForward: QuotedReplyMessageAttribute?
var replyQuote: EngineMessageReplyQuote?
var replyStory: StoryId?
for attribute in item.message.attributes {
@ -668,6 +669,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
replyMessage = item.message.associatedMessages[replyAttribute.messageId]
}
replyQuote = replyAttribute.quote
} else if let attribute = attribute as? QuotedReplyMessageAttribute {
replyForward = attribute
} else if let attribute = attribute as? ReplyStoryAttribute {
replyStory = attribute.storyId
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
@ -675,7 +678,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
}
}
var hasReply = replyMessage != nil || replyStory != nil
var hasReply = replyMessage != nil || replyForward != nil || replyStory != nil
if case let .peer(peerId) = item.chatLocation, (peerId == replyMessage?.id.peerId || item.message.threadId == 1), let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil {
if let threadId = item.message.threadId, let replyMessage = replyMessage, Int64(replyMessage.id.id) == threadId {
hasReply = false
@ -695,13 +698,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
))
}
if hasReply, (replyMessage != nil || replyStory != nil) {
if hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil) {
replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments(
presentationData: item.presentationData,
strings: item.presentationData.strings,
context: item.context,
type: .standalone,
message: replyMessage,
replyForward: replyForward,
quote: replyQuote,
story: replyStory,
parentMessage: item.message,

View File

@ -983,11 +983,11 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break
case let .channelMessage(peer, messageId, timecode):
if let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: true, timecode: timecode)))
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode)))
}
case let .replyThreadMessage(replyThreadMessage, messageId):
if let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadMessage), subject: .message(id: .id(messageId), highlight: true, timecode: nil)))
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadMessage), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil)))
}
case let .replyThread(messageId):
if let navigationController = strongSelf.getNavigationController() {

View File

@ -251,7 +251,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
switch item.content {
case let .peer(peerData):
if let message = peerData.messages.first {
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerData.peer.peerId), subject: .message(id: .id(message.id), highlight: true, timecode: nil), botStart: nil, mode: .standard(previewing: true))
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerData.peer.peerId), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), botStart: nil, mode: .standard(previewing: true))
chatController.canReadHistory.set(false)
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list([]))), gesture: gesture)
presentInGlobalOverlay(contextController)

View File

@ -3732,7 +3732,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
] as [UIAction])
let formatMenu = UIMenu(title: self.strings?.TextFormat_Format ?? "Format", image: nil, children: children)
actions.insert(formatMenu, at: 3)
actions.insert(formatMenu, at: 2)
}
return UIMenu(children: actions)
}

View File

@ -316,7 +316,7 @@ public func navigateToForumThreadImpl(context: AccountContext, peerId: EnginePee
context: context,
chatLocation: .replyThread(result.message),
chatLocationContextHolder: result.contextHolder,
subject: messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) },
subject: messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) },
activateInput: actualActivateInput,
keepStack: keepStack
)

View File

@ -187,7 +187,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
dismissInput()
navigationController?.pushViewController(controller)
case let .channelMessage(peer, messageId, timecode):
openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: true, timecode: timecode), peekData: nil))
openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode), peekData: nil))
case let .replyThreadMessage(replyThreadMessage, messageId):
if let navigationController = navigationController {
let _ = ChatControllerImpl.openMessageReplies(context: context, navigationController: navigationController, present: { c, a in
@ -979,7 +979,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
guard let peer else {
return
}
openPeer(peer, .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: true, timecode: nil), peekData: nil))
openPeer(peer, .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), peekData: nil))
})
},
shareLink: { link in

View File

@ -210,7 +210,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
self.isGlobalSearch = false
}
self.historyNode = ChatHistoryListNode(context: context, updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, source: source, subject: .message(id: .id(initialMessageId), highlight: true, timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch))
self.historyNode = ChatHistoryListNode(context: context, updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, source: source, subject: .message(id: .id(initialMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch))
self.historyNode.clipsToBounds = true
super.init()
@ -552,7 +552,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
let historyNode = ChatHistoryListNode(context: self.context, updatedPresentationData: (self.context.sharedContext.currentPresentationData.with({ $0 }), self.context.sharedContext.presentationData), chatLocation: self.chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, subject: .message(id: .id(messageId), highlight: true, timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch))
let historyNode = ChatHistoryListNode(context: self.context, updatedPresentationData: (self.context.sharedContext.currentPresentationData.with({ $0 }), self.context.sharedContext.presentationData), chatLocation: self.chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch))
historyNode.clipsToBounds = true
historyNode.preloadPages = true
historyNode.stackFromBottom = true

View File

@ -2442,7 +2442,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
let currentPeerId = strongSelf.peerId
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: true, timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {
var viewControllers = navigationController.viewControllers
var indexesToRemove = Set<Int>()
var keptCurrentChatController = false
@ -2604,7 +2604,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
let currentPeerId = strongSelf.peerId
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: true, timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always, useExisting: false, purposefulAction: {
var viewControllers = navigationController.viewControllers
var indexesToRemove = Set<Int>()
var keptCurrentChatController = false
@ -9333,7 +9333,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
chatController: nil,
context: strongSelf.context,
chatLocation: .peer(EnginePeer(peer)),
subject: .message(id: .id(index.id), highlight: false, timecode: nil),
subject: .message(id: .id(index.id), highlight: nil, timecode: nil),
botStart: nil,
updateTextInputState: nil,
keepStack: .never,
@ -9354,7 +9354,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
))
})))
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: .message(id: .id(index.id), highlight: false, timecode: nil), botStart: nil, mode: .standard(previewing: true))
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: .message(id: .id(index.id), highlight: nil, timecode: nil), botStart: nil, mode: .standard(previewing: true))
chatController.canReadHistory.set(false)
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
strongSelf.controller?.presentInGlobalOverlay(contextController)

View File

@ -69,7 +69,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n
openResolvedPeerImpl(EnginePeer(peer), .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive)))
case let .channelMessage(peer, messageId, timecode):
if let navigationController = controller.navigationController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: true, timecode: timecode)))
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode)))
}
case let .replyThreadMessage(replyThreadMessage, messageId):
if let navigationController = controller.navigationController as? NavigationController {

View File

@ -235,13 +235,18 @@ public final class TextSelectionNode: ASDisplayNode {
public private(set) var recognizer: TextSelectionGestureRecognizer?
private var displayLinkAnimator: DisplayLinkAnimator?
public var enableCopy: Bool = true
public var enableLookup: Bool = true
public var enableQuote: Bool = false
public var enableTranslate: Bool = true
public var enableShare: Bool = true
public var didRecognizeTap: Bool {
return self.recognizer?.didRecognizeTap ?? false
}
private weak var contextMenu: ContextMenuController?
public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNode, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: @escaping () -> ASDisplayNode?, externalKnobSurface: UIView? = nil, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) {
self.theme = theme
self.strings = strings
@ -438,6 +443,24 @@ public final class TextSelectionNode: ASDisplayNode {
self.displayLinkAnimator = displayLinkAnimator
}
public func setSelection(range: NSRange, displayMenu: Bool) {
self.currentRange = (range.lowerBound, range.upperBound)
self.updateSelection(range: range, animateIn: true)
self.updateIsActive(true)
if displayMenu {
self.displayMenu()
}
}
public func getSelection() -> NSRange? {
guard let currentRange = self.currentRange else {
return nil
}
let range = NSRange(location: min(currentRange.0, currentRange.1), length: max(currentRange.0, currentRange.1) - min(currentRange.0, currentRange.1))
return range
}
private func updateSelection(range: NSRange?, animateIn: Bool) {
self.updateRange?(range)
@ -570,10 +593,12 @@ public final class TextSelectionNode: ASDisplayNode {
}
var actions: [ContextMenuAction] = []
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
self?.performAction(string, .copy)
self?.cancelSelection()
}))
if self.enableCopy {
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
self?.performAction(string, .copy)
self?.cancelSelection()
}))
}
if self.enableQuote {
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuQuote, accessibilityLabel: self.strings.Conversation_ContextMenuQuote), action: { [weak self] in
self?.performAction(string, .quote(range: range.lowerBound ..< range.upperBound))
@ -586,10 +611,12 @@ public final class TextSelectionNode: ASDisplayNode {
}))
}
if #available(iOS 15.0, *) {
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in
self?.performAction(string, .translate)
self?.cancelSelection()
}))
if self.enableTranslate {
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in
self?.performAction(string, .translate)
self?.cancelSelection()
}))
}
}
// if isSpeakSelectionEnabled() {
// actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in
@ -597,16 +624,41 @@ public final class TextSelectionNode: ASDisplayNode {
// self?.dismissSelection()
// }))
// }
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
self?.performAction(string, .share)
self?.cancelSelection()
}))
self.present(ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
let realFullRange = NSRange(location: 0, length: attributedString.length)
if range != realFullRange {
//TODO:localize
actions.append(ContextMenuAction(content: .text(title: "Select All", accessibilityLabel: "Select All"), action: { [weak self] in
guard let self else {
return
}
self.contextMenu?.dismiss()
self.setSelection(range: realFullRange, displayMenu: true)
}))
} else if self.enableShare {
actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
self?.performAction(string, .share)
self?.cancelSelection()
}))
}
let contextMenu = ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false)
contextMenu.dismissOnTap = { [weak self] view, point in
guard let self else {
return true
}
if self.knobAtPoint(view.convert(point, to: self.view)) == nil {
//self.cancelSelection()
return true
}
return true
}
self.contextMenu = contextMenu
self.present(contextMenu, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
guard let strongSelf = self, let rootNode = strongSelf.rootNode() else {
return nil
}
return (strongSelf, completeRect, rootNode, rootNode.bounds)
return (strongSelf, completeRect, rootNode, rootNode.bounds.insetBy(dx: 0.0, dy: -100.0))
}, bounce: false))
}

View File

@ -831,7 +831,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
return .replyThreadMessage(replyThreadMessage: result, messageId: messageId)
}
} else {
return .single(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: true, timecode: timecode), peekData: nil)))
return .single(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode), peekData: nil)))
}
} else {
return .single(.inaccessiblePeer)