[WIP] Quotes

This commit is contained in:
Ali 2023-10-13 15:24:53 +04:00
parent 50881b558f
commit a753d71cd7
46 changed files with 2076 additions and 606 deletions

View File

@ -517,30 +517,50 @@ 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 var hideNames: Bool
public var hideCaptions: Bool
public var replyOptions: ReplyOptions?
public init(hideNames: Bool, hideCaptions: Bool, replyOptions: ReplyOptions?) {
public init(hideNames: Bool, hideCaptions: Bool) {
self.hideNames = hideNames
self.hideCaptions = hideCaptions
self.replyOptions = replyOptions
}
}
public struct MessageOptionsInfo: Equatable {
public struct ReplyQuote: Equatable {
public struct LinkOptions: Equatable {
public var messageText: String
public var messageEntities: [MessageTextEntity]
public var replyMessageId: EngineMessage.Id?
public var replyQuote: String?
public var url: String
public var webpage: TelegramMediaWebpage
public var linkBelowText: Bool
public var largeMedia: Bool
public init(
messageText: String,
messageEntities: [MessageTextEntity],
replyMessageId: EngineMessage.Id?,
replyQuote: String?,
url: String,
webpage: TelegramMediaWebpage,
linkBelowText: Bool,
largeMedia: Bool
) {
self.messageText = messageText
self.messageEntities = messageEntities
self.replyMessageId = replyMessageId
self.replyQuote = replyQuote
self.url = url
self.webpage = webpage
self.linkBelowText = linkBelowText
self.largeMedia = largeMedia
}
}
public enum MessageOptionsInfo: Equatable {
public struct Quote: Equatable {
public let messageId: EngineMessage.Id
public let text: String
@ -550,16 +570,61 @@ public enum ChatControllerSubject: Equatable {
}
}
public enum Kind: Equatable {
case forward
case reply(initialQuote: ReplyQuote?)
public struct SelectionState: Equatable {
public var quote: Quote?
public init(quote: Quote?) {
self.quote = quote
}
}
public let kind: Kind
public init(kind: Kind) {
self.kind = kind
public struct Reply: Equatable {
public var quote: Quote?
public var selectionState: Promise<SelectionState>
public init(quote: Quote?, selectionState: Promise<SelectionState>) {
self.quote = quote
self.selectionState = selectionState
}
public static func ==(lhs: Reply, rhs: Reply) -> Bool {
if lhs.quote != rhs.quote {
return false
}
if lhs.selectionState !== rhs.selectionState {
return false
}
return true
}
}
public struct Forward: Equatable {
public var options: Signal<ForwardOptions, NoError>
public init(options: Signal<ForwardOptions, NoError>) {
self.options = options
}
public static func ==(lhs: Forward, rhs: Forward) -> Bool {
return true
}
}
public struct Link: Equatable {
public var options: Signal<LinkOptions, NoError>
public init(options: Signal<LinkOptions, NoError>) {
self.options = options
}
public static func ==(lhs: Link, rhs: Link) -> Bool {
return true
}
}
case reply(Reply)
case forward(Forward)
case link(Link)
}
public struct MessageHighlight: Equatable {
@ -573,7 +638,7 @@ public enum ChatControllerSubject: Equatable {
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>)
case messageOptions(peerIds: [EnginePeer.Id], ids: [EngineMessage.Id], info: MessageOptionsInfo)
public static func ==(lhs: ChatControllerSubject, rhs: ChatControllerSubject) -> Bool {
switch lhs {
@ -595,8 +660,8 @@ public enum ChatControllerSubject: Equatable {
} else {
return false
}
case let .messageOptions(lhsPeerIds, lhsIds, lhsInfo, _):
if case let .messageOptions(rhsPeerIds, rhsIds, rhsInfo, _) = rhs, lhsPeerIds == rhsPeerIds, lhsIds == rhsIds, lhsInfo == rhsInfo {
case let .messageOptions(lhsPeerIds, lhsIds, lhsInfo):
if case let .messageOptions(rhsPeerIds, rhsIds, rhsInfo) = rhs, lhsPeerIds == rhsPeerIds, lhsIds == rhsIds, lhsInfo == rhsInfo {
return true
} else {
return false

View File

@ -779,6 +779,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
}
}, presentForwardOptions: { _ in
}, presentReplyOptions: { _ in
}, presentLinkOptions: { _ in
}, shareSelectedMessages: {
}, updateTextInputStateAndMode: { [weak self] f in
if let strongSelf = self {

View File

@ -3603,7 +3603,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
return nil
case .links:
var media: [EngineMedia] = []
media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil)))))
media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil, displayOptions: .default)))))
let message = EngineMessage(
stableId: 0,
stableVersion: 0,

View File

@ -81,6 +81,7 @@ public final class ChatPanelInterfaceInteraction {
public let updateForwardOptionsState: ((ChatInterfaceForwardOptionsState) -> ChatInterfaceForwardOptionsState) -> Void
public let presentForwardOptions: (ASDisplayNode) -> Void
public let presentReplyOptions: (ASDisplayNode) -> Void
public let presentLinkOptions: (ASDisplayNode) -> Void
public let shareSelectedMessages: () -> Void
public let updateTextInputStateAndMode: (@escaping (ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void
public let updateInputModeAndDismissedButtonKeyboardMessageId: ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void
@ -186,6 +187,7 @@ public final class ChatPanelInterfaceInteraction {
updateForwardOptionsState: @escaping ((ChatInterfaceForwardOptionsState) -> ChatInterfaceForwardOptionsState) -> Void,
presentForwardOptions: @escaping (ASDisplayNode) -> Void,
presentReplyOptions: @escaping (ASDisplayNode) -> Void,
presentLinkOptions: @escaping (ASDisplayNode) -> Void,
shareSelectedMessages: @escaping () -> Void,
updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void,
updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void,
@ -290,6 +292,7 @@ public final class ChatPanelInterfaceInteraction {
self.updateForwardOptionsState = updateForwardOptionsState
self.presentForwardOptions = presentForwardOptions
self.presentReplyOptions = presentReplyOptions
self.presentLinkOptions = presentLinkOptions
self.shareSelectedMessages = shareSelectedMessages
self.updateTextInputStateAndMode = updateTextInputStateAndMode
self.updateInputModeAndDismissedButtonKeyboardMessageId = updateInputModeAndDismissedButtonKeyboardMessageId
@ -402,6 +405,7 @@ public final class ChatPanelInterfaceInteraction {
}, updateForwardOptionsState: { _ in
}, presentForwardOptions: { _ in
}, presentReplyOptions: { _ in
}, presentLinkOptions: { _ in
}, shareSelectedMessages: {
}, updateTextInputStateAndMode: updateTextInputStateAndMode, updateInputModeAndDismissedButtonKeyboardMessageId: updateInputModeAndDismissedButtonKeyboardMessageId, openStickers: {
}, editMessage: {

View File

@ -25,6 +25,9 @@ swift_library(
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
"//submodules/UndoUI:UndoUI",
"//submodules/AnimationUI:AnimationUI",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/TabSelectorComponent",
],
visibility = [
"//visibility:public",

View File

@ -232,14 +232,19 @@ func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) ->
return targetWindowFrame
}
private final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
private weak var controller: ContextController?
private var presentationData: PresentationData
private let source: ContextContentSource
private var items: Signal<ContextController.Items, NoError>
private let beginDismiss: (ContextMenuActionResult) -> Void
private let configuration: ContextController.Configuration
private let legacySource: ContextContentSource
private var legacyItems: Signal<ContextController.Items, NoError>
let beginDismiss: (ContextMenuActionResult) -> Void
private let beganAnimatingOut: () -> Void
private let attemptTransitionControllerIntoNavigation: () -> Void
fileprivate var dismissedForCancel: (() -> Void)?
var dismissedForCancel: (() -> Void)?
private let getController: () -> ContextControllerProtocol?
private weak var gesture: ContextGesture?
@ -260,8 +265,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
private let dismissNode: ASDisplayNode
private let dismissAccessibilityArea: AccessibilityAreaNode
private var presentationNode: ContextControllerPresentationNode?
private var currentPresentationStateTransition: ContextControllerPresentationNodeStateTransition?
private var sourceContainer: ContextSourceContainer?
private let clippingNode: ASDisplayNode
private let scrollNode: ASScrollNode
@ -288,32 +292,33 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
private let blurBackground: Bool
var overlayWantsToBeBelowKeyboard: Bool {
if let presentationNode = self.presentationNode {
return presentationNode.wantsDisplayBelowKeyboard()
} else {
guard let sourceContainer = self.sourceContainer else {
return false
}
return sourceContainer.overlayWantsToBeBelowKeyboard
}
init(
controller: ContextController,
presentationData: PresentationData,
source: ContextContentSource,
items: Signal<ContextController.Items, NoError>,
configuration: ContextController.Configuration,
beginDismiss: @escaping (ContextMenuActionResult) -> Void,
recognizer: TapLongTapOrDoubleTapGestureRecognizer?,
gesture: ContextGesture?,
beganAnimatingOut: @escaping () -> Void,
attemptTransitionControllerIntoNavigation: @escaping () -> Void
) {
self.controller = controller
self.presentationData = presentationData
self.source = source
self.items = items
self.configuration = configuration
self.beginDismiss = beginDismiss
self.beganAnimatingOut = beganAnimatingOut
self.attemptTransitionControllerIntoNavigation = attemptTransitionControllerIntoNavigation
self.gesture = gesture
self.legacySource = configuration.sources[0].source
self.legacyItems = configuration.sources[0].items
self.getController = { [weak controller] in
return controller
}
@ -359,10 +364,12 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
var updateLayout: (() -> Void)?
var blurBackground = true
if case .reference = source {
blurBackground = false
} else if case let .extracted(extractedSource) = source, !extractedSource.blurBackground {
blurBackground = false
if let mainSource = configuration.sources.first(where: { $0.id == configuration.initialId }) {
if case .reference = mainSource.source {
blurBackground = false
} else if case let .extracted(extractedSource) = mainSource.source, !extractedSource.blurBackground {
blurBackground = false
}
}
self.blurBackground = blurBackground
@ -423,9 +430,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
}
if strongSelf.didMoveFromInitialGesturePoint {
if let presentationNode = strongSelf.presentationNode {
let presentationPoint = strongSelf.view.convert(localPoint, to: presentationNode.view)
presentationNode.highlightGestureMoved(location: presentationPoint, hover: false)
if let sourceContainer = strongSelf.sourceContainer {
let presentationPoint = strongSelf.view.convert(localPoint, to: sourceContainer.view)
sourceContainer.highlightGestureMoved(location: presentationPoint, hover: false)
} else {
let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view)
let actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint)
@ -447,8 +454,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
recognizer.externalUpdated = nil
if strongSelf.didMoveFromInitialGesturePoint {
if let presentationNode = strongSelf.presentationNode {
presentationNode.highlightGestureFinished(performAction: viewAndPoint != nil)
if let sourceContainer = strongSelf.sourceContainer {
sourceContainer.highlightGestureFinished(performAction: viewAndPoint != nil)
} else {
if let (_, _) = viewAndPoint {
if let highlightedActionNode = strongSelf.highlightedActionNode {
@ -485,9 +492,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
}
if strongSelf.didMoveFromInitialGesturePoint {
if let presentationNode = strongSelf.presentationNode {
let presentationPoint = strongSelf.view.convert(localPoint, to: presentationNode.view)
presentationNode.highlightGestureMoved(location: presentationPoint, hover: false)
if let sourceContainer = strongSelf.sourceContainer {
let presentationPoint = strongSelf.view.convert(localPoint, to: sourceContainer.view)
sourceContainer.highlightGestureMoved(location: presentationPoint, hover: false)
} else {
let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view)
var actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint)
@ -513,8 +520,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
gesture.externalUpdated = nil
if strongSelf.didMoveFromInitialGesturePoint {
if let presentationNode = strongSelf.presentationNode {
presentationNode.highlightGestureFinished(performAction: viewAndPoint != nil)
if let sourceContainer = strongSelf.sourceContainer {
sourceContainer.highlightGestureFinished(performAction: viewAndPoint != nil)
} else {
if let (_, _) = viewAndPoint {
if let highlightedActionNode = strongSelf.highlightedActionNode {
@ -532,22 +539,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
}
switch source {
case .location, .reference, .extracted:
self.contentReady.set(.single(true))
case let .controller(source):
self.contentReady.set(source.controller.ready.get())
//TODO:
//self.contentReady.set(.single(true))
}
self.initializeContent()
self.itemsDisposable.set((items
|> deliverOnMainQueue).start(next: { [weak self] items in
self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale)
}))
self.dismissAccessibilityArea.activate = { [weak self] in
self?.dimNodeTapped()
return true
@ -593,9 +586,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
switch gestureRecognizer.state {
case .changed:
if let presentationNode = self.presentationNode {
let presentationPoint = self.view.convert(localPoint, to: presentationNode.view)
presentationNode.highlightGestureMoved(location: presentationPoint, hover: true)
if let sourceContainer = self.sourceContainer {
let presentationPoint = self.view.convert(localPoint, to: sourceContainer.view)
sourceContainer.highlightGestureMoved(location: presentationPoint, hover: true)
} else {
let actionPoint = self.view.convert(localPoint, to: self.actionsContainerNode.view)
let actionNode = self.actionsContainerNode.actionNode(at: actionPoint)
@ -608,8 +601,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
}
case .ended, .cancelled:
if let presentationNode = self.presentationNode {
presentationNode.highlightGestureMoved(location: CGPoint(x: -1, y: -1), hover: true)
if let sourceContainer = self.sourceContainer {
sourceContainer.highlightGestureMoved(location: CGPoint(x: -1, y: -1), hover: true)
} else {
if let highlightedActionNode = self.highlightedActionNode {
self.highlightedActionNode = nil
@ -622,230 +615,86 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
private func initializeContent() {
switch self.source {
case let .location(source):
let presentationNode = ContextControllerExtractedPresentationNode(
getController: { [weak self] in
return self?.getController()
},
requestUpdate: { [weak self] transition in
guard let strongSelf = self else {
return
if self.configuration.sources.count == 1 {
switch self.configuration.sources[0].source {
case .location:
break
case let .reference(source):
if let controller = self.getController() as? ContextController, controller.workaroundUseLegacyImplementation {
self.contentReady.set(.single(true))
let transitionInfo = source.transitionInfo()
if let transitionInfo = transitionInfo {
let referenceView = transitionInfo.referenceView
self.contentContainerNode.contentNode = .reference(view: referenceView)
self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace
self.customPosition = transitionInfo.customPosition
var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view)
projectedFrame.origin.x += transitionInfo.insets.left
projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right
projectedFrame.origin.y += transitionInfo.insets.top
projectedFrame.size.width -= transitionInfo.insets.top + transitionInfo.insets.bottom
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
}
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: .location(source)
)
self.presentationNode = presentationNode
self.addSubnode(presentationNode)
case let .reference(source):
if let controller = self.getController() as? ContextController, controller.workaroundUseLegacyImplementation {
let transitionInfo = source.transitionInfo()
if let transitionInfo = transitionInfo {
let referenceView = transitionInfo.referenceView
self.contentContainerNode.contentNode = .reference(view: referenceView)
self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace
self.customPosition = transitionInfo.customPosition
var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view)
projectedFrame.origin.x += transitionInfo.insets.left
projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right
projectedFrame.origin.y += transitionInfo.insets.top
projectedFrame.size.width -= transitionInfo.insets.top + transitionInfo.insets.bottom
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: .reference(source)
)
self.presentationNode = presentationNode
self.addSubnode(presentationNode)
}
case let .extracted(source):
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: .extracted(source)
)
self.presentationNode = presentationNode
self.addSubnode(presentationNode)
case let .controller(source):
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)
self.itemsDisposable.set((self.configuration.sources[0].items
|> deliverOnMainQueue).start(next: { [weak self] items in
self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale)
}))
let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view)
self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame)
return
}
case .extracted:
break
case let .controller(source):
if let controller = self.getController() as? ContextController, controller.workaroundUseLegacyImplementation {
self.contentReady.set(source.controller.ready.get())
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)
}
self.itemsDisposable.set((self.configuration.sources[0].items
|> deliverOnMainQueue).start(next: { [weak self] items in
self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale)
}))
return
}
} 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)
}
}
if let controller = self.controller {
let sourceContainer = ContextSourceContainer(controller: controller, configuration: self.configuration)
self.contentReady.set(sourceContainer.ready.get())
self.itemsReady.set(.single(true))
self.sourceContainer = sourceContainer
self.addSubnode(sourceContainer)
}
}
func animateIn() {
self.gesture?.endPressedAppearance()
self.hapticFeedback.impact()
if let _ = self.presentationNode {
if let sourceContainer = self.sourceContainer {
self.didCompleteAnimationIn = true
self.currentPresentationStateTransition = .animateIn
if let validLayout = self.validLayout {
self.updateLayout(
layout: validLayout,
transition: .animated(duration: 0.5, curve: .spring),
previousActionsContainerNode: nil
)
}
sourceContainer.animateIn()
return
}
switch self.source {
switch self.legacySource {
case .location, .reference:
break
case .extracted:
@ -935,7 +784,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
case let .extracted(extracted, keepInPlace):
let springDuration: Double = 0.42 * animationDurationFactor
var springDamping: CGFloat = 104.0
if case let .extracted(source) = self.source, source.centerVertically {
if case let .extracted(source) = self.legacySource, source.centerVertically {
springDamping = 124.0
}
@ -949,7 +798,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
var actionsDuration = springDuration
var actionsOffset: CGFloat = 0.0
var contentDuration = springDuration
if case let .extracted(source) = self.source, source.centerVertically {
if case let .extracted(source) = self.legacySource, source.centerVertically {
actionsOffset = -(originalProjectedContentViewFrame.1.height - originalProjectedContentViewFrame.0.height) * 0.57
actionsDuration *= 1.0
contentDuration *= 0.9
@ -988,7 +837,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.contentContainerNode.layer.animateSpring(from: min(localSourceFrame.width / self.contentContainerNode.frame.width, localSourceFrame.height / self.contentContainerNode.frame.height) as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
switch self.source {
switch self.legacySource {
case let .controller(controller):
controller.animatedIn()
default:
@ -1024,28 +873,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.beganAnimatingOut()
if let _ = self.presentationNode {
self.currentPresentationStateTransition = .animateOut(result: initialResult, completion: completion)
if let validLayout = self.validLayout {
if case let .custom(transition) = initialResult {
self.delayLayoutUpdate = true
Queue.mainQueue().after(0.1) {
self.delayLayoutUpdate = false
self.updateLayout(
layout: validLayout,
transition: transition,
previousActionsContainerNode: nil
)
self.isAnimatingOut = true
}
} else {
self.updateLayout(
layout: validLayout,
transition: .animated(duration: 0.35, curve: .easeInOut),
previousActionsContainerNode: nil
)
}
}
if let sourceContainer = self.sourceContainer {
sourceContainer.animateOut(result: initialResult, completion: completion)
return
}
@ -1054,7 +883,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
var result = initialResult
switch self.source {
switch self.legacySource {
case let .location(source):
let transitionInfo = source.transitionInfo()
if transitionInfo == nil {
@ -1296,7 +1125,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
var actionsOffset: CGFloat = 0.0
if case let .extracted(source) = self.source, source.centerVertically {
if case let .extracted(source) = self.legacySource, source.centerVertically {
actionsOffset = -localSourceFrame.width * 0.6
}
@ -1469,24 +1298,23 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
if let presentationNode = self.presentationNode {
presentationNode.addRelativeContentOffset(offset, transition: transition)
if let sourceContainer = self.sourceContainer {
sourceContainer.addRelativeContentOffset(offset, transition: transition)
}
}
func cancelReactionAnimation() {
if let presentationNode = self.presentationNode {
presentationNode.cancelReactionAnimation()
if let sourceContainer = self.sourceContainer {
sourceContainer.cancelReactionAnimation()
}
}
func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) {
if let presentationNode = self.presentationNode {
presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: completion)
if let sourceContainer = self.sourceContainer {
sourceContainer.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: completion)
}
}
func getActionsMinHeight() -> ContextController.ActionsHeight? {
if !self.actionsContainerNode.bounds.height.isZero {
return ContextController.ActionsHeight(
@ -1499,20 +1327,24 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
func setItemsSignal(items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) {
self.items = items
self.itemsDisposable.set((items
|> deliverOnMainQueue).start(next: { [weak self] items in
guard let strongSelf = self else {
return
}
strongSelf.setItems(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition)
}))
if let sourceContainer = self.sourceContainer {
sourceContainer.setItems(items: items, animated: false)
} else {
self.legacyItems = items
self.itemsDisposable.set((items
|> deliverOnMainQueue).start(next: { [weak self] items in
guard let strongSelf = self else {
return
}
strongSelf.setItems(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition)
}))
}
}
private func setItems(items: ContextController.Items, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) {
if let presentationNode = self.presentationNode {
if let sourceContainer = self.sourceContainer {
let disableAnimations = self.getController()?.immediateItemsTransitionAnimation == true
presentationNode.replaceItems(items: items, animated: self.didCompleteAnimationIn && !disableAnimations)
sourceContainer.setItems(items: .single(items), animated: !disableAnimations)
if !self.didSetItemsReady {
self.didSetItemsReady = true
@ -1554,18 +1386,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
func pushItems(items: Signal<ContextController.Items, NoError>) {
self.itemsDisposable.set((items
|> deliverOnMainQueue).start(next: { [weak self] items in
guard let strongSelf = self, let presentationNode = strongSelf.presentationNode else {
return
}
presentationNode.pushItems(items: items)
}))
if let sourceContainer = self.sourceContainer {
sourceContainer.pushItems(items: items)
}
}
func popItems() {
if let presentationNode = self.presentationNode {
presentationNode.popItems()
if let sourceContainer = self.sourceContainer {
sourceContainer.popItems()
}
}
@ -1593,16 +1421,12 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.validLayout = layout
let presentationStateTransition = self.currentPresentationStateTransition
self.currentPresentationStateTransition = .none
if let presentationNode = self.presentationNode {
transition.updateFrame(node: presentationNode, frame: CGRect(origin: CGPoint(), size: layout.size))
presentationNode.update(
if let sourceContainer = self.sourceContainer {
transition.updateFrame(node: sourceContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
sourceContainer.update(
presentationData: self.presentationData,
layout: layout,
transition: transition,
stateTransition: presentationStateTransition
transition: transition
)
return
}
@ -1618,8 +1442,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
switch layout.metrics.widthClass {
case .compact:
if case .reference = self.source {
} else if case let .extracted(extractedSource) = self.source, !extractedSource.blurBackground {
if case .reference = self.legacySource {
} else if case let .extracted(extractedSource) = self.legacySource, !extractedSource.blurBackground {
} else if self.effectView.superview == nil {
self.view.insertSubview(self.effectView, at: 0)
if #available(iOS 10.0, *) {
@ -1634,8 +1458,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
self.dimNode.isHidden = false
self.withoutBlurDimNode.isHidden = true
case .regular:
if case .reference = self.source {
} else if case let .extracted(extractedSource) = self.source, !extractedSource.blurBackground {
if case .reference = self.legacySource {
} else if case let .extracted(extractedSource) = self.legacySource, !extractedSource.blurBackground {
} else if self.effectView.superview != nil {
self.effectView.removeFromSuperview()
self.withoutBlurDimNode.alpha = 1.0
@ -1744,7 +1568,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
case let .extracted(contentParentNode, keepInPlace):
var centerVertically = false
if case let .extracted(source) = self.source, source.centerVertically {
if case let .extracted(source) = self.legacySource, source.centerVertically {
centerVertically = true
}
let contentActionsSpacing: CGFloat = keepInPlace ? 16.0 : 8.0
@ -1896,7 +1720,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
case let .controller(contentParentNode):
var projectedFrame: CGRect = convertFrame(contentParentNode.sourceView.bounds, from: contentParentNode.sourceView, to: self.view)
switch self.source {
switch self.legacySource {
case let .controller(source):
let transitionInfo = source.transitionInfo()
if let (sourceView, sourceRect) = transitionInfo?.sourceNode() {
@ -2127,8 +1951,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
}
if let presentationNode = self.presentationNode {
return presentationNode.hitTest(self.view.convert(point, to: presentationNode.view), with: event)
if let sourceContainer = self.sourceContainer {
return sourceContainer.hitTest(self.view.convert(point, to: sourceContainer.view), with: event)
}
let mappedPoint = self.view.convert(point, to: self.scrollNode.view)
@ -2140,7 +1964,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
maybePassthrough = passthroughTouchEvent(self.view, point)
}
case let .extracted(contentParentNode, _):
if case let .extracted(source) = self.source {
if case let .extracted(source) = self.legacySource {
if !source.ignoreContentTouches {
let contentPoint = self.view.convert(point, to: contentParentNode.contentNode.view)
if let result = contentParentNode.contentNode.customHitTest?(contentPoint) {
@ -2156,7 +1980,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
case let .controller(controller):
var passthrough = false
switch self.source {
switch self.legacySource {
case let .controller(controllerSource):
passthrough = controllerSource.passthroughTouches
default:
@ -2165,9 +1989,6 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
if passthrough {
let controllerPoint = self.view.convert(point, to: controller.controller.view)
if let result = controller.controller.view.hitTest(controllerPoint, with: event) {
#if DEBUG
//return controller.view
#endif
return result
}
}
@ -2198,15 +2019,15 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
fileprivate func performHighlightedAction() {
self.presentationNode?.highlightGestureFinished(performAction: true)
self.sourceContainer?.performHighlightedAction()
}
fileprivate func decreaseHighlightedIndex() {
self.presentationNode?.decreaseHighlightedIndex()
self.sourceContainer?.decreaseHighlightedIndex()
}
fileprivate func increaseHighlightedIndex() {
self.presentationNode?.increaseHighlightedIndex()
self.sourceContainer?.increaseHighlightedIndex()
}
}
@ -2374,6 +2195,30 @@ public protocol ContextControllerItemsContent: AnyObject {
}
public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol, KeyShortcutResponder {
public final class Source {
public let id: AnyHashable
public let title: String
public let source: ContextContentSource
public let items: Signal<ContextController.Items, NoError>
public init(id: AnyHashable, title: String, source: ContextContentSource, items: Signal<ContextController.Items, NoError>) {
self.id = id
self.title = title
self.source = source
self.items = items
}
}
public final class Configuration {
public let sources: [Source]
public let initialId: AnyHashable
public init(sources: [Source], initialId: AnyHashable) {
self.sources = sources
self.initialId = initialId
}
}
public struct Items {
public enum Content {
case list([ContextMenuItem])
@ -2390,8 +2235,20 @@ public final class ContextController: ViewController, StandalonePresentableContr
public var disablePositionLock: Bool
public var tip: Tip?
public var tipSignal: Signal<Tip?, NoError>?
public var dismissed: (() -> Void)?
public init(content: Content, context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], selectedReactionItems: Set<MessageReaction.Reaction> = Set(), animationCache: AnimationCache? = nil, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)? = nil, disablePositionLock: Bool = false, tip: Tip? = nil, tipSignal: Signal<Tip?, NoError>? = nil) {
public init(
content: Content,
context: AccountContext? = nil,
reactionItems: [ReactionContextItem] = [],
selectedReactionItems: Set<MessageReaction.Reaction> = Set(),
animationCache: AnimationCache? = nil,
getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)? = nil,
disablePositionLock: Bool = false,
tip: Tip? = nil,
tipSignal: Signal<Tip?, NoError>? = nil,
dismissed: (() -> Void)? = nil
) {
self.content = content
self.context = context
self.animationCache = animationCache
@ -2401,6 +2258,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.disablePositionLock = disablePositionLock
self.tip = tip
self.tipSignal = tipSignal
self.dismissed = dismissed
}
public init() {
@ -2412,6 +2270,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.disablePositionLock = false
self.tip = nil
self.tipSignal = nil
self.dismissed = nil
}
}
@ -2487,8 +2346,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
private var presentationData: PresentationData
private let source: ContextContentSource
private var items: Signal<ContextController.Items, NoError>
private let configuration: ContextController.Configuration
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
@ -2511,7 +2369,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
}
private var controllerNode: ContextControllerNode {
var controllerNode: ContextControllerNode {
return self.displayNode as! ContextControllerNode
}
@ -2540,17 +2398,41 @@ public final class ContextController: ViewController, StandalonePresentableContr
public var getOverlayViews: (() -> [UIView])?
public init(presentationData: PresentationData, source: ContextContentSource, items: Signal<ContextController.Items, NoError>, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false) {
convenience public init(presentationData: PresentationData, source: ContextContentSource, items: Signal<ContextController.Items, NoError>, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false) {
self.init(
presentationData: presentationData,
configuration: ContextController.Configuration(
sources: [ContextController.Source(
id: AnyHashable(0 as Int),
title: "",
source: source,
items: items
)],
initialId: AnyHashable(0 as Int)
),
recognizer: recognizer,
gesture: gesture,
workaroundUseLegacyImplementation: workaroundUseLegacyImplementation
)
}
public init(
presentationData: PresentationData,
configuration: ContextController.Configuration,
recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil,
gesture: ContextGesture? = nil,
workaroundUseLegacyImplementation: Bool = false
) {
self.presentationData = presentationData
self.source = source
self.items = items
self.configuration = configuration
self.recognizer = recognizer
self.gesture = gesture
self.workaroundUseLegacyImplementation = workaroundUseLegacyImplementation
super.init(navigationBarPresentationData: nil)
switch source {
if let mainSource = configuration.sources.first(where: { $0.id == configuration.initialId }) {
switch mainSource.source {
case let .location(locationSource):
self.statusBar.statusBarStyle = .Ignore
@ -2592,6 +2474,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
}).strict()
case .controller:
self.statusBar.statusBarStyle = .Hide
}
}
self.lockOrientation = true
@ -2607,7 +2490,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
override public func loadDisplayNode() {
self.displayNode = ContextControllerNode(controller: self, presentationData: self.presentationData, source: self.source, items: self.items, beginDismiss: { [weak self] result in
self.displayNode = ContextControllerNode(controller: self, presentationData: self.presentationData, configuration: self.configuration, beginDismiss: { [weak self] result in
self?.dismiss(result: result, completion: nil)
}, recognizer: self.recognizer, gesture: self.gesture, beganAnimatingOut: { [weak self] in
guard let strongSelf = self else {
@ -2622,19 +2505,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
return true
}
}, attemptTransitionControllerIntoNavigation: { [weak self] in
guard let strongSelf = self else {
return
}
switch strongSelf.source {
/*case let .controller(controller):
if let navigationController = controller.navigationController {
strongSelf.presentingViewController?.dismiss(animated: false, completion: nil)
navigationController.pushViewController(controller.controller, animated: false)
}*/
default:
break
}
}, attemptTransitionControllerIntoNavigation: {
})
self.controllerNode.dismissedForCancel = self.dismissedForCancel
self.displayNodeDidLoad()
@ -2685,17 +2556,23 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
public func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?) {
self.items = items
//self.items = items
if self.isNodeLoaded {
self.immediateItemsTransitionAnimation = false
self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: .scale)
} else {
assertionFailure()
}
}
public func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) {
self.items = items
//self.items = items
if self.isNodeLoaded {
self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition)
} else {
assertionFailure()
}
}
@ -2742,6 +2619,10 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.dismiss(result: .default, completion: completion)
}
public func dismissWithCustomTransition(transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) {
self.dismiss(result: .custom(transition), completion: nil)
}
public func dismissWithoutContent() {
self.dismiss(result: .dismissWithoutContent, completion: nil)
}

View File

@ -44,6 +44,7 @@ public protocol ContextControllerActionsStackItem: AnyObject {
var tip: ContextController.Tip? { get }
var tipSignal: Signal<ContextController.Tip?, NoError>? { get }
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? { get }
var dismissed: (() -> Void)? { get }
}
protocol ContextControllerActionsListItemNode: ASDisplayNode {
@ -836,17 +837,20 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let tip: ContextController.Tip?
let tipSignal: Signal<ContextController.Tip?, NoError>?
let dismissed: (() -> Void)?
init(
items: [ContextMenuItem],
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?
tipSignal: Signal<ContextController.Tip?, NoError>?,
dismissed: (() -> Void)?
) {
self.items = items
self.reactionItems = reactionItems
self.tip = tip
self.tipSignal = tipSignal
self.dismissed = dismissed
}
func node(
@ -928,17 +932,20 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let tip: ContextController.Tip?
let tipSignal: Signal<ContextController.Tip?, NoError>?
let dismissed: (() -> Void)?
init(
content: ContextControllerItemsContent,
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?
tipSignal: Signal<ContextController.Tip?, NoError>?,
dismissed: (() -> Void)?
) {
self.content = content
self.reactionItems = reactionItems
self.tip = tip
self.tipSignal = tipSignal
self.dismissed = dismissed
}
func node(
@ -963,11 +970,11 @@ func makeContextControllerActionsStackItem(items: ContextController.Items) -> [C
}
switch items.content {
case let .list(listItems):
return [ContextControllerActionsListStackItem(items: listItems, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal)]
return [ContextControllerActionsListStackItem(items: listItems, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)]
case let .twoLists(listItems1, listItems2):
return [ContextControllerActionsListStackItem(items: listItems1, reactionItems: nil, tip: nil, tipSignal: nil), ContextControllerActionsListStackItem(items: listItems2, reactionItems: nil, tip: nil, tipSignal: nil)]
return [ContextControllerActionsListStackItem(items: listItems1, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: items.dismissed), ContextControllerActionsListStackItem(items: listItems2, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: nil)]
case let .custom(customContent):
return [ContextControllerActionsCustomStackItem(content: customContent, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal)]
return [ContextControllerActionsCustomStackItem(content: customContent, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)]
}
}
@ -1083,6 +1090,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
let tipSignal: Signal<ContextController.Tip?, NoError>?
var tipNode: InnerTextSelectionTipContainerNode?
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let itemDismissed: (() -> Void)?
var storedScrollingState: CGFloat?
let positionLock: CGFloat?
@ -1097,6 +1105,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?,
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
itemDismissed: (() -> Void)?,
positionLock: CGFloat?
) {
self.getController = getController
@ -1113,6 +1122,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
self.dimNode.alpha = 0.0
self.reactionItems = reactionItems
self.itemDismissed = itemDismissed
self.positionLock = positionLock
self.tip = tip
@ -1339,6 +1349,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
tip: item.tip,
tipSignal: item.tipSignal,
reactionItems: item.reactionItems,
itemDismissed: item.dismissed,
positionLock: positionLock
)
self.itemContainers.append(itemContainer)
@ -1365,6 +1376,8 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
let itemContainer = self.itemContainers[self.itemContainers.count - 1]
self.itemContainers.remove(at: self.itemContainers.count - 1)
self.dismissingItemContainers.append((itemContainer, true))
itemContainer.itemDismissed?()
}
self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1

View File

@ -163,22 +163,50 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
private final class ControllerContentNode: ASDisplayNode {
let controller: ViewController
let passthroughTouches: Bool
var storedContentHeight: CGFloat?
init(controller: ViewController) {
init(controller: ViewController, passthroughTouches: Bool) {
self.controller = controller
self.passthroughTouches = passthroughTouches
super.init()
self.clipsToBounds = true
self.cornerRadius = 14.0
self.addSubnode(self.controller.displayNode)
}
func update(presentationData: PresentationData, size: CGSize, transition: ContainedViewLayoutTransition) {
func update(presentationData: PresentationData, parentLayout: ContainerViewLayout, size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.controller.displayNode, frame: CGRect(origin: CGPoint(), size: size))
self.controller.containerLayoutUpdated(
ContainerViewLayout(
size: size,
metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact),
deviceMetrics: parentLayout.deviceMetrics,
intrinsicInsets: UIEdgeInsets(),
safeInsets: UIEdgeInsets(),
additionalInsets: UIEdgeInsets(),
statusBarHeight: nil,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
),
transition: transition
)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if self.passthroughTouches {
let controllerPoint = self.view.convert(point, to: self.controller.view)
if let result = self.controller.view.hitTest(controllerPoint, with: event) {
return result
}
}
return self.view
}
}
@ -476,11 +504,21 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
if !items.disablePositionLock {
positionLock = self.getActionsStackPositionLock()
}
if self.actionsStackNode.topPositionLock == nil {
if let contentNode = self.controllerContentNode, contentNode.bounds.height != 0.0 {
contentNode.storedContentHeight = contentNode.bounds.height
}
}
self.actionsStackNode.push(item: makeContextControllerActionsStackItem(items: items).first!, currentScrollingState: currentScrollingState, positionLock: positionLock, animated: true)
}
func popItems() {
self.actionsStackNode.pop()
if self.actionsStackNode.topPositionLock == nil {
if let contentNode = self.controllerContentNode {
contentNode.storedContentHeight = nil
}
}
}
private func getCurrentScrollingState() -> CGFloat {
@ -517,7 +555,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
let contentActionsSpacing: CGFloat = 7.0
let actionsEdgeInset: CGFloat
let actionsSideInset: CGFloat = 6.0
let actionsSideInset: CGFloat
let topInset: CGFloat = layout.insets(options: .statusBar).top + 8.0
let bottomInset: CGFloat = 10.0
@ -540,7 +578,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
transition: .immediate
)
actionsEdgeInset = 16.0
case .extracted, .controller:
actionsSideInset = 6.0
case .extracted:
self.backgroundNode.updateColor(
color: presentationData.theme.contextMenu.dimColor,
enableBlur: true,
@ -548,6 +587,16 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
transition: .immediate
)
actionsEdgeInset = 12.0
actionsSideInset = 6.0
case .controller:
self.backgroundNode.updateColor(
color: presentationData.theme.contextMenu.dimColor,
enableBlur: true,
forceKeepBlur: true,
transition: .immediate
)
actionsEdgeInset = 12.0
actionsSideInset = -2.0
}
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true)
@ -583,7 +632,18 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
} else {
switch self.source {
case let .controller(source):
controllerContentNode = ControllerContentNode(controller: source.controller)
let controllerContentNodeValue = ControllerContentNode(controller: source.controller, passthroughTouches: source.passthroughTouches)
//source.controller.viewWillAppear(false)
//source.controller.setIgnoreAppearanceMethodInvocations(true)
self.scrollNode.insertSubnode(controllerContentNodeValue, aboveSubnode: self.actionsContainerNode)
self.controllerContentNode = controllerContentNodeValue
controllerContentNode = controllerContentNodeValue
contentTransition = .immediate
//source.controller.setIgnoreAppearanceMethodInvocations(false)
//source.controller.viewDidAppear(false)
case .location, .reference, .extracted:
controllerContentNode = nil
}
@ -688,6 +748,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
let contentParentGlobalFrame: CGRect
var contentRect: CGRect
var isContentResizeableVertically: Bool = false
let _ = isContentResizeableVertically
switch self.source {
case let .location(location):
@ -718,10 +780,21 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
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))
var defaultContentSize = CGSize(width: layout.size.width - 12.0 * 2.0, height: layout.size.height - 12.0 * 2.0 - contentTopInset - layout.safeInsets.bottom)
defaultContentSize.height = min(defaultContentSize.height, 460.0)
let contentSize: CGSize
if let preferredSize = contentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: defaultContentSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
contentSize = preferredSize
} else if let storedContentHeight = contentNode.storedContentHeight {
contentSize = CGSize(width: defaultContentSize.width, height: storedContentHeight)
} else {
contentSize = defaultContentSize
isContentResizeableVertically = true
}
contentRect = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) * 0.5), y: floor((layout.size.height - contentSize.height) * 0.5)), size: contentSize)
contentParentGlobalFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height))
} else {
return
@ -752,14 +825,6 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
transition: contentTransition
)
}
if let contentNode = controllerContentNode {
//TODO
contentNode.update(
presentationData: presentationData,
size: CGSize(),
transition: contentTransition
)
}
let actionsConstrainedHeight: CGFloat
if let actionsPositionLock = self.actionsStackNode.topPositionLock {
@ -795,6 +860,14 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
transition: transition
)
if isContentResizeableVertically && self.actionsStackNode.topPositionLock == nil {
var contentHeight = layout.size.height - contentTopInset - contentActionsSpacing - bottomInset - layout.intrinsicInsets.bottom - actionsSize.height
contentHeight = min(contentHeight, contentRect.height)
contentHeight = max(contentHeight, 200.0)
contentRect = CGRect(origin: CGPoint(x: 12.0, y: floor((layout.size.height - contentHeight) * 0.5)), size: CGSize(width: layout.size.width - 12.0 * 2.0, height: contentHeight))
}
var isAnimatingOut = false
if case .animateOut = stateTransition {
isAnimatingOut = true
@ -950,11 +1023,18 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
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))
var contentFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentRect.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)
contentNode.update(
presentationData: presentationData,
parentLayout: layout,
size: contentFrame.size,
transition: contentTransition
)
}
let contentHeight: CGFloat
@ -1046,6 +1126,34 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
damping: springDamping,
additive: true
)
} else if let contentNode = controllerContentNode {
if case let .controller(source) = self.source, let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() {
let sourcePoint = sourceView.convert(sourceRect.center, to: self.view)
animationInContentYDistance = contentRect.midY - sourcePoint.y
} else {
animationInContentYDistance = 0.0
}
currentContentScreenFrame = contentRect
contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
contentNode.layer.animateSpring(
from: -animationInContentYDistance as NSNumber, to: 0.0 as NSNumber,
keyPath: "position.y",
duration: duration,
delay: 0.0,
initialVelocity: 0.0,
damping: springDamping,
additive: true
)
contentNode.layer.animateSpring(
from: 0.01 as NSNumber, to: 1.0 as NSNumber,
keyPath: "transform.scale",
duration: duration,
delay: 0.0,
initialVelocity: 0.0,
damping: springDamping,
additive: false
)
} else {
animationInContentYDistance = 0.0
currentContentScreenFrame = contentRect
@ -1200,8 +1308,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
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)
/*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))
@ -1216,7 +1324,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
let currentContentLocalFrame = convertFrame(contentRect, from: self.scrollNode.view, to: self.view)
let animationInContentYDistance: CGFloat
var animationInContentYDistance: CGFloat
switch result {
case .default, .custom:
@ -1295,10 +1403,37 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
)
}
if let controllerContentNode {
let _ = controllerContentNode
//TODO
completion()
if let contentNode = controllerContentNode {
if case let .controller(source) = self.source, let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() {
let sourcePoint = sourceView.convert(sourceRect.center, to: self.view)
animationInContentYDistance = contentRect.midY - sourcePoint.y
} else {
animationInContentYDistance = 0.0
}
contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.8, removeOnCompletion: false, completion: { _ in
completion()
})
contentNode.layer.animate(
from: 0.0 as NSNumber,
to: -animationInContentYDistance as NSNumber,
keyPath: "position.y",
timingFunction: timingFunction,
duration: duration,
delay: 0.0,
removeOnCompletion: false,
additive: true
)
contentNode.layer.animate(
from: 1.0 as NSNumber,
to: 0.01 as NSNumber,
keyPath: "transform.scale",
timingFunction: timingFunction,
duration: duration,
delay: 0.0,
removeOnCompletion: false,
additive: false
)
}
self.actionsContainerNode.layer.animateAlpha(from: self.actionsContainerNode.alpha, to: 0.0, duration: duration, removeOnCompletion: false)

View File

@ -0,0 +1,621 @@
import Foundation
import AsyncDisplayKit
import Display
import TelegramPresentationData
import SwiftSignalKit
import TelegramCore
import ReactionSelectionNode
import ComponentFlow
import TabSelectorComponent
import ComponentDisplayAdapters
final class ContextSourceContainer: ASDisplayNode {
final class Source {
weak var controller: ContextController?
let id: AnyHashable
let title: String
let source: ContextContentSource
private var _presentationNode: ContextControllerPresentationNode?
var presentationNode: ContextControllerPresentationNode {
return self._presentationNode!
}
var currentPresentationStateTransition: ContextControllerPresentationNodeStateTransition?
var validLayout: ContainerViewLayout?
var presentationData: PresentationData?
var delayLayoutUpdate: Bool = false
var isAnimatingOut: Bool = false
let itemsDisposable = MetaDisposable()
let ready = Promise<Bool>()
private let contentReady = Promise<Bool>()
private let actionsReady = Promise<Bool>()
init(
controller: ContextController,
id: AnyHashable,
title: String,
source: ContextContentSource,
items: Signal<ContextController.Items, NoError>
) {
self.controller = controller
self.id = id
self.title = title
self.source = source
self.ready.set(combineLatest(queue: .mainQueue(), self.contentReady.get(), self.actionsReady.get())
|> map { a, b -> Bool in
return a && b
}
|> distinctUntilChanged)
switch source {
case let .location(source):
self.contentReady.set(.single(true))
let presentationNode = ContextControllerExtractedPresentationNode(
getController: { [weak self] in
guard let self else {
return nil
}
return self.controller
},
requestUpdate: { [weak self] transition in
guard let self else {
return
}
self.update(transition: transition)
},
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
guard let self else {
return
}
if let controller = self.controller {
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
}
},
requestDismiss: { [weak self] result in
guard let self, let controller = self.controller else {
return
}
controller.controllerNode.dismissedForCancel?()
controller.controllerNode.beginDismiss(result)
},
requestAnimateOut: { [weak self] result, completion in
guard let self, let controller = self.controller else {
return
}
controller.controllerNode.animateOut(result: result, completion: completion)
},
source: .location(source)
)
self._presentationNode = presentationNode
case let .reference(source):
self.contentReady.set(.single(true))
let presentationNode = ContextControllerExtractedPresentationNode(
getController: { [weak self] in
guard let self else {
return nil
}
return self.controller
},
requestUpdate: { [weak self] transition in
guard let self else {
return
}
self.update(transition: transition)
},
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
guard let self else {
return
}
if let controller = self.controller {
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
}
},
requestDismiss: { [weak self] result in
guard let self, let controller = self.controller else {
return
}
controller.controllerNode.dismissedForCancel?()
controller.controllerNode.beginDismiss(result)
},
requestAnimateOut: { [weak self] result, completion in
guard let self, let controller = self.controller else {
return
}
controller.controllerNode.animateOut(result: result, completion: completion)
},
source: .reference(source)
)
self._presentationNode = presentationNode
case let .extracted(source):
self.contentReady.set(.single(true))
let presentationNode = ContextControllerExtractedPresentationNode(
getController: { [weak self] in
guard let self else {
return nil
}
return self.controller
},
requestUpdate: { [weak self] transition in
guard let self else {
return
}
self.update(transition: transition)
},
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
guard let self else {
return
}
if let controller = self.controller {
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
}
},
requestDismiss: { [weak self] result in
guard let self, let controller = self.controller else {
return
}
controller.controllerNode.dismissedForCancel?()
controller.controllerNode.beginDismiss(result)
},
requestAnimateOut: { [weak self] result, completion in
guard let self, let controller = self.controller else {
return
}
controller.controllerNode.animateOut(result: result, completion: completion)
},
source: .extracted(source)
)
self._presentationNode = presentationNode
case let .controller(source):
self.contentReady.set(source.controller.ready.get())
let presentationNode = ContextControllerExtractedPresentationNode(
getController: { [weak self] in
guard let self else {
return nil
}
return self.controller
},
requestUpdate: { [weak self] transition in
guard let self else {
return
}
self.update(transition: transition)
},
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
guard let self else {
return
}
if let controller = self.controller {
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
}
},
requestDismiss: { [weak self] result in
guard let self, let controller = self.controller else {
return
}
controller.controllerNode.dismissedForCancel?()
controller.controllerNode.beginDismiss(result)
},
requestAnimateOut: { [weak self] result, completion in
guard let self, let controller = self.controller else {
return
}
controller.controllerNode.animateOut(result: result, completion: completion)
},
source: .controller(source)
)
self._presentationNode = presentationNode
}
self.itemsDisposable.set((items |> deliverOnMainQueue).start(next: { [weak self] items in
guard let self else {
return
}
self.setItems(items: items, animated: false)
self.actionsReady.set(.single(true))
}))
}
deinit {
self.itemsDisposable.dispose()
}
func animateIn() {
self.currentPresentationStateTransition = .animateIn
self.update(transition: .animated(duration: 0.5, curve: .spring))
}
func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) {
self.currentPresentationStateTransition = .animateOut(result: result, completion: completion)
if let _ = self.validLayout {
if case let .custom(transition) = result {
self.delayLayoutUpdate = true
Queue.mainQueue().after(0.1) {
self.delayLayoutUpdate = false
self.update(transition: transition)
self.isAnimatingOut = true
}
} else {
self.update(transition: .animated(duration: 0.35, curve: .easeInOut))
}
}
}
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
self.presentationNode.addRelativeContentOffset(offset, transition: transition)
}
func cancelReactionAnimation() {
self.presentationNode.cancelReactionAnimation()
}
func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) {
self.presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: completion)
}
func setItems(items: Signal<ContextController.Items, NoError>, animated: Bool) {
self.itemsDisposable.set((items
|> deliverOnMainQueue).start(next: { [weak self] items in
guard let self else {
return
}
self.setItems(items: items, animated: animated)
}))
}
func setItems(items: ContextController.Items, animated: Bool) {
self.presentationNode.replaceItems(items: items, animated: animated)
}
func pushItems(items: Signal<ContextController.Items, NoError>) {
self.itemsDisposable.set((items
|> deliverOnMainQueue).start(next: { [weak self] items in
guard let self else {
return
}
self.presentationNode.pushItems(items: items)
}))
}
func popItems() {
self.presentationNode.popItems()
}
func update(transition: ContainedViewLayoutTransition) {
guard let validLayout = self.validLayout else {
return
}
guard let presentationData = self.presentationData else {
return
}
self.update(presentationData: presentationData, layout: validLayout, transition: transition)
}
func update(
presentationData: PresentationData,
layout: ContainerViewLayout,
transition: ContainedViewLayoutTransition
) {
if self.isAnimatingOut || self.delayLayoutUpdate {
return
}
self.validLayout = layout
self.presentationData = presentationData
let presentationStateTransition = self.currentPresentationStateTransition
self.currentPresentationStateTransition = .none
self.presentationNode.update(
presentationData: presentationData,
layout: layout,
transition: transition,
stateTransition: presentationStateTransition
)
}
}
private struct PanState {
var fraction: CGFloat
init(fraction: CGFloat) {
self.fraction = fraction
}
}
private weak var controller: ContextController?
var sources: [Source] = []
var activeIndex: Int = 0
private var tabSelector: ComponentView<Empty>?
private var presentationData: PresentationData?
private var validLayout: ContainerViewLayout?
private var panState: PanState?
let ready = Promise<Bool>()
var activeSource: Source? {
if self.activeIndex >= self.sources.count {
return nil
}
return self.sources[self.activeIndex]
}
var overlayWantsToBeBelowKeyboard: Bool {
return self.activeSource?.presentationNode.wantsDisplayBelowKeyboard() ?? false
}
init(controller: ContextController, configuration: ContextController.Configuration) {
self.controller = controller
super.init()
for i in 0 ..< configuration.sources.count {
let source = configuration.sources[i]
let mappedSource = Source(
controller: controller,
id: source.id,
title: source.title,
source: source.source,
items: source.items
)
self.sources.append(mappedSource)
self.addSubnode(mappedSource.presentationNode)
if source.id == configuration.initialId {
self.activeIndex = i
}
}
self.ready.set(self.sources[self.activeIndex].ready.get())
self.view.addGestureRecognizer(InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let self else {
return []
}
if self.sources.count <= 1 {
return []
}
return [.left, .right]
}))
}
@objc private func panGesture(_ recognizer: InteractiveTransitionGestureRecognizer) {
switch recognizer.state {
case .began, .changed:
if let validLayout = self.validLayout {
var translationX = recognizer.translation(in: self.view).x
if self.activeIndex == 0 && translationX > 0.0 {
translationX = scrollingRubberBandingOffset(offset: abs(translationX), bandingStart: 0.0, range: 20.0)
} else if self.activeIndex == self.sources.count - 1 && translationX < 0.0 {
translationX = -scrollingRubberBandingOffset(offset: abs(translationX), bandingStart: 0.0, range: 20.0)
}
self.panState = PanState(fraction: translationX / validLayout.size.width)
self.update(transition: .immediate)
}
case .cancelled, .ended:
if let panState = self.panState {
self.panState = nil
let velocity = recognizer.velocity(in: self.view)
var nextIndex = self.activeIndex
if panState.fraction < -0.4 {
nextIndex += 1
} else if panState.fraction > 0.4 {
nextIndex -= 1
} else if abs(velocity.x) >= 200.0 {
if velocity.x < 0.0 {
nextIndex += 1
} else {
nextIndex -= 1
}
}
if nextIndex < 0 {
nextIndex = 0
}
if nextIndex > self.sources.count - 1 {
nextIndex = self.sources.count - 1
}
if nextIndex != self.activeIndex {
self.activeIndex = nextIndex
}
self.update(transition: .animated(duration: 0.4, curve: .spring))
}
default:
break
}
}
func animateIn() {
if let activeSource = self.activeSource {
activeSource.animateIn()
}
if let tabSelectorView = self.tabSelector?.view {
tabSelectorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) {
if let tabSelectorView = self.tabSelector?.view {
tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
if let activeSource = self.activeSource {
activeSource.animateOut(result: result, completion: completion)
} else {
completion()
}
}
func highlightGestureMoved(location: CGPoint, hover: Bool) {
if self.activeIndex >= self.sources.count {
return
}
self.sources[self.activeIndex].presentationNode.highlightGestureMoved(location: location, hover: hover)
}
func highlightGestureFinished(performAction: Bool) {
if self.activeIndex >= self.sources.count {
return
}
self.sources[self.activeIndex].presentationNode.highlightGestureFinished(performAction: performAction)
}
func performHighlightedAction() {
self.activeSource?.presentationNode.highlightGestureFinished(performAction: true)
}
func decreaseHighlightedIndex() {
self.activeSource?.presentationNode.decreaseHighlightedIndex()
}
func increaseHighlightedIndex() {
self.activeSource?.presentationNode.increaseHighlightedIndex()
}
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
if let activeSource = self.activeSource {
activeSource.addRelativeContentOffset(offset, transition: transition)
}
}
func cancelReactionAnimation() {
if let activeSource = self.activeSource {
activeSource.cancelReactionAnimation()
}
}
func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) {
if let activeSource = self.activeSource {
activeSource.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: completion)
} else {
completion()
}
}
func setItems(items: Signal<ContextController.Items, NoError>, animated: Bool) {
if let activeSource = self.activeSource {
activeSource.setItems(items: items, animated: animated)
}
}
func pushItems(items: Signal<ContextController.Items, NoError>) {
if let activeSource = self.activeSource {
activeSource.pushItems(items: items)
}
}
func popItems() {
if let activeSource = self.activeSource {
activeSource.popItems()
}
}
private func update(transition: ContainedViewLayoutTransition) {
if let presentationData = self.presentationData, let validLayout = self.validLayout {
self.update(presentationData: presentationData, layout: validLayout, transition: transition)
}
}
func update(
presentationData: PresentationData,
layout: ContainerViewLayout,
transition: ContainedViewLayoutTransition
) {
self.presentationData = presentationData
self.validLayout = layout
var childLayout = layout
if self.sources.count > 1 {
let tabSelector: ComponentView<Empty>
if let current = self.tabSelector {
tabSelector = current
} else {
tabSelector = ComponentView()
self.tabSelector = tabSelector
}
let mappedItems = self.sources.map { source -> TabSelectorComponent.Item in
return TabSelectorComponent.Item(id: source.id, title: source.title)
}
let tabSelectorSize = tabSelector.update(
transition: Transition(transition),
component: AnyComponent(TabSelectorComponent(
colors: TabSelectorComponent.Colors(
foreground: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.8),
selection: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.1)
),
items: mappedItems,
selectedId: self.activeSource?.id,
setSelectedId: { [weak self] id in
guard let self else {
return
}
if let index = self.sources.firstIndex(where: { $0.id == id }) {
self.activeIndex = index
self.update(transition: .animated(duration: 0.4, curve: .spring))
}
}
)),
environment: {},
containerSize: CGSize(width: layout.size.width, height: 44.0)
)
childLayout.intrinsicInsets.bottom += 44.0
if let tabSelectorView = tabSelector.view {
if tabSelectorView.superview == nil {
self.view.addSubview(tabSelectorView)
}
transition.updateFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - tabSelectorSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - tabSelectorSize.height), size: tabSelectorSize))
}
} else if let tabSelector = self.tabSelector {
self.tabSelector = nil
tabSelector.view?.removeFromSuperview()
}
for i in 0 ..< self.sources.count {
var itemFrame = CGRect(origin: CGPoint(), size: childLayout.size)
itemFrame.origin.x += CGFloat(i - self.activeIndex) * childLayout.size.width
if let panState = self.panState {
itemFrame.origin.x += panState.fraction * childLayout.size.width
}
let itemTransition = transition
itemTransition.updateFrame(node: self.sources[i].presentationNode, frame: itemFrame)
self.sources[i].update(
presentationData: presentationData,
layout: childLayout,
transition: itemTransition
)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let tabSelectorView = self.tabSelector?.view {
if let result = tabSelectorView.hitTest(self.view.convert(point, to: tabSelectorView), with: event) {
return result
}
}
guard let activeSource = self.activeSource else {
return nil
}
return activeSource.presentationNode.view.hitTest(point, with: event)
}
}

View File

@ -28,6 +28,7 @@ public final class ContextMenuController: ViewController, KeyShortcutResponder,
private let catchTapsOutside: Bool
private let hasHapticFeedback: Bool
private let blurred: Bool
private let skipCoordnateConversion: Bool
private var layout: ContainerViewLayout?
@ -36,11 +37,12 @@ public final class ContextMenuController: ViewController, KeyShortcutResponder,
public var dismissOnTap: ((UIView, CGPoint) -> Bool)?
public init(actions: [ContextMenuAction], catchTapsOutside: Bool = false, hasHapticFeedback: Bool = false, blurred: Bool = false) {
public init(actions: [ContextMenuAction], catchTapsOutside: Bool = false, hasHapticFeedback: Bool = false, blurred: Bool = false, skipCoordnateConversion: Bool = false) {
self.actions = actions
self.catchTapsOutside = catchTapsOutside
self.hasHapticFeedback = hasHapticFeedback
self.blurred = blurred
self.skipCoordnateConversion = skipCoordnateConversion
super.init(navigationBarPresentationData: nil)
@ -92,8 +94,13 @@ public final class ContextMenuController: ViewController, KeyShortcutResponder,
self.layout = layout
if let presentationArguments = self.presentationArguments as? ContextMenuControllerPresentationArguments, let (sourceNode, sourceRect, containerNode, containerRect) = presentationArguments.sourceNodeAndRect() {
self.contextMenuNode.sourceRect = sourceNode.view.convert(sourceRect, to: nil)
self.contextMenuNode.containerRect = containerNode.view.convert(containerRect, to: nil)
if self.skipCoordnateConversion {
self.contextMenuNode.sourceRect = sourceRect
self.contextMenuNode.containerRect = containerRect
} else {
self.contextMenuNode.sourceRect = sourceNode.view.convert(sourceRect, to: nil)
self.contextMenuNode.containerRect = containerNode.view.convert(containerRect, to: nil)
}
} else {
self.contextMenuNode.sourceRect = nil
self.contextMenuNode.containerRect = nil

View File

@ -146,7 +146,7 @@ final class ContextMenuNode: ASDisplayNode {
private let feedback: HapticFeedback?
init(actions: [ContextMenuAction], dismiss: @escaping () -> Void, dismissOnTap: @escaping (UIView, CGPoint) -> Bool, catchTapsOutside: Bool, hasHapticFeedback: Bool = false, blurred: Bool = false) {
init(actions: [ContextMenuAction], dismiss: @escaping () -> Void, dismissOnTap: @escaping (UIView, CGPoint) -> Bool, catchTapsOutside: Bool, hasHapticFeedback: Bool, blurred: Bool = false) {
self.actions = actions
self.dismiss = dismiss
self.dismissOnTap = dismissOnTap

View File

@ -140,7 +140,7 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
override public func translation(in view: UIView?) -> CGPoint {
let result = super.translation(in: view)
return result.offsetBy(dx: self.ignoreOffset.x, dy: self.ignoreOffset.y)
return result//.offsetBy(dx: self.ignoreOffset.x, dy: self.ignoreOffset.y)
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {

View File

@ -306,7 +306,7 @@ public final class PresentationContext {
UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: nil)
}
func hitTest(view: UIView, point: CGPoint, with event: UIEvent?) -> UIView? {
public func hitTest(view: UIView, point: CGPoint, with event: UIEvent?) -> UIView? {
for (controller, _) in self.controllers.reversed() {
if controller.isViewLoaded {
if let result = controller.view.hitTest(view.convert(point, to: controller.view), with: event) {

View File

@ -1199,9 +1199,12 @@ open class TextNode: ASDisplayNode {
let rawSubstring = segment.substring.string as NSString
let substringLength = rawSubstring.length
let typesetter = CTTypesetterCreateWithAttributedString(segment.substring as CFAttributedString)
var currentLineStartIndex = 0
let segmentTypesetterString = attributedString.attributedSubstring(from: NSRange(location: 0, length: segment.firstCharacterOffset + substringLength))
let typesetter = CTTypesetterCreateWithAttributedString(segmentTypesetterString as CFAttributedString)
var currentLineStartIndex = segment.firstCharacterOffset
let segmentEndIndex = segment.firstCharacterOffset + substringLength
var constrainedSegmentWidth = constrainedSize.width
var additionalOffsetX: CGFloat = 0.0
@ -1250,7 +1253,7 @@ open class TextNode: ASDisplayNode {
frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)),
ascent: lineAscent,
descent: lineDescent,
range: NSRange(location: segment.firstCharacterOffset + currentLineStartIndex, length: lineCharacterCount),
range: NSRange(location: currentLineStartIndex, length: lineCharacterCount),
isRTL: false,
strikethroughs: [],
spoilers: [],
@ -1265,7 +1268,7 @@ open class TextNode: ASDisplayNode {
currentLineStartIndex += lineCharacterCount
if currentLineStartIndex >= substringLength {
if currentLineStartIndex >= segmentEndIndex {
break
}
}

View File

@ -632,7 +632,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size)
let item: InstantPageItem
if let url = url, let coverId = coverId, case let .image(image) = media[coverId] {
let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, story: nil, attributes: [], instantPage: nil)
let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, story: nil, attributes: [], instantPage: nil, displayOptions: .default)
let content = TelegramMediaWebpageContent.Loaded(loadedContent)
item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: .webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content)), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false)

View File

@ -158,6 +158,10 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
}
}
deinit {
self.pendingFetch?.disposable.dispose()
}
func request(
range: Range<Int64>,
isFullRange: Bool,

View File

@ -1,11 +1,17 @@
import Foundation
private var postboxLogger: (String) -> Void = { _ in }
private var postboxLoggerSync: () -> Void = {}
public func setPostboxLogger(_ f: @escaping (String) -> Void) {
public func setPostboxLogger(_ f: @escaping (String) -> Void, sync: @escaping () -> Void) {
postboxLogger = f
postboxLoggerSync = sync
}
public func postboxLog(_ what: @autoclosure () -> String) {
postboxLogger(what())
}
public func postboxLogSync() {
postboxLoggerSync()
}

View File

@ -60,6 +60,7 @@ struct SqlitePreparedStatement {
if let path = pathToRemoveOnError {
postboxLog("Corrupted DB at step, dropping")
try? FileManager.default.removeItem(atPath: path)
postboxLogSync()
preconditionFailure()
}
}
@ -84,6 +85,7 @@ struct SqlitePreparedStatement {
if let path = pathToRemoveOnError {
postboxLog("Corrupted DB at step, dropping")
try? FileManager.default.removeItem(atPath: path)
postboxLogSync()
preconditionFailure()
}
}
@ -300,12 +302,14 @@ public final class SqliteValueBox: ValueBox {
} catch {
let _ = try? FileManager.default.removeItem(atPath: tempPath)
postboxLog("Don't have write access to database folder")
postboxLogSync()
preconditionFailure("Don't have write access to database folder")
}
if self.removeDatabaseOnError {
let _ = try? FileManager.default.removeItem(atPath: path)
}
postboxLogSync()
preconditionFailure("Couldn't open database")
}
@ -577,6 +581,7 @@ public final class SqliteValueBox: ValueBox {
try? FileManager.default.removeItem(atPath: databasePath)
}
postboxLogSync()
preconditionFailure()
}
})
@ -1197,6 +1202,7 @@ public final class SqliteValueBox: ValueBox {
let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
if status != SQLITE_OK {
let errorText = self.database.currentError() ?? "Unknown error"
postboxLogSync()
preconditionFailure(errorText)
}
let preparedStatement = SqlitePreparedStatement(statement: statement)
@ -1211,6 +1217,7 @@ public final class SqliteValueBox: ValueBox {
let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?)", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
if status != SQLITE_OK {
let errorText = self.database.currentError() ?? "Unknown error"
postboxLogSync()
preconditionFailure(errorText)
}
let preparedStatement = SqlitePreparedStatement(statement: statement)
@ -1250,6 +1257,7 @@ public final class SqliteValueBox: ValueBox {
let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?) ON CONFLICT(key) DO NOTHING", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
if status != SQLITE_OK {
let errorText = self.database.currentError() ?? "Unknown error"
postboxLogSync()
preconditionFailure(errorText)
}
let preparedStatement = SqlitePreparedStatement(statement: statement)
@ -1264,6 +1272,7 @@ public final class SqliteValueBox: ValueBox {
let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?)", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil)
if status != SQLITE_OK {
let errorText = self.database.currentError() ?? "Unknown error"
postboxLogSync()
preconditionFailure(errorText)
}
let preparedStatement = SqlitePreparedStatement(statement: statement)
@ -2297,6 +2306,7 @@ public final class SqliteValueBox: ValueBox {
self.clearStatements()
if self.isReadOnly {
postboxLogSync()
preconditionFailure()
}
@ -2346,6 +2356,7 @@ public final class SqliteValueBox: ValueBox {
private func reencryptInPlace(database: Database, encryptionParameters: ValueBoxEncryptionParameters) -> Database {
if self.isReadOnly {
postboxLogSync()
preconditionFailure()
}

View File

@ -71,6 +71,7 @@ final class AccountManagerImpl<Types: AccountManagerTypes> {
return (atomicState.records.sorted(by: { $0.key.int64 < $1.key.int64 }).map({ $1 }), atomicState.currentRecordId)
} catch let e {
postboxLog("decode atomic state error: \(e)")
postboxLogSync()
preconditionFailure()
}
}
@ -85,10 +86,16 @@ final class AccountManagerImpl<Types: AccountManagerTypes> {
self.temporarySessionId = temporarySessionId
let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil)
guard let guardValueBox = SqliteValueBox(basePath: basePath + "/guard_db", queue: queue, isTemporary: isTemporary, isReadOnly: false, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: nil, upgradeProgress: { _ in }) else {
postboxLog("Could not open guard value box at \(basePath + "/guard_db")")
postboxLogSync()
preconditionFailure()
return nil
}
self.guardValueBox = guardValueBox
guard let valueBox = SqliteValueBox(basePath: basePath + "/db", queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: nil, upgradeProgress: { _ in }) else {
postboxLog("Could not open value box at \(basePath + "/db")")
postboxLogSync()
preconditionFailure()
return nil
}
self.valueBox = valueBox
@ -106,6 +113,7 @@ final class AccountManagerImpl<Types: AccountManagerTypes> {
} catch let e {
postboxLog("decode atomic state error: \(e)")
let _ = try? FileManager.default.removeItem(atPath: self.atomicStatePath)
postboxLogSync()
preconditionFailure()
}
} catch let e {
@ -246,9 +254,11 @@ final class AccountManagerImpl<Types: AccountManagerTypes> {
if let data = try? JSONEncoder().encode(self.currentAtomicState) {
if let _ = try? data.write(to: URL(fileURLWithPath: self.atomicStatePath), options: [.atomic]) {
} else {
postboxLogSync()
preconditionFailure()
}
} else {
postboxLogSync()
preconditionFailure()
}
}
@ -523,6 +533,7 @@ public final class AccountManager<Types: AccountManagerTypes> {
if let value = AccountManagerImpl<Types>(queue: queue, basePath: basePath, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, temporarySessionId: temporarySessionId) {
return value
} else {
postboxLogSync()
preconditionFailure()
}
})

View File

@ -55,7 +55,7 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage, url: String?) ->
if let cachedPage = cachedPage {
instantPage = InstantPage(apiPage: cachedPage)
}
return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, image: image, file: file, story: story, attributes: webpageAttributes, instantPage: instantPage)))
return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, image: image, file: file, story: story, attributes: webpageAttributes, instantPage: instantPage, displayOptions: .default)))
case .webPageEmpty:
return nil
}

View File

@ -69,6 +69,34 @@ public final class TelegraMediaWebpageThemeAttribute: PostboxCoding, Equatable {
}
}
public struct TelegramMediaWebpageDisplayOptions: Codable, Equatable {
public enum CodingKeys: String, CodingKey {
case position = "p"
case largeMedia = "lm"
}
public enum Position: Int32, Codable {
case aboveText = 0
case belowText = 1
}
public var position: Position?
public var largeMedia: Bool?
public static let `default` = TelegramMediaWebpageDisplayOptions(
position: nil,
largeMedia: nil
)
public init(
position: Position?,
largeMedia: Bool?
) {
self.position = position
self.largeMedia = largeMedia
}
}
public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
public let url: String
public let displayUrl: String
@ -89,7 +117,28 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
public let attributes: [TelegramMediaWebpageAttribute]
public let instantPage: InstantPage?
public init(url: String, displayUrl: String, hash: Int32, type: String?, websiteName: String?, title: String?, text: String?, embedUrl: String?, embedType: String?, embedSize: PixelDimensions?, duration: Int?, author: String?, image: TelegramMediaImage?, file: TelegramMediaFile?, story: TelegramMediaStory?, attributes: [TelegramMediaWebpageAttribute], instantPage: InstantPage?) {
public let displayOptions: TelegramMediaWebpageDisplayOptions
public init(
url: String,
displayUrl: String,
hash: Int32,
type: String?,
websiteName: String?,
title: String?,
text: String?,
embedUrl: String?,
embedType: String?,
embedSize: PixelDimensions?,
duration: Int?,
author: String?,
image: TelegramMediaImage?,
file: TelegramMediaFile?,
story: TelegramMediaStory?,
attributes: [TelegramMediaWebpageAttribute],
instantPage: InstantPage?,
displayOptions: TelegramMediaWebpageDisplayOptions
) {
self.url = url
self.displayUrl = displayUrl
self.hash = hash
@ -107,6 +156,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
self.story = story
self.attributes = attributes
self.instantPage = instantPage
self.displayOptions = displayOptions
}
public init(decoder: PostboxDecoder) {
@ -163,6 +213,8 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
} else {
self.instantPage = nil
}
self.displayOptions = decoder.decodeCodable(TelegramMediaWebpageDisplayOptions.self, forKey: "do") ?? TelegramMediaWebpageDisplayOptions.default
}
public func encode(_ encoder: PostboxEncoder) {
@ -239,6 +291,31 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "ip")
}
encoder.encodeCodable(self.displayOptions, forKey: "do")
}
public func withDisplayOptions(_ displayOptions: TelegramMediaWebpageDisplayOptions) -> TelegramMediaWebpageLoadedContent {
return TelegramMediaWebpageLoadedContent(
url: self.url,
displayUrl: self.displayUrl,
hash: self.hash,
type: self.type,
websiteName: self.websiteName,
title: self.title,
text: self.text,
embedUrl: self.embedUrl,
embedType: self.embedType,
embedSize: self.embedSize,
duration: self.duration,
author: self.author,
image: self.image,
file: self.file,
story: self.story,
attributes: self.attributes,
instantPage: self.instantPage,
displayOptions: displayOptions
)
}
}
@ -296,6 +373,10 @@ public func ==(lhs: TelegramMediaWebpageLoadedContent, rhs: TelegramMediaWebpage
return false
}
if lhs.displayOptions != rhs.displayOptions {
return false
}
return true
}

View File

@ -108,6 +108,8 @@ public final class Logger {
setPostboxLogger({ s in
Logger.shared.log("Postbox", s)
Logger.shared.shortLog("Postbox", s)
}, sync: {
Logger.shared.sync()
})
}
@ -128,6 +130,14 @@ public final class Logger {
self.basePath = basePath
}
public func sync() {
self.queue.sync {
if let (currentFile, _) = self.file {
let _ = currentFile.sync()
}
}
}
public func collectLogs(prefix: String? = nil) -> Signal<[(String, String)], NoError> {
return Signal { subscriber in
self.queue.async {

View File

@ -92,7 +92,7 @@ public func actualizedWebpage(account: Account, webpage: TelegramMediaWebpage) -
if let updatedWebpage = telegramMediaWebpageFromApiWebpage(apiWebpage, url: nil), case .Loaded = updatedWebpage.content, updatedWebpage.webpageId == webpage.webpageId {
return .single(updatedWebpage)
} else if case let .webPageNotModified(_, viewsValue) = apiWebpage, let views = viewsValue, case let .Loaded(content) = webpage.content {
let updatedContent: TelegramMediaWebpageContent = .Loaded(TelegramMediaWebpageLoadedContent(url: content.url, displayUrl: content.displayUrl, hash: content.hash, type: content.type, websiteName: content.websiteName, title: content.title, text: content.text, embedUrl: content.embedUrl, embedType: content.embedType, embedSize: content.embedSize, duration: content.duration, author: content.author, image: content.image, file: content.file, story: content.story, attributes: content.attributes, instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) })))
let updatedContent: TelegramMediaWebpageContent = .Loaded(TelegramMediaWebpageLoadedContent(url: content.url, displayUrl: content.displayUrl, hash: content.hash, type: content.type, websiteName: content.websiteName, title: content.title, text: content.text, embedUrl: content.embedUrl, embedType: content.embedType, embedSize: content.embedSize, duration: content.duration, author: content.author, image: content.image, file: content.file, story: content.story, attributes: content.attributes, instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) }), displayOptions: .default))
let updatedWebpage = TelegramMediaWebpage(webpageId: webpage.webpageId, content: updatedContent)
updateMessageMedia(transaction: transaction, id: webpage.webpageId, media: updatedWebpage)
return .single(updatedWebpage)

View File

@ -87,7 +87,7 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess
}
}
if let subject = associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .forward = info.kind {
if let subject = associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
authorTitle = nil
}

View File

@ -67,6 +67,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private var cachedChatMessageText: CachedChatMessageText?
private var textSelectionState: Promise<ChatControllerSubject.MessageOptionsInfo.SelectionState>?
override public var visibility: ListViewItemNodeVisibility {
didSet {
if oldValue != self.visibility {
@ -140,7 +142,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let message = item.message
let incoming: Bool
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .forward = info.kind {
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
incoming = false
} else {
incoming = item.message.effectivelyIncoming(item.context.account.peerId)
@ -564,16 +566,28 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.statusNode.pressed = nil
}
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case let .reply(initialQuote) = info.kind {
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case let .reply(info) = info {
if strongSelf.textSelectionNode == nil {
strongSelf.updateIsExtractedToContextPreview(true)
if let initialQuote, item.message.id == initialQuote.messageId, let string = strongSelf.textNode.textNode.cachedLayout?.attributedString {
if let initialQuote = info.quote, 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)
}
}
if strongSelf.textSelectionState == nil {
if let textSelectionNode = strongSelf.textSelectionNode {
let range = textSelectionNode.getSelection()
strongSelf.textSelectionState = Promise(strongSelf.getSelectionState(range: range))
} else {
strongSelf.textSelectionState = Promise(strongSelf.getSelectionState(range: nil))
}
}
if let textSelectionState = strongSelf.textSelectionState {
info.selectionState.set(textSelectionState.get())
}
}
}
}
@ -799,11 +813,11 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return
}
/*if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .reply = info.kind {
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .reply = info {
item.controllerInteraction.presentControllerInCurrent(c, a)
} else {*/
} else {
item.controllerInteraction.presentGlobalOverlayController(c, a)
//}
}
}, rootNode: { [weak rootNode] in
return rootNode
}, performAction: { [weak self] text, action in
@ -813,7 +827,10 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
item.controllerInteraction.performTextSelectionAction(item.message, true, text, action)
})
textSelectionNode.updateRange = { [weak self] selectionRange in
if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange {
guard let strongSelf = self else {
return
}
if let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange {
for (spoilerRange, _) in textLayout.spoilers {
if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 {
dustNode.update(revealed: true)
@ -821,23 +838,25 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
}
if let textSelectionState = strongSelf.textSelectionState {
textSelectionState.set(.single(strongSelf.getSelectionState(range: selectionRange)))
}
}
let enableCopy = !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected()
textSelectionNode.enableCopy = enableCopy
var enableQuote = false
let enableQuote = true
var enableOtherActions = true
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .reply = info.kind {
enableQuote = true
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .reply = info {
enableOtherActions = false
} else if item.controllerInteraction.canSetupReply(item.message) == .reply {
enableQuote = true
enableOtherActions = false
}
textSelectionNode.enableQuote = enableQuote
textSelectionNode.enableTranslate = enableOtherActions
textSelectionNode.enableShare = enableOtherActions
textSelectionNode.menuSkipCoordnateConversion = !enableOtherActions
self.textSelectionNode = textSelectionNode
self.addSubnode(textSelectionNode)
self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode.textNode)
@ -904,11 +923,26 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
textSelectionNode.setSelection(range: range, displayMenu: displayMenu)
}
public func getCurrentTextSelection() -> (text: String, entities: [MessageTextEntity])? {
public func cancelTextSelection() {
guard let textSelectionNode = self.textSelectionNode else {
return
}
textSelectionNode.cancelSelection()
}
private func getSelectionState(range: NSRange?) -> ChatControllerSubject.MessageOptionsInfo.SelectionState {
var quote: ChatControllerSubject.MessageOptionsInfo.Quote?
if let item = self.item, let range, let selection = self.getCurrentTextSelection(customRange: range) {
quote = ChatControllerSubject.MessageOptionsInfo.Quote(messageId: item.message.id, text: selection.text)
}
return ChatControllerSubject.MessageOptionsInfo.SelectionState(quote: quote)
}
public func getCurrentTextSelection(customRange: NSRange? = nil) -> (text: String, entities: [MessageTextEntity])? {
guard let textSelectionNode = self.textSelectionNode else {
return nil
}
guard let range = textSelectionNode.getSelection() else {
guard let range = customRange ?? textSelectionNode.getSelection() else {
return nil
}
guard let item = self.item else {

View File

@ -362,14 +362,14 @@ 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, replyOptions: nil)
return ChatControllerSubject.ForwardOptions(hideNames: state.interfaceState.forwardOptionsState?.hideNames ?? false, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false)
}
|> distinctUntilChanged
let chatController = strongSelf.context.sharedContext.makeChatController(
context: strongSelf.context,
chatLocation: .peer(id: strongSelf.context.account.peerId),
subject: .messageOptions(peerIds: peerIds, ids: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: ChatControllerSubject.MessageOptionsInfo(kind: .forward), options: forwardOptions),
subject: .messageOptions(peerIds: peerIds, ids: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .forward(ChatControllerSubject.MessageOptionsInfo.Forward(options: forwardOptions))),
botStart: nil,
mode: .standard(previewing: true)
)
@ -544,6 +544,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
contextController.immediateItemsTransitionAnimation = true
strongSelf.controller?.presentInGlobalOverlay(contextController)
}, presentReplyOptions: { _ in
}, presentLinkOptions: { _ in
}, shareSelectedMessages: {
}, updateTextInputStateAndMode: { [weak self] f in
if let strongSelf = self {

View File

@ -12,11 +12,68 @@ import ContextUI
import ChatInterfaceState
import PresentationDataUtils
import ChatMessageTextBubbleContentNode
import TextFormat
func presentChatForwardOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) {
guard let peerId = selfController.chatLocation.peerId else {
private enum OptionsId: Hashable {
case reply
case forward
case link
}
private func presentChatInputOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, initialId: OptionsId) {
var getContextController: (() -> ContextController?)?
var sources: [ContextController.Source] = []
let replySelectionState = Promise<ChatControllerSubject.MessageOptionsInfo.SelectionState>(ChatControllerSubject.MessageOptionsInfo.SelectionState(quote: nil))
if let source = chatForwardOptions(selfController: selfController, sourceNode: sourceNode, getContextController: {
return getContextController?()
}) {
sources.append(source)
}
if let source = chatReplyOptions(selfController: selfController, sourceNode: sourceNode, getContextController: {
return getContextController?()
}, selectionState: replySelectionState) {
sources.append(source)
}
if let source = chatLinkOptions(selfController: selfController, sourceNode: sourceNode, getContextController: {
return getContextController?()
}, replySelectionState: replySelectionState) {
sources.append(source)
}
if sources.isEmpty {
return
}
selfController.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
selfController.canReadHistory.set(false)
let contextController = ContextController(
presentationData: selfController.presentationData,
configuration: ContextController.Configuration(
sources: sources,
initialId: AnyHashable(initialId)
)
)
contextController.dismissed = { [weak selfController] in
selfController?.canReadHistory.set(true)
}
getContextController = { [weak contextController] in
return contextController
}
selfController.presentInGlobalOverlay(contextController)
}
private func chatForwardOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?) -> ContextController.Source? {
guard let peerId = selfController.chatLocation.peerId else {
return nil
}
let presentationData = selfController.presentationData
let forwardOptions = selfController.presentationInterfaceStatePromise.get()
@ -25,11 +82,11 @@ 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, replyOptions: nil)
return ChatControllerSubject.ForwardOptions(hideNames: hideNames, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false)
}
|> distinctUntilChanged
let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: ChatControllerSubject.MessageOptionsInfo(kind: .forward), options: forwardOptions), botStart: nil, mode: .standard(previewing: true))
let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .forward(ChatControllerSubject.MessageOptionsInfo.Forward(options: forwardOptions))), botStart: nil, mode: .standard(previewing: true))
chatController.canReadHistory.set(false)
let messageIds = selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? []
@ -201,15 +258,49 @@ func presentChatForwardOptions(selfController: ChatControllerImpl, sourceNode: A
return items
}
//TODO:localize
return ContextController.Source(
id: AnyHashable(OptionsId.forward),
title: "Forward",
source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)),
items: items |> map { ContextController.Items(content: .list($0)) }
)
}
func presentChatForwardOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) {
if "".isEmpty {
presentChatInputOptions(selfController: selfController, sourceNode: sourceNode, initialId: .forward)
return
}
var getContextController: (() -> ContextController?)?
guard let source = chatForwardOptions(selfController: selfController, sourceNode: sourceNode, getContextController: {
return getContextController?()
}) else {
return
}
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,
configuration: ContextController.Configuration(
sources: [source],
initialId: source.id
)
)
contextController.dismissed = { [weak selfController] in
selfController?.canReadHistory.set(true)
}
contextController.dismissedForCancel = { [weak selfController, weak chatController] in
getContextController = { [weak contextController] in
return contextController
}
//TODO:loc
/*contextController.dismissedForCancel = { [weak selfController, weak chatController] in
guard let selfController else {
return
}
@ -218,8 +309,7 @@ func presentChatForwardOptions(selfController: ChatControllerImpl, sourceNode: A
forwardMessageIds = forwardMessageIds.filter { selectedMessageIds.contains($0) }
selfController.updateChatPresentationInterfaceState(interactive: false, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds) }) })
}
}
contextController.immediateItemsTransitionAnimation = true
}*/
selfController.presentInGlobalOverlay(contextController)
}
@ -228,12 +318,9 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch
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)
let items = selfController.context.account.postbox.messagesAtIds([replySubject.messageId])
|> deliverOnMainQueue
|> map { [weak selfController, weak chatController] messages, messagesCount -> [ContextMenuItem] in
|> map { [weak selfController, weak chatController] messages -> [ContextMenuItem] in
guard let selfController, let chatController else {
return []
}
@ -297,12 +384,8 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch
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()
}, iconPosition: .left, action: { c, _ in
c.popItems()
})))
subItems.append(.separator)
@ -322,11 +405,12 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch
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))
c.pushItems(items: .single(ContextController.Items(content: .list(subItems), dismissed: { [weak contentNode] in
guard let contentNode else {
return
}
contentNode.cancelTextSelection()
})))
break
}
@ -371,45 +455,29 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch
return items |> map { ContextController.Items(content: .list($0), tip: tip) }
}
func presentChatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) {
private func chatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?, selectionState: Promise<ChatControllerSubject.MessageOptionsInfo.SelectionState>) -> ContextController.Source? {
guard let peerId = selfController.chatLocation.peerId else {
return
return nil
}
guard let replySubject = selfController.presentationInterfaceState.interfaceState.replyMessageSubject else {
return
return nil
}
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
var replyQuote: ChatControllerSubject.MessageOptionsInfo.ReplyQuote?
var replyQuote: ChatControllerSubject.MessageOptionsInfo.Quote?
if let quote = replySubject.quote {
replyQuote = ChatControllerSubject.MessageOptionsInfo.ReplyQuote(messageId: replySubject.messageId, text: quote.text)
replyQuote = ChatControllerSubject.MessageOptionsInfo.Quote(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
selectionState.set(.single(ChatControllerSubject.MessageOptionsInfo.SelectionState(quote: replyQuote)))
guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [replySubject.messageId.peerId], ids: [replySubject.messageId], info: .reply(ChatControllerSubject.MessageOptionsInfo.Reply(quote: replyQuote, selectionState: selectionState))), botStart: nil, mode: .standard(previewing: true)) as? ChatControllerImpl else {
return nil
}
chatController.canReadHistory.set(false)
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)
contextController.dismissed = { [weak selfController] in
selfController?.canReadHistory.set(true)
}
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 {
chatController.performTextSelectionAction = { [weak selfController] message, canCopy, text, action in
guard let selfController, let contextController = getContextController() else {
return
}
@ -417,6 +485,18 @@ func presentChatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASD
selfController.controllerInteraction?.performTextSelectionAction(message, canCopy, text, action)
}
//TODO:localize
return ContextController.Source(
id: AnyHashable(OptionsId.reply),
title: "Reply",
source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)),
items: items
)
}
func presentChatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) {
presentChatInputOptions(selfController: selfController, sourceNode: sourceNode, initialId: .reply)
}
func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubject: ChatInterfaceState.ReplyMessageSubject) {
@ -537,3 +617,205 @@ func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubj
selfController.effectiveNavigationController?.pushViewController(controller)
})
}
private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?, replySelectionState: Promise<ChatControllerSubject.MessageOptionsInfo.SelectionState>) -> ContextController.Source? {
guard let peerId = selfController.chatLocation.peerId else {
return nil
}
guard let initialUrlPreview = selfController.presentationInterfaceState.urlPreview else {
return nil
}
let linkOptions = combineLatest(queue: .mainQueue(),
selfController.presentationInterfaceStatePromise.get(),
replySelectionState.get()
)
|> map { state, replySelectionState -> ChatControllerSubject.LinkOptions in
let urlPreview = state.urlPreview ?? initialUrlPreview
var webpageOptions: TelegramMediaWebpageDisplayOptions = .default
if let (_, webpage) = state.urlPreview, case let .Loaded(content) = webpage.content {
webpageOptions = content.displayOptions
}
return ChatControllerSubject.LinkOptions(
messageText: state.interfaceState.composeInputState.inputText.string,
messageEntities: generateChatInputTextEntities(state.interfaceState.composeInputState.inputText, generateLinks: true),
replyMessageId: state.interfaceState.replyMessageSubject?.messageId,
replyQuote: replySelectionState.quote?.text,
url: urlPreview.0,
webpage: urlPreview.1,
linkBelowText: webpageOptions.position != .aboveText,
largeMedia: webpageOptions.largeMedia != false
)
}
|> distinctUntilChanged
guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .link(ChatControllerSubject.MessageOptionsInfo.Link(options: linkOptions))), botStart: nil, mode: .standard(previewing: true)) as? ChatControllerImpl else {
return nil
}
chatController.canReadHistory.set(false)
let items = linkOptions
|> deliverOnMainQueue
|> map { [weak selfController] linkOptions -> [ContextMenuItem] in
guard let selfController else {
return []
}
var items: [ContextMenuItem] = []
if "".isEmpty {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Above the Message", icon: { theme in
if linkOptions.linkBelowText {
return nil
} else {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}
}, action: { [weak selfController] _, f in
selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
guard var urlPreview = state.urlPreview else {
return state
}
if case let .Loaded(content) = urlPreview.1.content {
var displayOptions = content.displayOptions
displayOptions.position = .aboveText
urlPreview = (urlPreview.0, TelegramMediaWebpage(webpageId: urlPreview.1.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions))))
}
return state.updatedUrlPreview(urlPreview)
})
})))
items.append(.action(ContextMenuActionItem(text: "Below the Message", icon: { theme in
if !linkOptions.linkBelowText {
return nil
} else {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}
}, action: { [weak selfController] _, f in
selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
guard var urlPreview = state.urlPreview else {
return state
}
if case let .Loaded(content) = urlPreview.1.content {
var displayOptions = content.displayOptions
displayOptions.position = .belowText
urlPreview = (urlPreview.0, TelegramMediaWebpage(webpageId: urlPreview.1.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions))))
}
return state.updatedUrlPreview(urlPreview)
})
})))
}
if "".isEmpty {
if !items.isEmpty {
items.append(.separator)
}
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Smaller Media", icon: { theme in
if linkOptions.largeMedia {
return nil
} else {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}
}, action: { [weak selfController] _, f in
selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
guard var urlPreview = state.urlPreview else {
return state
}
if case let .Loaded(content) = urlPreview.1.content {
var displayOptions = content.displayOptions
displayOptions.largeMedia = false
urlPreview = (urlPreview.0, TelegramMediaWebpage(webpageId: urlPreview.1.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions))))
}
return state.updatedUrlPreview(urlPreview)
})
})))
items.append(.action(ContextMenuActionItem(text: "Larger Media", icon: { theme in
if !linkOptions.largeMedia {
return nil
} else {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
}
}, action: { [weak selfController] _, f in
selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
guard var urlPreview = state.urlPreview else {
return state
}
if case let .Loaded(content) = urlPreview.1.content {
var displayOptions = content.displayOptions
displayOptions.largeMedia = true
urlPreview = (urlPreview.0, TelegramMediaWebpage(webpageId: urlPreview.1.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions))))
}
return state.updatedUrlPreview(urlPreview)
})
})))
}
if !items.isEmpty {
items.append(.separator)
}
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Remove Link Preview", textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak selfController, weak chatController] c, f in
guard let selfController else {
return
}
//selfController.updateChatPresentationInterfaceState(interactive: false, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds) }) })
//selfController.controllerInteraction?.sendCurrentMessage(false)
let _ = selfController
let _ = chatController
f(.default)
})))
return items
}
chatController.performOpenURL = { [weak selfController] message, url in
guard let selfController else {
return
}
//TODO:
//func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: AccountContext, currentQuery: String?) -> (String?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? {
if let (updatedUrlPreviewUrl, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil), let updatedUrlPreviewUrl {
let _ = (signal
|> deliverOnMainQueue).start(next: { [weak selfController] result in
guard let selfController else {
return
}
selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in
if let webpage = result(nil), var urlPreview = state.urlPreview {
if case let .Loaded(content) = urlPreview.1.content, case let .Loaded(newContent) = webpage.content {
urlPreview = (updatedUrlPreviewUrl, TelegramMediaWebpage(webpageId: webpage.webpageId, content: .Loaded(newContent.withDisplayOptions(content.displayOptions))))
}
return state.updatedUrlPreview(urlPreview)
} else {
return state
}
})
})
}
}
//TODO:localize
return ContextController.Source(
id: AnyHashable(OptionsId.link),
title: "Link",
source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)),
items: items |> map { ContextController.Items(content: .list($0)) }
)
}
func presentChatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) {
presentChatInputOptions(selfController: selfController, sourceNode: sourceNode, initialId: .link)
}

View File

@ -545,6 +545,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var storyStats: PeerStoryStats?
var performTextSelectionAction: ((Message?, Bool, NSAttributedString, TextSelectionAction) -> Void)?
var performOpenURL: ((Message?, String) -> 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
@ -2756,7 +2757,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId)
}
strongSelf.openUrl(url, concealed: concealed, skipConcealedAlert: skipConcealedAlert, message: message)
if let performOpenURL = strongSelf.performOpenURL {
performOpenURL(message, url)
} else {
strongSelf.openUrl(url, concealed: concealed, skipConcealedAlert: skipConcealedAlert, message: message)
}
}
}, shareCurrentLocation: { [weak self] in
if let strongSelf = self {
@ -3948,35 +3953,42 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
f()
}
case let .quote(range):
if let currentContextController = strongSelf.currentContextController {
currentContextController.dismiss(completion: {
})
let completion: (ContainedViewLayoutTransition?) -> Void = { transition in
guard let self else {
return
}
if let currentContextController = self.currentContextController {
self.currentContextController = nil
if let transition {
currentContextController.dismissWithCustomTransition(transition: transition)
} else {
currentContextController.dismiss(completion: {})
}
}
}
let completion: (ContainedViewLayoutTransition) -> Void = { _ in }
if let messageId = message?.id {
if let messageId = message?.id, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
var quoteData: EngineMessageReplyQuote?
let quoteText = (message.text as NSString).substring(with: NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound))
quoteData = EngineMessageReplyQuote(text: quoteText, entities: [])
let replySubject = ChatInterfaceState.ReplyMessageSubject(
messageId: message.id,
quote: quoteData
)
if canSendMessagesToChat(strongSelf.presentationInterfaceState) {
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
var quoteData: EngineMessageReplyQuote?
let quoteText = (message.text as NSString).substring(with: NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound))
quoteData = EngineMessageReplyQuote(text: quoteText, entities: [])
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(
messageId: message.id,
quote: quoteData
)) }).updatedSearch(nil).updatedShowCommands(false) }, completion: completion)
strongSelf.updateItemNodesSearchTextHighlightStates()
strongSelf.chatDisplayNode.ensureInputViewFocused()
} else {
completion(.immediate)
}
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject) }).updatedSearch(nil).updatedShowCommands(false) }, completion: completion)
strongSelf.updateItemNodesSearchTextHighlightStates()
strongSelf.chatDisplayNode.ensureInputViewFocused()
}, alertAction: {
completion(.immediate)
completion(nil)
}, delay: true)
} else {
completion(.immediate)
moveReplyMessageToAnotherChat(selfController: strongSelf, replySubject: replySubject)
completion(nil)
}
} else {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil) }) }, completion: completion)
@ -5008,6 +5020,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
//}
self.chatTitleView = ChatTitleView(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer)
if case .messageOptions = self.subject {
self.chatTitleView?.disableAnimations = true
}
self.navigationItem.titleView = self.chatTitleView
self.chatTitleView?.longPressed = { [weak self] in
if let strongSelf = self, let peerView = strongSelf.peerView, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil && !strongSelf.presentationInterfaceState.isNotAccessible {
@ -5208,7 +5225,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return message?.totalCount
}
|> distinctUntilChanged
} else if case let .messageOptions(peerIds, messageIds, info, options) = subject {
} else if case let .messageOptions(peerIds, messageIds, info) = subject {
displayedCountSignal = self.presentationInterfaceStatePromise.get()
|> map { state -> Int? in
if let selectionState = state.interfaceState.selectionState {
@ -5224,9 +5241,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let presentationData = self.presentationData
switch info.kind {
case .forward:
subtitleTextSignal = combineLatest(peers, options, displayedCountSignal)
switch info {
case let .forward(forward):
subtitleTextSignal = combineLatest(peers, forward.options, displayedCountSignal)
|> map { peersView, options, count in
let peers = peersView.peers.values
if !peers.isEmpty {
@ -5297,6 +5314,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .reply:
//TODO:localize
subtitleTextSignal = .single("You can select a specific part to quote")
case .link:
//TODO:localize
subtitleTextSignal = .single("Tap on a link to generate its preview")
}
}
@ -5309,9 +5329,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else {
hasPeerInfo = .single(true)
}
enum MessageOptionsTitleInfo {
case reply(hasQuote: Bool)
}
let messageOptionsTitleInfo: Signal<MessageOptionsTitleInfo?, NoError>
if case let .messageOptions(_, _, info) = self.subject {
switch info {
case .forward, .link:
messageOptionsTitleInfo = .single(nil)
case let .reply(reply):
messageOptionsTitleInfo = reply.selectionState.get()
|> map { selectionState -> Bool in
return selectionState.quote != nil
}
|> distinctUntilChanged
|> map { hasQuote -> MessageOptionsTitleInfo in
return .reply(hasQuote: hasQuote)
}
}
} else {
messageOptionsTitleInfo = .single(nil)
}
self.titleDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, displayedCountSignal, subtitleTextSignal, self.presentationInterfaceStatePromise.get(), hasPeerInfo)
|> deliverOnMainQueue).startStrict(next: { [weak self] peerView, onlineMemberCount, displayedCount, subtitleText, presentationInterfaceState, hasPeerInfo in
self.titleDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, displayedCountSignal, subtitleTextSignal, self.presentationInterfaceStatePromise.get(), hasPeerInfo, messageOptionsTitleInfo)
|> deliverOnMainQueue).startStrict(next: { [weak self] peerView, onlineMemberCount, displayedCount, subtitleText, presentationInterfaceState, hasPeerInfo, messageOptionsTitleInfo in
if let strongSelf = self {
var isScheduledMessages = false
if case .scheduledMessages = presentationInterfaceState.subject {
@ -5319,14 +5361,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if let peer = peerViewMainPeer(peerView) {
if case let .messageOptions(_, _, info, _) = presentationInterfaceState.subject {
if case let .reply(initialQuote) = info.kind {
if case let .messageOptions(_, _, info) = presentationInterfaceState.subject {
if case .reply = info {
//TODO:localize
if initialQuote != nil {
if case let .reply(hasQuote) = messageOptionsTitleInfo, hasQuote {
strongSelf.chatTitleView?.titleContent = .custom("Reply to Quote", subtitleText, false)
} else {
strongSelf.chatTitleView?.titleContent = .custom("Reply to Message", subtitleText, false)
}
} else if case .link = info {
//TODO:localize
strongSelf.chatTitleView?.titleContent = .custom("Link Preview Settings", subtitleText, false)
} else if displayedCount == 1 {
strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Conversation_ForwardOptions_ForwardTitleSingle, subtitleText, false)
} else {
@ -6661,7 +6706,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
if case let .messageOptions(_, messageIds, _, _) = self.subject, messageIds.count > 1 {
if case let .messageOptions(_, messageIds, _) = self.subject, messageIds.count > 1 {
self.updateChatPresentationInterfaceState(interactive: false, { state in
return state.updatedInterfaceState({ $0.withUpdatedSelectedMessages(messageIds) })
})
@ -8852,6 +8897,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
presentChatReplyOptions(selfController: self, sourceNode: sourceNode)
}, presentLinkOptions: { [weak self] sourceNode in
guard let self else {
return
}
presentChatLinkOptions(selfController: self, sourceNode: sourceNode)
}, shareSelectedMessages: { [weak self] in
if let strongSelf = self, let selectedIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty {
strongSelf.commitPurposefulAction()
@ -11924,16 +11974,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
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
}

View File

@ -394,27 +394,28 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.inputContextOverTextPanelContainer = ChatControllerTitlePanelNodeContainer()
var source: ChatHistoryListSource
if case let .messageOptions(_, messageIds, info, options) = subject {
let messages = combineLatest(context.account.postbox.messagesAtIds(messageIds), context.account.postbox.loadedPeerWithId(context.account.peerId), options)
|> map { messages, accountPeer, options -> ([Message], Int32, Bool) in
var messages = messages
let forwardedMessageIds = Set(messages.map { $0.id })
messages.sort(by: { lhsMessage, rhsMessage in
return lhsMessage.timestamp > rhsMessage.timestamp
})
messages = messages.map { message in
var flags = message.flags
flags.remove(.Incoming)
flags.remove(.IsIncomingMask)
var hideNames = options.hideNames
if message.id.peerId == accountPeer.id && message.forwardInfo == nil {
hideNames = true
}
var attributes = message.attributes
attributes = attributes.filter({ attribute in
if case .forward = info.kind {
if case let .messageOptions(_, messageIds, info) = subject {
switch info {
case let .forward(forward):
let messages = combineLatest(context.account.postbox.messagesAtIds(messageIds), context.account.postbox.loadedPeerWithId(context.account.peerId), forward.options)
|> map { messages, accountPeer, options -> ([Message], Int32, Bool) in
var messages = messages
let forwardedMessageIds = Set(messages.map { $0.id })
messages.sort(by: { lhsMessage, rhsMessage in
return lhsMessage.timestamp > rhsMessage.timestamp
})
messages = messages.map { message in
var flags = message.flags
flags.remove(.Incoming)
flags.remove(.IsIncomingMask)
var hideNames = options.hideNames
if message.id.peerId == accountPeer.id && message.forwardInfo == nil {
hideNames = true
}
var attributes = message.attributes
attributes = attributes.filter({ attribute in
if attribute is EditedMessageAttribute {
return false
}
@ -438,15 +439,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
if attribute is ReactionsMessageAttribute {
return false
}
}
return true
})
var messageText = message.text
var messageMedia = message.media
var hasDice = false
if case .forward = info.kind {
return true
})
var messageText = message.text
var messageMedia = message.media
var hasDice = false
if hideNames {
for media in message.media {
if options.hideCaptions {
@ -478,14 +477,112 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
return message.withUpdatedFlags(flags).withUpdatedText(messageText).withUpdatedMedia(messageMedia).withUpdatedTimestamp(Int32(context.account.network.context.globalTime())).withUpdatedAttributes(attributes).withUpdatedAuthor(accountPeer).withUpdatedForwardInfo(forwardInfo)
} else {
}
return (messages, Int32(messages.count), false)
}
source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), loadMore: nil)
case .reply:
let messages = combineLatest(context.account.postbox.messagesAtIds(messageIds), context.account.postbox.loadedPeerWithId(context.account.peerId))
|> map { messages, accountPeer -> ([Message], Int32, Bool) in
var messages = messages
messages.sort(by: { lhsMessage, rhsMessage in
return lhsMessage.timestamp > rhsMessage.timestamp
})
messages = messages.map { message in
return message
}
return (messages, Int32(messages.count), false)
}
return (messages, Int32(messages.count), false)
source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), loadMore: nil)
case let .link(link):
let messages = link.options
|> mapToSignal { options -> Signal<(ChatControllerSubject.LinkOptions, Peer, Message?), NoError> in
if let replyMessageId = options.replyMessageId {
return combineLatest(
context.account.postbox.messagesAtIds([replyMessageId]),
context.account.postbox.loadedPeerWithId(context.account.peerId)
)
|> map { messages, peer -> (ChatControllerSubject.LinkOptions, Peer, Message?) in
return (options, peer, messages.first)
}
} else {
return context.account.postbox.loadedPeerWithId(context.account.peerId)
|> map { peer -> (ChatControllerSubject.LinkOptions, Peer, Message?) in
return (options, peer, nil)
}
}
}
|> map { options, accountPeer, replyMessage -> ([Message], Int32, Bool) in
var peers = SimpleDictionary<PeerId, Peer>()
peers[accountPeer.id] = accountPeer
var associatedMessages = SimpleDictionary<MessageId, Message>()
var media: [Media] = []
if case let .Loaded(content) = options.webpage.content {
var displayOptions: TelegramMediaWebpageDisplayOptions = .default
if options.linkBelowText {
displayOptions.position = .belowText
} else {
displayOptions.position = .aboveText
}
if options.largeMedia {
displayOptions.largeMedia = true
} else {
displayOptions.largeMedia = false
}
media.append(TelegramMediaWebpage(webpageId: options.webpage.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions))))
}
var attributes: [MessageAttribute] = []
attributes.append(TextEntitiesMessageAttribute(entities: options.messageEntities))
if let replyMessage {
associatedMessages[replyMessage.id] = replyMessage
var mappedQuote: EngineMessageReplyQuote?
if let quote = options.replyQuote {
mappedQuote = EngineMessageReplyQuote(text: quote, entities: [])
}
attributes.append(ReplyMessageAttribute(messageId: replyMessage.id, threadMessageId: nil, quote: mappedQuote))
}
let message = Message(
stableId: 1,
stableVersion: 1,
id: MessageId(peerId: accountPeer.id, namespace: 0, id: 1),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: Int32(Date().timeIntervalSince1970),
flags: [],
tags: [],
globalTags: [],
localTags: [],
forwardInfo: nil,
author: accountPeer,
text: options.messageText,
attributes: attributes,
media: media,
peers: peers,
associatedMessages: associatedMessages,
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
return ([message], 1, false)
}
source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), loadMore: nil)
}
source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), loadMore: nil)
} else {
source = .default
}
@ -2981,12 +3078,31 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
switch self.chatPresentationInterfaceState.mode {
case .standard(previewing: true):
if let subject = self.controller?.subject, case let .messageOptions(_, _, info, _) = subject, case .reply = info.kind {
if let subject = self.controller?.subject, case let .messageOptions(_, _, info) = subject, case .reply = info {
if let controller = self.controller {
if let result = controller.presentationContext.hitTest(view: self.view, point: point, with: event) {
return result
}
}
if let result = self.historyNode.view.hitTest(self.view.convert(point, to: self.historyNode.view), with: event), let node = result.asyncdisplaykit_node {
if node is TextSelectionNode {
return result
}
}
} else if let subject = self.controller?.subject, case let .messageOptions(_, _, info) = subject, case .link = info {
if let controller = self.controller {
if let result = controller.presentationContext.hitTest(view: self.view, point: point, with: event) {
return result
}
}
if let result = self.historyNode.view.hitTest(self.view.convert(point, to: self.historyNode.view), with: event), let node = result.asyncdisplaykit_node {
if let textNode = node as? TextAccessibilityOverlayNode {
let _ = textNode
return result
}
}
}
if let result = self.historyNode.view.hitTest(self.view.convert(point, to: self.historyNode.view), with: event), let node = result.asyncdisplaykit_node, node is ChatMessageSelectionNode || node is GridMessageSelectionNode {

View File

@ -10,6 +10,9 @@ import ForwardAccessoryPanelNode
import ReplyAccessoryPanelNode
func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: AccessoryPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? {
if case .standard(previewing: true) = chatPresentationInterfaceState.mode {
return nil
}
if let _ = chatPresentationInterfaceState.interfaceState.selectionState {
return nil
}

View File

@ -271,7 +271,7 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
}
final class ChatMessageAttachedContentNode: ASDisplayNode {
private let lineNode: ASImageNode
private var backgroundView: UIImageView?
private let topTitleNode: TextNode
private let textNode: TextNodeWithEntities
private let inlineImageNode: TransformImageNode
@ -313,11 +313,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
}
override init() {
self.lineNode = ASImageNode()
self.lineNode.isLayerBacked = true
self.lineNode.displaysAsynchronously = false
self.lineNode.displayWithoutProcessing = true
self.topTitleNode = TextNode()
self.topTitleNode.isUserInteractionEnabled = false
self.topTitleNode.displaysAsynchronously = false
@ -339,7 +334,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
super.init()
self.addSubnode(self.lineNode)
self.addSubnode(self.topTitleNode)
self.addSubnode(self.textNode.textNode)
@ -350,6 +344,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
let topTitleAsyncLayout = TextNode.asyncLayout(self.topTitleNode)
let textAsyncLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let currentImage = self.media as? TelegramMediaImage
let currentMediaIsInline = self.inlineImageNode.supernode != nil
let imageLayout = self.inlineImageNode.asyncLayout()
let statusLayout = self.statusNode.asyncLayout()
let contentImageLayout = ChatMessageInteractiveMediaNode.asyncLayout(self.contentImageNode)
@ -378,13 +373,14 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
let textBlockQuoteFont = Font.regular(fontSize)
var incoming = message.effectivelyIncoming(context.account.peerId)
if let subject = associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .forward = info.kind {
if let subject = associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
incoming = false
}
var horizontalInsets = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0)
if displayLine {
horizontalInsets.left += 12.0
horizontalInsets.right += 12.0
}
var titleBeforeMedia = false
@ -698,7 +694,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
} else if let dimensions = largestImageRepresentation(image.representations)?.dimensions {
inlineImageDimensions = dimensions.cgSize
if image != currentImage {
if image != currentImage || !currentMediaIsInline {
updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image))
}
}
@ -742,10 +738,15 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
return (initialWidth, { constrainedSize, position in
var insets = UIEdgeInsets(top: 0.0, left: horizontalInsets.left, bottom: 5.0, right: horizontalInsets.right)
var lineInsets = insets
//insets.top += 4.0
//insets.bottom += 4.0
switch position {
case .linear(.None, _):
insets.top += 8.0
lineInsets.top += 8.0 + 8.0
insets.top += 10.0
insets.bottom += 8.0
lineInsets.top += 10.0 + 8.0
default:
break
}
@ -798,7 +799,38 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top)
let lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(presentationData.theme.theme)
let mainColor: UIColor
if !incoming {
mainColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor
} else {
var authorNameColor: UIColor?
let author = message.author
if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), author?.id.namespace == Namespaces.Peer.CloudUser {
authorNameColor = author.flatMap { chatMessagePeerIdColors[Int(clamping: $0.id.id._internalGetInt64Value() % 7)] }
if let rawAuthorNameColor = authorNameColor {
var dimColors = false
switch presentationData.theme.theme.name {
case .builtin(.nightAccent), .builtin(.night):
dimColors = true
default:
break
}
if dimColors {
var hue: CGFloat = 0.0
var saturation: CGFloat = 0.0
var brightness: CGFloat = 0.0
rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil)
authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0)
}
}
}
if let authorNameColor {
mainColor = authorNameColor
} else {
mainColor = presentationData.theme.theme.chat.message.incoming.accentTextColor
}
}
var boundingSize = textFrame.size
var lineHeight = textLayout.rawTextSize.height
@ -1020,9 +1052,22 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
strongSelf.media = mediaAndFlags?.0
strongSelf.theme = presentationData.theme
strongSelf.lineNode.image = lineImage
animation.animator.updateFrame(layer: strongSelf.lineNode.layer, frame: CGRect(origin: CGPoint(x: 13.0, y: insets.top), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)), completion: nil)
strongSelf.lineNode.isHidden = !displayLine
let backgroundView: UIImageView
if let current = strongSelf.backgroundView {
backgroundView = current
} else {
backgroundView = UIImageView()
strongSelf.backgroundView = backgroundView
strongSelf.view.insertSubview(backgroundView, at: 0)
}
if backgroundView.image == nil {
backgroundView.image = PresentationResourcesChat.chatReplyBackgroundTemplateImage(presentationData.theme.theme)
}
backgroundView.tintColor = mainColor
animation.animator.updateFrame(layer: backgroundView.layer, frame: CGRect(origin: CGPoint(x: 11.0, y: insets.top), size: CGSize(width: adjustedBoundingSize.width - 1.0 - insets.right, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)), completion: nil)
backgroundView.isHidden = !displayLine
strongSelf.textNode.textNode.displaysAsynchronously = !isPreview

View File

@ -273,7 +273,11 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
}
}
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
if content.displayOptions.position == .aboveText {
result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)), at: 0)
} else {
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
}
needReactions = false
}
break inner
@ -975,6 +979,27 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { [weak self] point in
if let strongSelf = self {
if let item = strongSelf.item, let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject {
if case .link = info {
for contentNode in strongSelf.contentNodes {
let contentNodePoint = strongSelf.view.convert(point, to: contentNode.view)
let tapAction = contentNode.tapActionAtPoint(contentNodePoint, gesture: .tap, isEstimating: true)
switch tapAction {
case .none:
break
case .ignore:
return .fail
case .url:
return .waitForSingleTap
default:
break
}
}
}
return .fail
}
if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) {
return .fail
}
@ -1125,7 +1150,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
self.view.addGestureRecognizer(replyRecognizer)
if let item = self.item, let subject = item.associatedData.subject, case .messageOptions = subject {
self.tapRecognizer?.isEnabled = false
//self.tapRecognizer?.isEnabled = false
self.replyRecognizer?.isEnabled = false
}
}
@ -1239,7 +1264,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
do {
let peerId = chatLocationPeerId
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .forward = info.kind {
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
displayAuthorInfo = false
} else if item.message.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
if let forwardInfo = item.content.firstMessage.forwardInfo {
@ -1913,7 +1938,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
}
let dateFormat: MessageTimestampStatusFormat
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .forward = info.kind {
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
dateFormat = .minimal
} else {
dateFormat = .regular
@ -2215,6 +2240,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
}
}
var updatedContentNodeOrder = false
if currentContentClassesPropertiesAndLayouts.count == contentNodeMessagesAndClasses.count {
for i in 0 ..< currentContentClassesPropertiesAndLayouts.count {
let currentClass: AnyClass = currentContentClassesPropertiesAndLayouts[i].1
let contentItem = contentNodeMessagesAndClasses[i] as (message: Message, type: AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)
if currentClass != contentItem.type {
updatedContentNodeOrder = true
break
}
}
}
var contentNodePropertiesAndFinalize: [(ChatMessageBubbleContentProperties, ChatMessageBubbleContentPosition?, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void), UInt32?, Bool?)] = []
var maxContentWidth: CGFloat = headerSize.width
@ -2617,6 +2654,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
replyInfoSizeApply: replyInfoSizeApply,
replyInfoOriginY: replyInfoOriginY,
removedContentNodeIndices: removedContentNodeIndices,
updatedContentNodeOrder: updatedContentNodeOrder,
addedContentNodes: addedContentNodes,
contentNodeMessagesAndClasses: contentNodeMessagesAndClasses,
contentNodeFramesPropertiesAndApply: contentNodeFramesPropertiesAndApply,
@ -2667,6 +2705,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
replyInfoSizeApply: (CGSize, (CGSize, Bool) -> ChatMessageReplyInfoNode?),
replyInfoOriginY: CGFloat,
removedContentNodeIndices: [Int]?,
updatedContentNodeOrder: Bool,
addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]?,
contentNodeMessagesAndClasses: [(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)],
contentNodeFramesPropertiesAndApply: [(CGRect, ChatMessageBubbleContentProperties, Bool, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)],
@ -3169,7 +3208,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
strongSelf.contentContainersWrapperNode.view.mask = nil
}
if removedContentNodeIndices?.count ?? 0 != 0 || addedContentNodes?.count ?? 0 != 0 {
if removedContentNodeIndices?.count ?? 0 != 0 || addedContentNodes?.count ?? 0 != 0 || updatedContentNodeOrder {
var updatedContentNodes = strongSelf.contentNodes
if let removedContentNodeIndices = removedContentNodeIndices {
@ -3536,7 +3575,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
}
if let subject = item.associatedData.subject, case .messageOptions = subject {
strongSelf.tapRecognizer?.isEnabled = false
//strongSelf.tapRecognizer?.isEnabled = false
strongSelf.replyRecognizer?.isEnabled = false
strongSelf.mainContainerNode.isGestureEnabled = false
for contentContainer in strongSelf.contentContainers {

View File

@ -96,7 +96,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
}
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .forward = info.kind {
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
incoming = false
}

View File

@ -106,7 +106,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
}
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .forward = info.kind {
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
incoming = false
}
let statusType: ChatMessageDateAndStatusType?

View File

@ -185,7 +185,7 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
}
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .forward = info.kind {
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
incoming = false
}

View File

@ -176,7 +176,7 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode {
}
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .forward = info.kind {
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
incoming = false
}

View File

@ -1805,7 +1805,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
item.controllerInteraction.performTextSelectionAction(item.message, true, text, action)
})
textSelectionNode.enableQuote = item.controllerInteraction.canSetupReply(item.message) == .reply
textSelectionNode.enableQuote = true
self.textSelectionNode = textSelectionNode
self.textClippingNode.addSubnode(textSelectionNode)
self.textClippingNode.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode)

View File

@ -235,7 +235,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
var updatedMuteIconImage: UIImage?
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .forward = info.kind {
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
incoming = false
}

View File

@ -18,7 +18,7 @@ public enum ChatMessageItemContent: Sequence {
case group(messages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)])
func effectivelyIncoming(_ accountPeerId: PeerId, associatedData: ChatMessageItemAssociatedData? = nil) -> Bool {
if let subject = associatedData?.subject, case let .messageOptions(_, _, info, _) = subject, case .forward = info.kind {
if let subject = associatedData?.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
return false
}
switch self {
@ -511,7 +511,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem)
var disableDate = self.disableDate
if let subject = self.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .reply = info.kind {
if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .reply = info {
disableDate = true
}

View File

@ -92,7 +92,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
}
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info, _) = subject, case .forward = info.kind {
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
incoming = false
}

View File

@ -347,6 +347,12 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
break
}
}
if webpage.displayOptions.largeMedia == false {
mediaAndFlags?.1.insert(.preferMediaInline)
} else {
mediaAndFlags?.1.remove(.preferMediaInline)
}
} else if let adAttribute = item.message.adAttribute {
title = nil
subtitle = nil

View File

@ -74,6 +74,7 @@ final class ChatRecentActionsController: TelegramBaseController {
}, updateForwardOptionsState: { _ in
}, presentForwardOptions: { _ in
}, presentReplyOptions: { _ in
}, presentLinkOptions: { _ in
}, shareSelectedMessages: {
}, updateTextInputStateAndMode: { _ in
}, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in

View File

@ -311,6 +311,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
}, updateForwardOptionsState: { _ in
}, presentForwardOptions: { _ in
}, presentReplyOptions: { _ in
}, presentLinkOptions: { _ in
}, shareSelectedMessages: {
shareMessages()
}, updateTextInputStateAndMode: { _ in

View File

@ -82,8 +82,18 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode {
self.iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
}
override public func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
if self.theme !== theme || self.strings !== strings {
self.updateThemeAndStrings(theme: theme, strings: strings, force: false)
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, force: Bool) {
if self.theme !== theme || self.strings !== strings || force {
self.strings = strings
if self.theme !== theme {
@ -209,4 +219,21 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode {
dismiss()
}
}
private var previousTapTimestamp: Double?
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let timestamp = CFAbsoluteTimeGetCurrent()
if let previousTapTimestamp = self.previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp {
return
}
self.previousTapTimestamp = CFAbsoluteTimeGetCurrent()
self.interfaceInteraction?.presentLinkOptions(self)
Queue.mainQueue().after(1.5) {
self.updateThemeAndStrings(theme: self.theme, strings: self.strings, force: true)
}
//let _ = ApplicationSpecificNotice.incrementChatReplyOptionsTip(accountManager: self.context.sharedContext.accountManager, count: 3).start()
}
}
}

View File

@ -144,7 +144,7 @@ private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType,
}
}
public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimatedEmojisInText: Int? = nil) -> [MessageTextEntity] {
public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimatedEmojisInText: Int? = nil, generateLinks: Bool = false) -> [MessageTextEntity] {
var entities: [MessageTextEntity] = []
text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: [], using: { attributes, range, _ in
@ -174,6 +174,13 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate
}
}
})
for entity in generateTextEntities(text.string, enabledTypes: .allUrl) {
if case .Url = entity.type {
entities.append(entity)
}
}
return entities
}

View File

@ -241,6 +241,8 @@ public final class TextSelectionNode: ASDisplayNode {
public var enableTranslate: Bool = true
public var enableShare: Bool = true
public var menuSkipCoordnateConversion: Bool = false
public var didRecognizeTap: Bool {
return self.recognizer?.didRecognizeTap ?? false
}
@ -549,6 +551,9 @@ public final class TextSelectionNode: ASDisplayNode {
self.currentRange = nil
self.recognizer?.isSelecting = false
self.updateSelection(range: nil, animateIn: false)
self.contextMenu?.dismiss()
self.contextMenu = nil
}
public func cancelSelection() {
@ -642,7 +647,9 @@ public final class TextSelectionNode: ASDisplayNode {
}))
}
let contextMenu = ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false)
self.contextMenu?.dismiss()
let contextMenu = ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false, skipCoordnateConversion: self.menuSkipCoordnateConversion)
contextMenu.dismissOnTap = { [weak self] view, point in
guard let self else {
return true
@ -658,7 +665,12 @@ public final class TextSelectionNode: ASDisplayNode {
guard let strongSelf = self, let rootNode = strongSelf.rootNode() else {
return nil
}
return (strongSelf, completeRect, rootNode, rootNode.bounds.insetBy(dx: 0.0, dy: -100.0))
if strongSelf.menuSkipCoordnateConversion {
return (strongSelf, strongSelf.view.convert(completeRect, to: rootNode.view), rootNode, rootNode.bounds)
} else {
return (strongSelf, completeRect, rootNode, rootNode.bounds)
}
}, bounce: false))
}