mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-30 01:11:46 +00:00
[WIP] Quotes
This commit is contained in:
parent
50881b558f
commit
a753d71cd7
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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: {
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
621
submodules/ContextUI/Sources/ContextSourceContainer.swift
Normal file
621
submodules/ContextUI/Sources/ContextSourceContainer.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -158,6 +158,10 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.pendingFetch?.disposable.dispose()
|
||||
}
|
||||
|
||||
func request(
|
||||
range: Range<Int64>,
|
||||
isFullRange: Bool,
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -74,6 +74,7 @@ final class ChatRecentActionsController: TelegramBaseController {
|
||||
}, updateForwardOptionsState: { _ in
|
||||
}, presentForwardOptions: { _ in
|
||||
}, presentReplyOptions: { _ in
|
||||
}, presentLinkOptions: { _ in
|
||||
}, shareSelectedMessages: {
|
||||
}, updateTextInputStateAndMode: { _ in
|
||||
}, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in
|
||||
|
@ -311,6 +311,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
|
||||
}, updateForwardOptionsState: { _ in
|
||||
}, presentForwardOptions: { _ in
|
||||
}, presentReplyOptions: { _ in
|
||||
}, presentLinkOptions: { _ in
|
||||
}, shareSelectedMessages: {
|
||||
shareMessages()
|
||||
}, updateTextInputStateAndMode: { _ in
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user