Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2023-10-19 20:44:53 +04:00
commit c4b35131a6
67 changed files with 1984 additions and 481 deletions

View File

@ -761,7 +761,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
self.mainButtonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _ in
self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _ in
}, setupEditMessage: { _, _ in
}, beginMessageSelection: { _, _ in
}, deleteSelectedMessages: {

View File

@ -67,7 +67,7 @@ public enum ChatOpenWebViewSource: Equatable {
}
public final class ChatPanelInterfaceInteraction {
public let setupReplyMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void
public let setupReplyMessage: (MessageId?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void
public let setupEditMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void
public let beginMessageSelection: ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void
public let deleteSelectedMessages: () -> Void
@ -173,7 +173,7 @@ public final class ChatPanelInterfaceInteraction {
public let statuses: ChatPanelInterfaceInteractionStatuses?
public init(
setupReplyMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void,
setupReplyMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void,
setupEditMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void,
beginMessageSelection: @escaping ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void,
deleteSelectedMessages: @escaping () -> Void,

View File

@ -22,6 +22,7 @@ public protocol ContextControllerProtocol: ViewController {
var getOverlayViews: (() -> [UIView])? { get set }
func dismiss(completion: (() -> Void)?)
func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?)
func getActionsMinHeight() -> ContextController.ActionsHeight?
func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, animated: Bool)
@ -2601,7 +2602,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
}
private func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) {
public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) {
if viewTreeContainsFirstResponder(view: self.view) {
self.dismissOnInputClose = (result, completion)
self.view.endEditing(true)

View File

@ -126,4 +126,8 @@ public final class PeekController: ViewController, ContextControllerProtocol {
self?.presentingViewController?.dismiss(animated: false, completion: nil)
})
}
public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) {
self.dismiss(completion: completion)
}
}

View File

@ -1133,7 +1133,7 @@ public extension ContainedViewLayoutTransition {
previousTransform = layer.transform
}
layer.transform = transform
layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in
layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, delay: delay, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in
completion?(value)
})
}
@ -1198,37 +1198,13 @@ public extension ContainedViewLayoutTransition {
}
}
func updateSublayerTransformScale(node: ASDisplayNode, scale: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
func updateSublayerTransformScale(node: ASDisplayNode, scale: CGFloat, delay: Double = 0.0, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
if !node.isNodeLoaded {
node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0)
completion?(true)
return
}
let t = node.layer.sublayerTransform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
if currentScale.isEqual(to: scale) {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
node.layer.removeAnimation(forKey: "sublayerTransform")
node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0)
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0)
node.layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: delay, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
result in
if let completion = completion {
completion(result)
}
})
}
self.updateSublayerTransformScale(layer: node.layer, scale: CGPoint(x: scale, y: scale), beginWithCurrentState: beginWithCurrentState, completion: completion)
}
func updateSublayerTransformScaleAdditive(node: ASDisplayNode, scale: CGFloat, completion: ((Bool) -> Void)? = nil) {

View File

@ -2236,7 +2236,9 @@ open class TextNode: ASDisplayNode {
}
let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray
if glyphRuns.count != 0 {
let hasAttachments = !line.attachments.isEmpty
for run in glyphRuns {
let run = run as! CTRun
let glyphCount = CTRunGetGlyphCount(run)
@ -2269,13 +2271,45 @@ open class TextNode: ASDisplayNode {
if fixDoubleEmoji {
context.setBlendMode(.normal)
}
CTRunDraw(run, context, CFRangeMake(0, glyphCount))
if hasAttachments {
let stringRange = CTRunGetStringRange(run)
if line.attachments.contains(where: { $0.range.contains(stringRange.location) }) {
} else {
CTRunDraw(run, context, CFRangeMake(0, glyphCount))
}
} else {
CTRunDraw(run, context, CFRangeMake(0, glyphCount))
}
if fixDoubleEmoji {
context.setBlendMode(blendMode)
}
}
}
for attachment in line.attachments {
let image = attachment.attachment
var textColor: UIColor?
layout.attributedString?.enumerateAttributes(in: attachment.range, options: []) { attributes, range, _ in
if let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor {
textColor = color
}
}
if let textColor {
if let tintedImage = generateTintedImage(image: image, color: textColor) {
let imageRect = CGRect(origin: CGPoint(x: attachment.frame.midX - tintedImage.size.width * 0.5, y: attachment.frame.midY - tintedImage.size.height * 0.5 + 1.0), size: tintedImage.size).offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.draw(tintedImage.cgImage!, in: imageRect)
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
}
}
}
if !line.strikethroughs.isEmpty {
for strikethrough in line.strikethroughs {
guard let lineRange = line.range else {

View File

@ -331,7 +331,7 @@ public func messageTextEntitiesInRange(entities: [MessageTextEntity], range: NSR
}
if entity.range.overlaps(range) {
var mappedRange = entity.range.clamped(to: range)
mappedRange = (entity.range.lowerBound - range.lowerBound) ..< (entity.range.upperBound - range.lowerBound)
mappedRange = (mappedRange.lowerBound - range.lowerBound) ..< (mappedRange.upperBound - range.lowerBound)
result.append(MessageTextEntity(range: mappedRange, type: entity.type))
}
}

View File

@ -170,13 +170,13 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
recognizer.tapActionAtPoint = { [weak self] point in
if let strongSelf = self {
let tapAction = strongSelf.tapActionAtPoint(point, gesture: .tap, isEstimating: true)
switch tapAction {
case .none:
break
case .ignore:
return .fail
case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji:
return .waitForSingleTap
switch tapAction.content {
case .none:
break
case .ignore:
return .fail
case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji:
return .waitForSingleTap
}
}
@ -427,20 +427,20 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return .url(url: url, concealed: concealed, activate: nil)
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName)
return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
return .botCommand(botCommand)
return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
} else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
} else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}
@ -449,49 +449,49 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false)
switch tapAction {
case .none, .ignore:
break
case let .url(url, concealed, activate):
self.item?.controllerInteraction.openUrl(url, concealed, nil, nil, activate?())
case let .peerMention(peerId, _, _):
if let item = self.item {
let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
if let peer = peer {
self?.item?.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
}
})
case .tap:
let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false)
switch tapAction.content {
case .none, .ignore:
break
case let .url(url):
self.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: url.concealed, progress: tapAction.activate?()))
case let .peerMention(peerId, _, _):
if let item = self.item {
let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
if let peer = peer {
self?.item?.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
}
case let .textMention(name):
self.item?.controllerInteraction.openPeerMention(name)
case let .botCommand(command):
self.item?.controllerInteraction.sendBotCommand(nil, command)
case let .hashtag(peerName, hashtag):
self.item?.controllerInteraction.openHashtag(peerName, hashtag)
default:
break
})
}
case let .textMention(name):
self.item?.controllerInteraction.openPeerMention(name)
case let .botCommand(command):
self.item?.controllerInteraction.sendBotCommand(nil, command)
case let .hashtag(peerName, hashtag):
self.item?.controllerInteraction.openHashtag(peerName, hashtag)
default:
break
}
case .longTap, .doubleTap:
if let item = self.item, self.backgroundNode.frame.contains(location) {
let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false)
switch tapAction {
case .none, .ignore:
break
case let .url(url, _, _):
item.controllerInteraction.longTap(.url(url), nil)
case let .peerMention(peerId, mention, _):
item.controllerInteraction.longTap(.peerMention(peerId, mention), nil)
case let .textMention(name):
item.controllerInteraction.longTap(.mention(name), nil)
case let .botCommand(command):
item.controllerInteraction.longTap(.command(command), nil)
case let .hashtag(_, hashtag):
item.controllerInteraction.longTap(.hashtag(hashtag), nil)
default:
break
switch tapAction.content {
case .none, .ignore:
break
case let .url(url):
item.controllerInteraction.longTap(.url(url.url), nil)
case let .peerMention(peerId, mention, _):
item.controllerInteraction.longTap(.peerMention(peerId, mention), nil)
case let .textMention(name):
item.controllerInteraction.longTap(.mention(name), nil)
case let .botCommand(command):
item.controllerInteraction.longTap(.command(command), nil)
case let .hashtag(_, hashtag):
item.controllerInteraction.longTap(.hashtag(hashtag), nil)
default:
break
}
}
default:

View File

@ -378,7 +378,7 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
self.controllerInteraction.sendMessage(markupButton.title)
dismissIfOnce = true
case let .url(url):
self.controllerInteraction.openUrl(url, true, nil, nil, nil)
self.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: true))
case .requestMap:
self.controllerInteraction.shareCurrentLocation()
case .requestPhone:

View File

@ -517,29 +517,29 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
if let (attributeText, fullText) = self.labelNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return .url(url: url, concealed: concealed, activate: nil)
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: true)
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: true))
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName)
return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
return .botCommand(botCommand)
return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
}
}
if let imageNode = imageNode, imageNode.frame.contains(point) {
return .openMessage
return ChatMessageBubbleContentTapAction(content: .openMessage)
}
if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) {
if let item = self.item, item.message.media.contains(where: { $0 is TelegramMediaStory }) {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
} else {
return .openMessage
return ChatMessageBubbleContentTapAction(content: .openMessage)
}
} else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}
}

View File

@ -83,7 +83,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
public var activateAction: (() -> Void)?
public var requestUpdateLayout: (() -> Void)?
public var defaultContentAction: () -> ChatMessageBubbleContentTapAction = { return .none }
public var defaultContentAction: () -> ChatMessageBubbleContentTapAction = { return ChatMessageBubbleContentTapAction(content: .none) }
public var visibility: ListViewItemNodeVisibility = .none {
didSet {
@ -731,6 +731,12 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
case .actionButton:
actualSize.height += buttonBottomSpacing
}
} else {
if let (_, inlineMediaSize) = inlineMediaAndSize {
if actualSize.height < insets.top + inlineMediaEdgeInset + inlineMediaSize.height + inlineMediaEdgeInset {
actualSize.height = insets.top + inlineMediaEdgeInset + inlineMediaSize.height + inlineMediaEdgeInset
}
}
}
if case let .linear(_, bottom) = position {
@ -798,7 +804,10 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
}
if let (inlineMediaValue, inlineMediaSize) = inlineMediaAndSize {
let inlineMediaFrame = CGRect(origin: CGPoint(x: actualSize.width - insets.right - inlineMediaSize.width, y: backgroundInsets.top + inlineMediaEdgeInset), size: inlineMediaSize)
var inlineMediaFrame = CGRect(origin: CGPoint(x: actualSize.width - insets.right - inlineMediaSize.width, y: backgroundInsets.top + inlineMediaEdgeInset), size: inlineMediaSize)
if contentLayoutOrder.isEmpty {
inlineMediaFrame.origin.x = insets.left
}
let inlineMedia: TransformImageNode
var updateMedia = false
@ -2004,21 +2013,21 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
if let (attributeText, fullText) = text.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return .url(url: url, concealed: concealed, activate: nil)
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName)
return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
return .botCommand(botCommand)
return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
}
}
}
if let actionButton = self.actionButton, actionButton.frame.contains(point) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return self.defaultContentAction()
@ -2103,9 +2112,9 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
highlightTimer.invalidate()
}
let transition: ContainedViewLayoutTransition = .animated(duration: self.isHighlighted ? 0.2 : 0.2, curve: .easeInOut)
let transition: ContainedViewLayoutTransition = .animated(duration: self.isHighlighted ? 0.3 : 0.2, curve: .easeInOut)
let scale: CGFloat = self.isHighlighted ? ((self.bounds.width - 5.0) / self.bounds.width) : 1.0
transition.updateSublayerTransformScale(node: self, scale: scale)
transition.updateSublayerTransformScale(node: self, scale: scale, beginWithCurrentState: true)
}
public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {

View File

@ -121,26 +121,52 @@ public enum ChatMessageBubblePreparePosition {
case mosaic(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition)
}
public enum ChatMessageBubbleContentTapAction {
case none
case url(url: String, concealed: Bool, activate: (() -> Promise<Bool>?)?)
case textMention(String)
case peerMention(peerId: PeerId, mention: String, openProfile: Bool)
case botCommand(String)
case hashtag(String?, String)
case instantPage
case wallpaper
case theme
case call(peerId: PeerId, isVideo: Bool)
case openMessage
case timecode(Double, String)
case tooltip(String, ASDisplayNode?, CGRect?)
case bankCard(String)
case ignore
case openPollResults(Data)
case copy(String)
case largeEmoji(String, String?, TelegramMediaFile)
case customEmoji(TelegramMediaFile)
public struct ChatMessageBubbleContentTapAction {
public struct Url {
public var url: String
public var concealed: Bool
public var allowInlineWebpageResolution: Bool
public init(
url: String,
concealed: Bool,
allowInlineWebpageResolution: Bool = false
) {
self.url = url
self.concealed = concealed
self.allowInlineWebpageResolution = allowInlineWebpageResolution
}
}
public enum Content {
case none
case url(Url)
case textMention(String)
case peerMention(peerId: PeerId, mention: String, openProfile: Bool)
case botCommand(String)
case hashtag(String?, String)
case instantPage
case wallpaper
case theme
case call(peerId: PeerId, isVideo: Bool)
case openMessage
case timecode(Double, String)
case tooltip(String, ASDisplayNode?, CGRect?)
case bankCard(String)
case ignore
case openPollResults(Data)
case copy(String)
case largeEmoji(String, String?, TelegramMediaFile)
case customEmoji(TelegramMediaFile)
}
public var content: Content
public var activate: (() -> Promise<Bool>?)?
public init(content: Content, activate: (() -> Promise<Bool>?)? = nil) {
self.content = content
self.activate = activate
}
}
public final class ChatMessageBubbleContentItem {
@ -235,7 +261,7 @@ open class ChatMessageBubbleContentNode: ASDisplayNode {
}
open func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
open func updateTouchesAtPoint(_ point: CGPoint?) {

View File

@ -521,6 +521,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private let mainContainerNode: ContextControllerSourceNode
private let backgroundWallpaperNode: ChatMessageBubbleBackdrop
private let backgroundNode: ChatMessageBackground
private var backgroundHighlightNode: ChatMessageBackground?
private let shadowNode: ChatMessageShadowNode
private var clippingNode: ChatMessageBubbleClippingNode
@ -557,7 +558,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private let messageAccessibilityArea: AccessibilityAreaNode
private var backgroundType: ChatMessageBackgroundType?
private var highlightedState: Bool = false
private struct HighlightedState: Equatable {
var quote: String?
}
private var highlightedState: HighlightedState?
private var backgroundFrameTransition: (CGRect, CGRect)?
@ -703,7 +708,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
if let singleUrl = accessibilityData.singleUrl {
strongSelf.item?.controllerInteraction.openUrl(singleUrl, false, false, strongSelf.item?.content.firstMessage, nil)
strongSelf.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: singleUrl, concealed: false, external: false, message: strongSelf.item?.content.firstMessage))
return true
}
@ -1007,7 +1012,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
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 {
switch tapAction.content {
case .none:
break
case .ignore:
@ -1066,7 +1071,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
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 {
switch tapAction.content {
case .none:
if let _ = strongSelf.item?.controllerInteraction.tapMessage {
return .waitForSingleTap
@ -2818,7 +2823,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if item.presentationData.theme.theme.forceSync {
legacyTransition = .immediate
}
strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, maskMode: strongSelf.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: legacyTransition, backgroundNode: presentationContext.backgroundNode)
strongSelf.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics, maskMode: strongSelf.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: legacyTransition, backgroundNode: presentationContext.backgroundNode)
strongSelf.backgroundWallpaperNode.setType(type: backgroundType, theme: item.presentationData.theme, essentialGraphics: graphics, maskMode: strongSelf.backgroundMaskMode, backgroundNode: presentationContext.backgroundNode)
strongSelf.shadowNode.setType(type: backgroundType, hasWallpaper: hasWallpaper, graphics: graphics)
@ -3535,6 +3540,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let backgroundAnimation = ListViewAnimation(from: strongSelf.backgroundNode.frame, to: backgroundFrame, duration: duration * UIView.animationDurationFactor(), curve: strongSelf.preferredAnimationCurve, beginAt: beginAt, update: { [weak strongSelf] _, frame in
if let strongSelf = strongSelf {
strongSelf.backgroundNode.frame = frame
if let backgroundHighlightNode = strongSelf.backgroundHighlightNode {
backgroundHighlightNode.frame = frame
backgroundHighlightNode.updateLayout(size: frame.size, transition: .immediate)
}
strongSelf.clippingNode.position = CGPoint(x: frame.midX, y: frame.midY)
strongSelf.clippingNode.bounds = CGRect(origin: CGPoint(x: frame.minX, y: frame.minY), size: frame.size)
@ -3546,6 +3555,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
strongSelf.setAnimationForKey("backgroundNodeFrame", animation: backgroundAnimation)
} else {
animation.animator.updateFrame(layer: strongSelf.backgroundNode.layer, frame: backgroundFrame, completion: nil)
if let backgroundHighlightNode = strongSelf.backgroundHighlightNode {
animation.animator.updateFrame(layer: backgroundHighlightNode.layer, frame: backgroundFrame, completion: nil)
backgroundHighlightNode.updateLayout(size: backgroundFrame.size, transition: animation)
}
animation.animator.updatePosition(layer: strongSelf.clippingNode.layer, position: backgroundFrame.center, completion: nil)
strongSelf.clippingNode.clipsToBounds = shouldClipOnTransitions
animation.animator.updateBounds(layer: strongSelf.clippingNode.layer, bounds: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size), completion: { [weak strongSelf] _ in
@ -3608,6 +3621,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if case .System = animation, strongSelf.mainContextSourceNode.isExtractedToContextPreview {
legacyTransition.updateFrame(node: strongSelf.backgroundNode, frame: backgroundFrame)
if let backgroundHighlightNode = strongSelf.backgroundHighlightNode {
legacyTransition.updateFrame(node: backgroundHighlightNode, frame: backgroundFrame, completion: nil)
backgroundHighlightNode.updateLayout(size: backgroundFrame.size, transition: legacyTransition)
}
legacyTransition.updateFrame(node: strongSelf.clippingNode, frame: backgroundFrame)
legacyTransition.updateBounds(node: strongSelf.clippingNode, bounds: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size))
@ -3617,6 +3634,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
strongSelf.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: legacyTransition)
} else {
strongSelf.backgroundNode.frame = backgroundFrame
if let backgroundHighlightNode = strongSelf.backgroundHighlightNode {
backgroundHighlightNode.frame = backgroundFrame
backgroundHighlightNode.updateLayout(size: backgroundFrame.size, transition: .immediate)
}
strongSelf.clippingNode.frame = backgroundFrame
strongSelf.clippingNode.bounds = CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size)
strongSelf.backgroundNode.updateLayout(size: backgroundFrame.size, transition: .immediate)
@ -3699,56 +3721,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
override public func shouldAnimateHorizontalFrameTransition() -> Bool {
return false
/*if let _ = self.backgroundFrameTransition {
return true
} else {
return false
}*/
}
override public func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
super.animateFrameTransition(progress, currentValue)
/*if let backgroundFrameTransition = self.backgroundFrameTransition {
let backgroundFrame = CGRect.interpolator()(backgroundFrameTransition.0, backgroundFrameTransition.1, progress) as! CGRect
self.backgroundNode.frame = backgroundFrame
self.clippingNode.frame = backgroundFrame
self.clippingNode.bounds = CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size)
self.backgroundNode.updateLayout(size: backgroundFrame.size, transition: .immediate)
self.backgroundWallpaperNode.frame = backgroundFrame
self.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: .immediate)
if let type = self.backgroundNode.type {
var incomingOffset: CGFloat = 0.0
switch type {
case .incoming:
incomingOffset = 5.0
default:
break
}
self.mainContextSourceNode.contentRect = backgroundFrame.offsetBy(dx: incomingOffset, dy: 0.0)
self.mainContainerNode.targetNodeForActivationProgressContentRect = self.mainContextSourceNode.contentRect
if !self.mainContextSourceNode.isExtractedToContextPreview {
if let (rect, size) = self.absoluteRect {
self.updateAbsoluteRect(rect, within: size)
}
}
}
self.messageAccessibilityArea.frame = backgroundFrame
if let item = self.item, let shareButtonNode = self.shareButtonNode {
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: true)
shareButtonNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize)
}
if CGFloat(1.0).isLessThanOrEqualTo(progress) {
self.backgroundFrameTransition = nil
self.clippingNode.clipsToBounds = false
}
}*/
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
@ -3948,7 +3924,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let convertedLocation = self.view.convert(location, to: contentNode.view)
let tapAction = contentNode.tapActionAtPoint(convertedLocation, gesture: gesture, isEstimating: false)
switch tapAction {
switch tapAction.content {
case .none:
if let item = self.item, self.backgroundNode.frame.contains(CGPoint(x: self.frame.width - location.x, y: location.y)), let tapMessage = self.item?.controllerInteraction.tapMessage {
return .action({
@ -3964,12 +3940,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return .action({
})
}
case let .url(url, concealed, activate):
case let .url(url):
return .action({ [weak self] in
guard let self, let item = self.item else {
return
}
item.controllerInteraction.openUrl(url, concealed, nil, item.content.firstMessage, activate?())
item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: url.concealed, message: item.content.firstMessage, allowInlineWebpageResolution: url.allowInlineWebpageResolution, progress: tapAction.activate?()))
})
case let .peerMention(peerId, _, openProfile):
return .action({ [weak self] in
@ -4123,12 +4099,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
tapMessage = contentNode.item?.message
}
let tapAction = contentNode.tapActionAtPoint(convertedLocation, gesture: gesture, isEstimating: false)
switch tapAction {
switch tapAction.content {
case .none, .ignore:
break
case let .url(url, _, _):
case let .url(url):
return .action({
item.controllerInteraction.longTap(.url(url), message)
item.controllerInteraction.longTap(.url(url.url), message)
})
case let .peerMention(peerId, mention, _):
return .action({
@ -4480,43 +4456,114 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return
}
var highlighted = false
var highlightedQuote: String?
var highlightedState: HighlightedState?
for contentNode in self.contentNodes {
let _ = contentNode.updateHighlightedState(animated: animated)
}
if let highlightedState = item.controllerInteraction.highlightedState {
if let highlightedStateValue = item.controllerInteraction.highlightedState {
for (message, _) in item.content {
if highlightedState.messageStableId == message.stableId {
highlighted = true
highlightedQuote = highlightedState.quote
if highlightedStateValue.messageStableId == message.stableId {
highlightedState = HighlightedState(quote: highlightedStateValue.quote)
break
}
}
}
if self.highlightedState != highlighted {
self.highlightedState = highlighted
if self.highlightedState != highlightedState {
self.highlightedState = highlightedState
for contentNode in self.contentNodes {
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
contentNode.updateQuoteTextHighlightState(text: nil, color: .clear, animated: true)
}
}
if let backgroundType = self.backgroundType {
let graphics = PresentationResourcesChat.principalGraphics(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper, bubbleCorners: item.presentationData.chatBubbleCorners)
let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper
self.backgroundNode.setType(type: backgroundType, highlighted: highlighted, graphics: graphics, maskMode: self.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, backgroundNode: item.controllerInteraction.presentationContext.backgroundNode)
}
}
if let highlightedQuote {
for contentNode in self.contentNodes {
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
contentNode.updateQuoteTextHighlightState(text: highlightedQuote, animated: animated)
}
}
} else {
for contentNode in self.contentNodes {
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
contentNode.updateQuoteTextHighlightState(text: nil, animated: animated)
if self.highlightedState != nil {
let backgroundHighlightNode: ChatMessageBackground
if let current = self.backgroundHighlightNode {
backgroundHighlightNode = current
} else {
backgroundHighlightNode = ChatMessageBackground()
self.mainContextSourceNode.contentNode.insertSubnode(backgroundHighlightNode, aboveSubnode: self.backgroundNode)
self.backgroundHighlightNode = backgroundHighlightNode
backgroundHighlightNode.setType(type: backgroundType, highlighted: true, graphics: graphics, maskMode: true, hasWallpaper: false, transition: .immediate, backgroundNode: nil)
backgroundHighlightNode.frame = self.backgroundNode.frame
backgroundHighlightNode.updateLayout(size: backgroundHighlightNode.frame.size, transition: .immediate)
if highlightedState?.quote != nil {
Queue.mainQueue().after(0.3, { [weak self] in
guard let self, let item = self.item, let backgroundHighlightNode = self.backgroundHighlightNode else {
return
}
if let highlightedState = self.highlightedState, let quote = highlightedState.quote {
let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper
let incoming: PresentationThemeBubbleColorComponents = !hasWallpaper ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper : item.presentationData.theme.theme.chat.message.incoming.bubble.withWallpaper
let outgoing: PresentationThemeBubbleColorComponents = !hasWallpaper ? item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper : item.presentationData.theme.theme.chat.message.outgoing.bubble.withWallpaper
let highlightColor: UIColor
if item.message.effectivelyIncoming(item.context.account.peerId) {
highlightColor = incoming.highlightedFill
} else {
highlightColor = outgoing.highlightedFill
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
var quoteFrame: CGRect?
for contentNode in self.contentNodes {
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
contentNode.updateQuoteTextHighlightState(text: quote, color: highlightColor, animated: false)
var sourceFrame = backgroundHighlightNode.view.convert(backgroundHighlightNode.bounds, to: contentNode.view)
if item.message.effectivelyIncoming(item.context.account.peerId) {
sourceFrame.origin.x += 6.0
sourceFrame.size.width -= 6.0
} else {
sourceFrame.size.width -= 6.0
}
if let localFrame = contentNode.animateQuoteTextHighlightIn(sourceFrame: sourceFrame, transition: transition) {
if self.contentNodes[0] !== contentNode && self.contentNodes[0].supernode === contentNode.supernode {
contentNode.supernode?.insertSubnode(contentNode, belowSubnode: self.contentNodes[0])
}
quoteFrame = contentNode.view.convert(localFrame, to: backgroundHighlightNode.view.superview)
}
break
}
}
if let quoteFrame {
self.backgroundHighlightNode = nil
backgroundHighlightNode.updateLayout(size: quoteFrame.size, transition: transition)
transition.updateFrame(node: backgroundHighlightNode, frame: quoteFrame)
backgroundHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, delay: 0.1, removeOnCompletion: false, completion: { [weak backgroundHighlightNode] _ in
backgroundHighlightNode?.removeFromSupernode()
})
}
}
})
}
}
} else {
if let backgroundHighlightNode = self.backgroundHighlightNode {
self.backgroundHighlightNode = nil
if animated {
backgroundHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak backgroundHighlightNode] _ in
backgroundHighlightNode?.removeFromSupernode()
})
} else {
backgroundHighlightNode.removeFromSupernode()
}
}
}
}
}

View File

@ -262,7 +262,7 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.frame.contains(point) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
} else if self.bounds.contains(point), let item = self.item {
var isVideo = false
for media in item.message.media {
@ -270,9 +270,9 @@ public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
isVideo = isVideoValue
}
}
return .call(peerId: item.message.id.peerId, isVideo: isVideo)
return ChatMessageBubbleContentTapAction(content: .call(peerId: item.message.id.peerId, isVideo: isVideo))
} else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}
}

View File

@ -410,9 +410,9 @@ public final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContent
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.frame.contains(point) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

View File

@ -379,12 +379,12 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.frame.contains(point) {
return .openMessage
return ChatMessageBubbleContentTapAction(content: .openMessage)
}
if self.dateAndStatusNode.supernode != nil, let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
@objc private func contactTap(_ recognizer: UITapGestureRecognizer) {

View File

@ -100,7 +100,7 @@ public final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessag
}
}*/
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {

View File

@ -95,7 +95,7 @@ public final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubble
}
}*/
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {

View File

@ -97,7 +97,7 @@ public final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBub
let contentNodeFrame = self.contentNode.frame
return self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture, isEstimating: isEstimating)
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func updateTouchesAtPoint(_ point: CGPoint?) {

View File

@ -221,10 +221,10 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.interactiveFileNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveFileNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.dateAndStatusNode.view), with: nil) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.interactiveFileNode.hasTapAction(at: self.view.convert(point, to: self.interactiveFileNode.view)) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return super.tapActionAtPoint(point, gesture: gesture, isEstimating: isEstimating)
}

View File

@ -129,7 +129,7 @@ public final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNod
}
}*/
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {

View File

@ -513,26 +513,26 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return .url(url: url, concealed: concealed, activate: nil)
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName)
return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
return .botCommand(botCommand)
return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
}
}
if self.buttonNode.frame.contains(point) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
} else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) {
return .openMessage
return ChatMessageBubbleContentTapAction(content: .openMessage)
} else if self.mediaBackgroundNode.frame.contains(point) {
return .openMessage
return ChatMessageBubbleContentTapAction(content: .openMessage)
} else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}

View File

@ -567,12 +567,12 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.frame.contains(point) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.dateAndStatusNode.supernode != nil, let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
@objc private func buttonPressed() {

View File

@ -414,18 +414,18 @@ public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentN
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if !self.interactiveFileNode.isHidden {
if self.interactiveFileNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveFileNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.dateAndStatusNode.view), with: nil) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.interactiveFileNode.hasTapAction(at: self.view.convert(point, to: self.interactiveFileNode.view)) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
}
if !self.interactiveVideoNode.isHidden {
if self.interactiveVideoNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveVideoNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveVideoNode.dateAndStatusNode.view), with: nil) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if let audioTranscriptionButton = self.interactiveVideoNode.audioTranscriptionButton, let _ = audioTranscriptionButton.hitTest(self.view.convert(point, to: audioTranscriptionButton), with: nil) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
}
return super.tapActionAtPoint(point, gesture: gesture, isEstimating: isEstimating)

View File

@ -125,7 +125,7 @@ public final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContent
}
}*/
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {

View File

@ -753,7 +753,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
if url.hasPrefix("tg://") {
concealed = false
}
item.controllerInteraction.openUrl(url, concealed, nil, nil, nil)
item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: concealed))
case .requestMap:
item.controllerInteraction.shareCurrentLocation()
case .requestPhone:

View File

@ -505,7 +505,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
@objc private func imageTap(_ recognizer: UITapGestureRecognizer) {

View File

@ -455,7 +455,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {

View File

@ -1608,17 +1608,17 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return .url(url: url, concealed: concealed, activate: nil)
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName)
return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
return .botCommand(botCommand)
return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
} else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
} else {
var isBotChat: Bool = false
@ -1629,7 +1629,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
for optionNode in self.optionNodes {
if optionNode.frame.contains(point), case .tap = gesture {
if optionNode.isUserInteractionEnabled {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
} else if let result = optionNode.currentResult, let item = self.item, !Namespaces.Message.allScheduled.contains(item.message.id.namespace), let poll = self.poll, let option = optionNode.option, !isBotChat {
switch poll.publicity {
case .anonymous:
@ -1648,7 +1648,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
string = item.presentationData.strings.MessagePoll_QuizCount(result.count)
}
}
return .tooltip(string, optionNode, optionNode.bounds.offsetBy(dx: 0.0, dy: 10.0))
return ChatMessageBubbleContentTapAction(content: .tooltip(string, optionNode, optionNode.bounds.offsetBy(dx: 0.0, dy: 10.0)))
case .public:
var hasNonZeroVoters = false
if let voters = poll.results.voters {
@ -1661,24 +1661,24 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
if hasNonZeroVoters {
if !isEstimating {
return .openPollResults(option.opaqueIdentifier)
return ChatMessageBubbleContentTapAction(content: .openPollResults(option.opaqueIdentifier))
}
return .openMessage
return ChatMessageBubbleContentTapAction(content: .openMessage)
}
}
}
}
}
if self.buttonNode.isUserInteractionEnabled, !self.buttonNode.isHidden, self.buttonNode.frame.contains(point) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.avatarsNode.isUserInteractionEnabled, !self.avatarsNode.isHidden, self.avatarsNode.frame.contains(point) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.solutionButtonNode.isUserInteractionEnabled, !self.solutionButtonNode.isHidden, !self.solutionButtonNode.alpha.isZero, self.solutionButtonNode.frame.contains(point) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}

View File

@ -314,9 +314,9 @@ public class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleCont
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.mediaBackgroundNode.frame.contains(point) {
return .openMessage
return ChatMessageBubbleContentTapAction(content: .openMessage)
} else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}
}

View File

@ -557,9 +557,9 @@ public final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleConte
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if let result = self.buttonsNode.hitTest(self.view.convert(point, to: self.buttonsNode.view), with: nil), result !== self.buttonsNode.view {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

View File

@ -26,6 +26,26 @@ private let quoteIcon: UIImage = {
return UIImage(bundleImageName: "Chat/Message/ReplyQuoteIcon")!.precomposed().withRenderingMode(.alwaysTemplate)
}()
private let channelIcon: UIImage = {
let sourceImage = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon")!
return generateImage(CGSize(width: sourceImage.size.width + 4.0, height: sourceImage.size.height + 4.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
sourceImage.draw(at: CGPoint(x: 2.0, y: 2.0))
UIGraphicsPopContext()
})!.precomposed().withRenderingMode(.alwaysTemplate)
}()
private let groupIcon: UIImage = {
let sourceImage = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextGroupIcon")!
return generateImage(CGSize(width: sourceImage.size.width + 3.0, height: sourceImage.size.height + 4.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
sourceImage.draw(at: CGPoint(x: 3.0, y: 1.0))
UIGraphicsPopContext()
})!.precomposed().withRenderingMode(.alwaysTemplate)
}()
public class ChatMessageReplyInfoNode: ASDisplayNode {
public final class TransitionReplyPanel {
public let titleNode: ASDisplayNode
@ -135,29 +155,119 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
let titleFont = Font.semibold(fontSize)
let textFont = Font.regular(fontSize)
var titleString: String
var titleString: NSAttributedString
var textString: NSAttributedString
let isMedia: Bool
let isText: Bool
var isExpiredStory: Bool = false
var isStory: Bool = false
let titleColor: UIColor
let mainColor: UIColor
let dustColor: UIColor
var secondaryColor: UIColor?
var authorNameColor: UIColor?
var dashSecondaryColor: UIColor?
let author = arguments.message?.effectiveAuthor
authorNameColor = author?.nameColor?.color
dashSecondaryColor = author?.nameColor?.dashColors.1
switch arguments.type {
case let .bubble(incoming):
titleColor = incoming ? (authorNameColor ?? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor) : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
if incoming {
if let authorNameColor {
mainColor = authorNameColor
secondaryColor = dashSecondaryColor
} else {
mainColor = arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor
}
} else {
mainColor = arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
}
dustColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
case .standalone:
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
titleColor = serviceColor.primaryText
mainColor = serviceMessageColorComponents(chatTheme: arguments.presentationData.theme.theme.chat, wallpaper: arguments.presentationData.theme.wallpaper).primaryText
dustColor = titleColor
}
if let message = arguments.message {
let author = message.effectiveAuthor
titleString = author.flatMap(EnginePeer.init)?.displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) ?? arguments.strings.User_DeletedAccount
let rawTitleString = author.flatMap(EnginePeer.init)?.displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder) ?? arguments.strings.User_DeletedAccount
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) || arguments.parentMessage.forwardInfo != nil {
if let author = forwardInfo.author {
titleString = EnginePeer(author).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
let rawTitleString = EnginePeer(author).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
} else if let authorSignature = forwardInfo.authorSignature {
titleString = authorSignature
let rawTitleString = authorSignature
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
}
}
if message.id.peerId != arguments.parentMessage.id.peerId {
//TODO:localize
if let peer = message.peers[message.id.peerId], (peer is TelegramChannel || peer is TelegramGroup) {
titleString += " in \(peer.debugDisplayTitle)"
if message.id.peerId != arguments.parentMessage.id.peerId, let peer = message.peers[message.id.peerId], (peer is TelegramChannel || peer is TelegramGroup) {
final class RunDelegateData {
let ascent: CGFloat
let descent: CGFloat
let width: CGFloat
init(ascent: CGFloat, descent: CGFloat, width: CGFloat) {
self.ascent = ascent
self.descent = descent
self.width = width
}
}
let font = titleFont
let runDelegateData = RunDelegateData(
ascent: font.ascender,
descent: font.descender,
width: channelIcon.size.width
)
var callbacks = CTRunDelegateCallbacks(
version: kCTRunDelegateCurrentVersion,
dealloc: { dataRef in
Unmanaged<RunDelegateData>.fromOpaque(dataRef).release()
},
getAscent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().ascent
},
getDescent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().descent
},
getWidth: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().width
}
)
if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) {
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
let rawTitleString = NSMutableAttributedString(attributedString: titleString)
rawTitleString.insert(NSAttributedString(string: ">", attributes: [
.attachment: channelIcon,
.foregroundColor: titleColor,
NSAttributedString.Key(rawValue: kCTRunDelegateAttributeName as String): runDelegate
]), at: 0)
titleString = rawTitleString
} else {
let rawTitleString = NSMutableAttributedString(attributedString: titleString)
rawTitleString.append(NSAttributedString(string: ">", attributes: [
.attachment: groupIcon,
.foregroundColor: titleColor,
NSAttributedString.Key(rawValue: kCTRunDelegateAttributeName as String): runDelegate
]))
rawTitleString.append(NSAttributedString(string: peer.debugDisplayTitle, font: titleFont, textColor: titleColor))
titleString = rawTitleString
}
}
}
@ -167,9 +277,11 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
isText = isTextValue
} else if let replyForward = arguments.replyForward {
if let replyAuthorId = replyForward.peerId, let replyAuthor = arguments.parentMessage.peers[replyAuthorId] {
titleString = EnginePeer(replyAuthor).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
let rawTitleString = EnginePeer(replyAuthor).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
} else {
titleString = replyForward.authorName ?? " "
let rawTitleString = replyForward.authorName ?? " "
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
}
//TODO:localize
@ -191,9 +303,11 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
isText = replyForward.quote?.text != nil && replyForward.quote?.text != ""
} else if let story = arguments.story {
if let authorPeer = arguments.parentMessage.peers[story.peerId] {
titleString = EnginePeer(authorPeer).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
let rawTitleString = EnginePeer(authorPeer).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
} else {
titleString = arguments.strings.User_DeletedAccount
let rawTitleString = arguments.strings.User_DeletedAccount
titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: titleColor)
}
isText = false
@ -221,65 +335,20 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
isMedia = true
}
} else {
titleString = " "
titleString = NSAttributedString(string: " ", font: titleFont, textColor: titleColor)
textString = NSAttributedString(string: " ")
isMedia = true
isText = false
}
let placeholderColor: UIColor = arguments.parentMessage.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : arguments.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
let isIncoming = arguments.parentMessage.effectivelyIncoming(arguments.context.account.peerId)
let placeholderColor: UIColor = isIncoming ? arguments.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : arguments.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
let titleColor: UIColor
let textColor: UIColor
let dustColor: UIColor
var authorNameColor: UIColor?
var dashSecondaryColor: UIColor?
let author = arguments.message?.effectiveAuthor
// if ([Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(arguments.parentMessage.id.peerId.namespace) && author?.id.namespace == Namespaces.Peer.CloudUser) {
authorNameColor = author?.nameColor?.color
dashSecondaryColor = author?.nameColor?.dashColors.1
// if let rawAuthorNameColor = authorNameColor {
// var dimColors = false
// switch arguments.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)
// }
// }
// }
let mainColor: UIColor
var secondaryColor: UIColor?
var isIncoming = false
switch arguments.type {
case let .bubble(incoming):
titleColor = incoming ? (authorNameColor ?? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor) : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
if incoming {
isIncoming = true
if let authorNameColor {
mainColor = authorNameColor
secondaryColor = dashSecondaryColor
} else {
mainColor = arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor
}
} else {
mainColor = arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
if let _ = dashSecondaryColor {
secondaryColor = arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
}
}
if isExpiredStory || isStory {
textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
} else if isMedia {
@ -287,17 +356,10 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
} else {
textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.primaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.primaryTextColor
}
dustColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
case .standalone:
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
titleColor = serviceColor.primaryText
mainColor = serviceMessageColorComponents(chatTheme: arguments.presentationData.theme.theme.chat, wallpaper: arguments.presentationData.theme.wallpaper).primaryText
textColor = titleColor
dustColor = titleColor
}
let messageText: NSAttributedString
if isText, let message = arguments.message {
var text: String
@ -453,7 +515,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
}
}
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: maxTitleNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: contrainedTextSize.width - additionalTitleWidth, height: contrainedTextSize.height), alignment: .natural, cutout: nil, insets: textInsets))
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: maxTitleNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: contrainedTextSize.width - additionalTitleWidth, height: contrainedTextSize.height), alignment: .natural, cutout: nil, insets: textInsets))
if isExpiredStory || isStory {
contrainedTextSize.width -= 26.0
}
@ -707,9 +769,9 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
isHighlighted = true
}
let transition: ContainedViewLayoutTransition = .animated(duration: isHighlighted ? 0.1 : 0.2, curve: .easeInOut)
let transition: ContainedViewLayoutTransition = .animated(duration: isHighlighted ? 0.3 : 0.2, curve: .easeInOut)
let scale: CGFloat = isHighlighted ? ((self.bounds.width - 5.0) / self.bounds.width) : 1.0
transition.updateSublayerTransformScale(node: self, scale: scale)
transition.updateSublayerTransformScale(node: self, scale: scale, beginWithCurrentState: true)
}
public func animateFromInputPanel(sourceReplyPanel: TransitionReplyPanel, unclippedTransitionNode: ASDisplayNode? = nil, localRect: CGRect, transition: CombinedTransition) -> CGPoint {

View File

@ -361,9 +361,9 @@ public class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode {
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.mediaBackgroundNode.frame.contains(point) {
return .openMessage
return ChatMessageBubbleContentTapAction(content: .openMessage)
} else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}
}

View File

@ -36,6 +36,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/MessageQuoteComponent",
"//submodules/TelegramUI/Components/TextLoadingEffect",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
],
visibility = [
"//visibility:public",

View File

@ -25,6 +25,7 @@ import ChatMessageBubbleContentNode
import ShimmeringLinkNode
import ChatMessageItemCommon
import TextLoadingEffect
import ChatControllerInteraction
private final class CachedChatMessageText {
let text: String
@ -72,7 +73,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private var linkPreviewOptionsDisposable: Disposable?
private var linkPreviewHighlightingNodes: [LinkHighlightingNode] = []
private var quoteHighlightingNodes: [LinkHighlightingNode] = []
private var quoteHighlightingNode: LinkHighlightingNode?
private var linkProgressRange: NSRange?
private var linkProgressView: TextLoadingEffectView?
@ -113,7 +114,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.textAccessibilityOverlayNode)
self.textAccessibilityOverlayNode.openUrl = { [weak self] url in
self?.item?.controllerInteraction.openUrl(url, false, false, nil, nil)
self?.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: false, external: false))
}
self.statusNode.reactionSelected = { [weak self] value in
@ -678,14 +679,14 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
if case .tap = gesture {
} else {
if let item = self.item, let subject = item.associatedData.subject, case .messageOptions = subject {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}
let textNodeFrame = self.textNode.textNode.frame
if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true
var urlRange: NSRange?
@ -693,7 +694,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
urlRange = urlRangeValue
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return .url(url: url, concealed: concealed, activate: { [weak self] in
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)), activate: { [weak self] in
guard let self else {
return nil
}
@ -721,23 +722,23 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return promise
})
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName)
return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
return .botCommand(botCommand)
return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
} else if let timecode = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode {
return .timecode(timecode.time, timecode.text)
return ChatMessageBubbleContentTapAction(content: .timecode(timecode.time, timecode.text))
} else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String {
return .bankCard(bankCard)
return ChatMessageBubbleContentTapAction(content: .bankCard(bankCard))
} else if let pre = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre)] as? String {
return .copy(pre)
return ChatMessageBubbleContentTapAction(content: .copy(pre))
} else if let code = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Code)] as? String {
return .copy(code)
return ChatMessageBubbleContentTapAction(content: .copy(code))
} else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file {
return .customEmoji(file)
return ChatMessageBubbleContentTapAction(content: .customEmoji(file))
} else {
if let item = self.item, item.message.text.count == 1, !item.presentationData.largeEmoji {
let (emoji, fitz) = item.message.text.basicEmoji
@ -749,19 +750,19 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
if let emojiFile = emojiFile {
return .largeEmoji(emoji, fitz, emojiFile)
return ChatMessageBubbleContentTapAction(content: .largeEmoji(emoji, fitz, emojiFile))
} else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
} else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}
} else {
if let _ = self.statusNode.hitTest(self.view.convert(point, to: self.statusNode.view), with: nil) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}
@ -941,43 +942,75 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
public func updateQuoteTextHighlightState(text: String?, animated: Bool) {
guard let item = self.item else {
return
public func animateQuoteTextHighlightIn(sourceFrame: CGRect, transition: ContainedViewLayoutTransition) -> CGRect? {
if let quoteHighlightingNode = self.quoteHighlightingNode {
var currentRect = CGRect()
for rect in quoteHighlightingNode.rects {
if currentRect.isEmpty {
currentRect = rect
} else {
currentRect = currentRect.union(rect)
}
}
if !currentRect.isEmpty {
currentRect = currentRect.insetBy(dx: -quoteHighlightingNode.inset, dy: -quoteHighlightingNode.inset)
let innerRect = currentRect.offsetBy(dx: quoteHighlightingNode.frame.minX, dy: quoteHighlightingNode.frame.minY)
quoteHighlightingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.08, delay: 0.1)
let fromScale = CGPoint(x: sourceFrame.width / innerRect.width, y: sourceFrame.height / innerRect.height)
var fromTransform = CATransform3DIdentity
let fromOffset = CGPoint(x: sourceFrame.midX - innerRect.midX, y: sourceFrame.midY - innerRect.midY)
fromTransform = CATransform3DTranslate(fromTransform, fromOffset.x, fromOffset.y, 0.0)
fromTransform = CATransform3DTranslate(fromTransform, -quoteHighlightingNode.bounds.width * 0.5 + currentRect.midX, -quoteHighlightingNode.bounds.height * 0.5 + currentRect.midY, 0.0)
fromTransform = CATransform3DScale(fromTransform, fromScale.x, fromScale.y, 1.0)
fromTransform = CATransform3DTranslate(fromTransform, quoteHighlightingNode.bounds.width * 0.5 - currentRect.midX, quoteHighlightingNode.bounds.height * 0.5 - currentRect.midY, 0.0)
quoteHighlightingNode.transform = fromTransform
transition.updateTransform(node: quoteHighlightingNode, transform: CGAffineTransformIdentity)
return currentRect.offsetBy(dx: quoteHighlightingNode.frame.minX, dy: quoteHighlightingNode.frame.minY)
}
}
var rectsSet: [[CGRect]] = []
return nil
}
public func updateQuoteTextHighlightState(text: String?, color: UIColor, animated: Bool) {
var rectsSet: [CGRect] = []
if let text = text, !text.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string {
let nsString = string as NSString
let range = nsString.range(of: text)
if range.location != NSNotFound {
if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty {
rectsSet = [rects]
rectsSet = rects
}
}
}
for i in 0 ..< rectsSet.count {
let rects = rectsSet[i]
if !rectsSet.isEmpty {
let rects = rectsSet
let textHighlightNode: LinkHighlightingNode
if i < self.quoteHighlightingNodes.count {
textHighlightNode = self.quoteHighlightingNodes[i]
if let current = self.quoteHighlightingNode {
textHighlightNode = current
} else {
textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor)
self.quoteHighlightingNodes.append(textHighlightNode)
textHighlightNode = LinkHighlightingNode(color: color)
self.quoteHighlightingNode = textHighlightNode
self.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode)
}
textHighlightNode.frame = self.textNode.textNode.frame
textHighlightNode.updateRects(rects)
}
for i in (rectsSet.count ..< self.quoteHighlightingNodes.count).reversed() {
let node = self.quoteHighlightingNodes[i]
self.quoteHighlightingNodes.remove(at: i)
if animated {
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
node?.removeFromSupernode()
})
} else {
node.removeFromSupernode()
} else {
if let quoteHighlightingNode = self.quoteHighlightingNode {
self.quoteHighlightingNode = nil
if animated {
quoteHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak quoteHighlightingNode] _ in
quoteHighlightingNode?.removeFromSupernode()
})
} else {
quoteHighlightingNode.removeFromSupernode()
}
}
}
}

View File

@ -94,11 +94,11 @@ public final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleCon
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.bounds.contains(point) {
if self.buttonNode.frame.contains(point) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
} else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}

View File

@ -461,11 +461,11 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.statusOverlayNode.alpha > 0.0 {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
} else if self.mediaBackgroundNode.frame.contains(point) {
return .openMessage
return ChatMessageBubbleContentTapAction(content: .openMessage)
} else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
}
}

View File

@ -29,6 +29,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode",
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
],
visibility = [
"//visibility:public",

View File

@ -18,6 +18,7 @@ import ChatMessageItemCommon
import WallpaperPreviewMedia
import ChatMessageInteractiveMediaNode
import ChatMessageAttachedContentNode
import ChatControllerInteraction
private let titleFont: UIFont = Font.semibold(15.0)
@ -52,6 +53,15 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
} else if content.type == "telegram_theme" {
item.controllerInteraction.openTheme(item.message)
return
} else {
if content.title != nil || content.text != nil {
var isConcealed = true
if item.message.text.contains(content.url) {
isConcealed = false
}
item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: content.url, concealed: isConcealed))
return
}
}
}
let openChatMessageMode: ChatControllerInteractionOpenMessageMode
@ -90,7 +100,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
if item.message.text.contains(webpage.url) {
isConcealed = false
}
item.controllerInteraction.openUrl(webpage.url, isConcealed, nil, nil, nil)
item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: webpage.url, concealed: isConcealed))
}
}
}
@ -103,13 +113,13 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
}
self.contentNode.defaultContentAction = { [weak self] in
guard let self, let item = self.item, let webPage = self.webPage, case let .Loaded(content) = webPage.content else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
var isConcealed = true
if item.message.text.contains(content.url) {
isConcealed = false
}
return .url(url: content.url, concealed: isConcealed, activate: nil)
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: content.url, concealed: isConcealed, allowInlineWebpageResolution: true)))
}
}
@ -501,22 +511,22 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
guard let item = self.item else {
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
if self.bounds.contains(point) {
let contentNodeFrame = self.contentNode.frame
let result = self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture, isEstimating: isEstimating)
if item.message.adAttribute != nil {
if case .none = result {
if case .none = result.content {
if self.contentNode.hasActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY)) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
}
return result
}
switch result {
switch result.content {
case .none:
break
case let .textMention(value):
@ -527,9 +537,9 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
}
switch websiteType(of: content.websiteName) {
case .twitter:
return .url(url: "https://twitter.com/\(mention)", concealed: false, activate: nil)
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: "https://twitter.com/\(mention)", concealed: false)))
case .instagram:
return .url(url: "https://instagram.com/\(mention)", concealed: false, activate: nil)
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: "https://instagram.com/\(mention)", concealed: false)))
default:
break
}
@ -542,9 +552,9 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
}
switch websiteType(of: content.websiteName) {
case .twitter:
return .url(url: "https://twitter.com/hashtag/\(hashtag)", concealed: false, activate: nil)
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: "https://twitter.com/hashtag/\(hashtag)", concealed: false)))
case .instagram:
return .url(url: "https://instagram.com/explore/tags/\(hashtag)", concealed: false, activate: nil)
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: "https://instagram.com/explore/tags/\(hashtag)", concealed: false)))
default:
break
}
@ -557,22 +567,22 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
if content.instantPage != nil {
switch websiteType(of: content.websiteName) {
case .instagram, .twitter:
return .none
return ChatMessageBubbleContentTapAction(content: .none)
default:
return .instantPage
return ChatMessageBubbleContentTapAction(content: .instantPage)
}
} else if content.type == "telegram_background" {
return .wallpaper
return ChatMessageBubbleContentTapAction(content: .wallpaper)
} else if content.type == "telegram_theme" {
return .theme
return ChatMessageBubbleContentTapAction(content: .theme)
}
}
if self.contentNode.hasActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY)) {
return .ignore
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
return .none
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {

View File

@ -276,8 +276,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, openMessageContextActions: { _, _, _, _ in
}, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in
}, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url, _, _, _, _ in
self?.openUrl(url)
}, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url in
self?.openUrl(url.url)
}, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController)

View File

@ -19,6 +19,7 @@ import TextNodeWithEntities
import AnimationCache
import MultiAnimationRenderer
import AccessoryPanelNode
import AppBundle
func textStringForForwardedMessage(_ message: Message, strings: PresentationStrings) -> (text: String, entities: [MessageTextEntity], isMedia: Bool) {
for media in message.media {
@ -91,7 +92,7 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode {
let closeButton: HighlightableButtonNode
let lineNode: ASImageNode
let iconNode: ASImageNode
let iconView: UIImageView
let titleNode: ImmediateTextNode
let textNode: ImmediateTextNodeWithEntities
private var originalText: NSAttributedString?
@ -127,10 +128,9 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode {
self.lineNode.displaysAsynchronously = false
self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme)
self.iconNode = ASImageNode()
self.iconNode.displayWithoutProcessing = false
self.iconNode.displaysAsynchronously = false
self.iconNode.image = PresentationResourcesChat.chatInputPanelForwardIconImage(theme)
self.iconView = UIImageView()
self.iconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/ForwardSettingsIcon")?.withRenderingMode(.alwaysTemplate)
self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
@ -148,7 +148,7 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode {
self.addSubnode(self.closeButton)
self.addSubnode(self.lineNode)
self.addSubnode(self.iconNode)
self.view.addSubview(self.iconView)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.actionArea)
@ -283,11 +283,11 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode {
}
override public func animateIn() {
self.iconNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
self.iconView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
}
override public func animateOut() {
self.iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
self.iconView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
}
override public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
@ -302,7 +302,7 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode {
self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: [])
self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme)
self.iconNode.image = PresentationResourcesChat.chatInputPanelForwardIconImage(theme)
self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor
let filteredMessages = self.messages
@ -350,8 +350,8 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode {
self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0))
if let icon = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size)
if let icon = self.iconView.image {
self.iconView.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size)
}
let titleSize = self.titleNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height))

View File

@ -27,6 +27,7 @@ swift_library(
"//submodules/TelegramUI/Components/AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/TelegramUI/Components/Chat/AccessoryPanelNode",
"//submodules/TelegramUI/Components/CompositeTextNode",
"//submodules/TelegramNotices",
],
visibility = [

View File

@ -18,6 +18,8 @@ import AnimationCache
import MultiAnimationRenderer
import AccessoryPanelNode
import TelegramNotices
import AppBundle
import CompositeTextNode
public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
private let messageDisposable = MetaDisposable()
@ -29,8 +31,8 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
public let closeButton: HighlightableButtonNode
public let lineNode: ASImageNode
public let iconNode: ASImageNode
public let titleNode: ImmediateTextNode
public let iconView: UIImageView
public let titleNode: CompositeTextNode
public let textNode: ImmediateTextNodeWithEntities
public let imageNode: TransformImageNode
@ -64,15 +66,16 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
self.lineNode.displaysAsynchronously = false
self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme)
self.iconNode = ASImageNode()
self.iconNode.displayWithoutProcessing = false
self.iconNode.displaysAsynchronously = false
self.iconNode.image = PresentationResourcesChat.chatInputPanelReplyIconImage(theme)
self.iconView = UIImageView()
if quote != nil {
self.iconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/ReplyQuoteIcon")?.withRenderingMode(.alwaysTemplate)
} else {
self.iconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/ReplySettingsIcon")?.withRenderingMode(.alwaysTemplate)
}
self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.displaysAsynchronously = false
self.titleNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
self.titleNode = CompositeTextNode()
self.titleNode.imageTintColor = theme.chat.inputPanel.panelControlAccentColor
self.textNode = ImmediateTextNodeWithEntities()
self.textNode.maximumNumberOfLines = 1
@ -103,7 +106,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
self.addSubnode(self.closeButton)
self.addSubnode(self.lineNode)
self.addSubnode(self.iconNode)
self.view.addSubview(self.iconView)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.imageNode)
@ -114,7 +117,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
if let strongSelf = self {
if messageView.message == nil {
Queue.mainQueue().justDispatch {
strongSelf.interfaceInteraction?.setupReplyMessage(nil, { _ in })
strongSelf.interfaceInteraction?.setupReplyMessage(nil, { _, _ in })
}
return
}
@ -235,27 +238,61 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
}
}
var titleText: String
if let quote = strongSelf.quote {
//TODO:localize
titleText = "Reply to quote by \(authorName)"
var titleText: [CompositeTextNode.Component] = []
if let peer = message?.peers[strongSelf.messageId.peerId] as? TelegramChannel, case .broadcast = peer.info {
let icon: UIImage?
icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon")?.withRenderingMode(.alwaysTemplate)
if let _ = strongSelf.quote {
if let icon {
let string = "Reply to Quote by "
titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: .white))]
titleText.append(.icon(icon))
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(15.0), textColor: .white)))
}
} else {
if let icon {
let string = "Reply to "
titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: .white))]
titleText.append(.icon(icon))
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(15.0), textColor: .white)))
}
}
} else {
if let _ = strongSelf.quote {
//TODO:localize
let string = "Reply to Quote by \(authorName)"
titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: .white))]
} else {
let string = strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string
titleText = [.text(NSAttributedString(string: string, font: Font.medium(15.0), textColor: .white))]
strongSelf.textNode.attributedText = messageText
}
if strongSelf.messageId.peerId != strongSelf.chatPeerId {
if let peer = message?.peers[strongSelf.messageId.peerId], (peer is TelegramChannel || peer is TelegramGroup) {
let icon: UIImage?
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon")?.withRenderingMode(.alwaysTemplate)
} else {
icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextGroupIcon")?.withRenderingMode(.alwaysTemplate)
}
if let icon {
titleText.append(.icon(icon))
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(15.0), textColor: .white)))
}
}
}
}
if let quote = strongSelf.quote {
let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor
let quoteText = stringWithAppliedEntities(trimToLineCount(quote.text, lineCount: 1), entities: quote.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message)
strongSelf.textNode.attributedText = quoteText
} else {
titleText = strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string
strongSelf.textNode.attributedText = messageText
}
if strongSelf.messageId.peerId != strongSelf.chatPeerId {
if let peer = message?.peers[strongSelf.messageId.peerId], (peer is TelegramChannel || peer is TelegramGroup) {
titleText += " in \(peer.debugDisplayTitle)"
}
}
strongSelf.titleNode.attributedText = NSAttributedString(string: titleText, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor)
strongSelf.titleNode.components = titleText
let headerString: String
if let message = message, message.flags.contains(.Incoming), let author = message.author {
@ -329,11 +366,11 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
}
override public func animateIn() {
self.iconNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
self.iconView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
}
override public func animateOut() {
self.iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
self.iconView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
}
override public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
@ -347,11 +384,9 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: [])
self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme)
self.iconNode.image = PresentationResourcesChat.chatInputPanelReplyIconImage(theme)
self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor
if let text = self.titleNode.attributedText?.string {
self.titleNode.attributedText = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor)
}
self.titleNode.imageTintColor = theme.chat.inputPanel.panelControlAccentColor
if let text = self.textNode.attributedText?.string {
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.textIsOptions ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor)
@ -387,8 +422,8 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0))
}
if let icon = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size)
if let icon = self.iconView.image {
self.iconView.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size)
}
var imageTextInset: CGFloat = 0.0
@ -399,9 +434,9 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
self.imageNode.frame = CGRect(origin: CGPoint(x: leftInset + 9.0, y: 8.0), size: CGSize(width: 35.0, height: 35.0))
}
let titleSize = self.titleNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height))
let titleSize = self.titleNode.update(constrainedSize: CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height))
if self.titleNode.supernode == self {
self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset - self.titleNode.insets.left, y: 7.0 - self.titleNode.insets.top), size: titleSize)
self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset, y: 7.0), size: titleSize)
}
let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height))

View File

@ -90,6 +90,24 @@ public final class ChatControllerInteraction {
case groupParticipant(storyStats: PeerStoryStats?, avatarHeaderNode: ASDisplayNode?)
}
public struct OpenUrl {
public var url: String
public var concealed: Bool
public var external: Bool?
public var message: Message?
public var allowInlineWebpageResolution: Bool
public var progress: Promise<Bool>?
public init(url: String, concealed: Bool, external: Bool? = nil, message: Message? = nil, allowInlineWebpageResolution: Bool = false, progress: Promise<Bool>? = nil) {
self.url = url
self.concealed = concealed
self.external = external
self.message = message
self.allowInlineWebpageResolution = allowInlineWebpageResolution
self.progress = progress
}
}
public let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool
public let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void
public let openPeerMention: (String) -> Void
@ -113,7 +131,7 @@ public final class ChatControllerInteraction {
public let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void
public let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void
public let activateSwitchInline: (PeerId?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void
public let openUrl: (String, Bool, Bool?, Message?, Promise<Bool>?) -> Void
public let openUrl: (OpenUrl) -> Void
public let shareCurrentLocation: () -> Void
public let shareAccountContact: () -> Void
public let sendBotCommand: (MessageId?, String) -> Void
@ -227,7 +245,7 @@ public final class ChatControllerInteraction {
requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool, Bool) -> Void,
requestMessageActionUrlAuth: @escaping (String, MessageActionUrlSubject) -> Void,
activateSwitchInline: @escaping (PeerId?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void,
openUrl: @escaping (String, Bool, Bool?, Message?, Promise<Bool>?) -> Void,
openUrl: @escaping (OpenUrl) -> Void,
shareCurrentLocation: @escaping () -> Void,
shareAccountContact: @escaping () -> Void,
sendBotCommand: @escaping (MessageId?, String) -> Void,

View File

@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "CompositeTextNode",
module_name = "CompositeTextNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,117 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
public class CompositeTextNode: ASDisplayNode {
public enum Component: Equatable {
case text(NSAttributedString)
case icon(UIImage)
}
public var components: [Component] = []
private var textNodes: [Int: ImmediateTextNode] = [:]
private var iconViews: [Int: UIImageView] = [:]
public var imageTintColor: UIColor? {
didSet {
for (_, textNode) in self.textNodes {
textNode.layer.layerTintColor = self.imageTintColor?.cgColor
}
for (_, iconView) in self.iconViews {
iconView.tintColor = self.imageTintColor
}
}
}
public func update(constrainedSize: CGSize) -> CGSize {
var validTextIds: [Int] = []
var validIconIds: [Int] = []
var size = CGSize()
var nextTextId = 0
var nextIconId = 0
for component in self.components {
switch component {
case let .text(text):
let id = nextTextId
nextTextId += 1
validTextIds.append(id)
let textNode: ImmediateTextNode
if let current = self.textNodes[id] {
textNode = current
} else {
textNode = ImmediateTextNode()
textNode.maximumNumberOfLines = 1
textNode.insets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
textNode.layer.layerTintColor = self.imageTintColor?.cgColor
self.textNodes[id] = textNode
self.addSubnode(textNode)
}
textNode.attributedText = text
let textSize = textNode.updateLayout(CGSize(width: max(1.0, constrainedSize.width - size.width), height: constrainedSize.height))
textNode.frame = CGRect(origin: CGPoint(x: size.width - textNode.insets.left, y: -textNode.insets.top), size: textSize)
size.width += textSize.width - textNode.insets.left - textNode.insets.right
size.height = max(size.height, textSize.height - textNode.insets.top - textNode.insets.bottom)
case let .icon(icon):
let id = nextIconId
nextIconId += 1
validIconIds.append(id)
let iconView: UIImageView
if let current = self.iconViews[id] {
iconView = current
} else {
iconView = UIImageView()
self.iconViews[id] = iconView
self.view.addSubview(iconView)
}
iconView.image = icon
iconView.tintColor = self.imageTintColor
let iconSize = icon.size
if size.width != 0.0 {
size.width += 3.0
}
iconView.frame = CGRect(origin: CGPoint(x: size.width, y: 3.0 + UIScreenPixel), size: iconSize)
size.width += iconSize.width
size.width += 3.0
size.height = max(size.height, iconSize.height)
}
if size.width >= constrainedSize.width {
size.width = constrainedSize.width
break
}
}
var removeTextIds: [Int] = []
for (id, textNode) in self.textNodes {
if !validTextIds.contains(id) {
textNode.removeFromSupernode()
removeTextIds.append(id)
}
}
for id in removeTextIds {
self.textNodes.removeValue(forKey: id)
}
var removeIconIds: [Int] = []
for (id, iconView) in self.iconViews {
if !validIconIds.contains(id) {
iconView.removeFromSuperview()
removeIconIds.append(id)
}
}
for id in removeIconIds {
self.iconViews.removeValue(forKey: id)
}
return size
}
}

View File

@ -1392,8 +1392,8 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode,
toggleMessagesSelection: { messageId, selected in
chatControllerInteraction.toggleMessagesSelection(messageId, selected)
},
openUrl: { url, param1, param2, message in
chatControllerInteraction.openUrl(url, param1, param2, message, nil)
openUrl: { url, concealed, external, message in
chatControllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: concealed, external: external, message: message))
},
openInstantPage: { message, data in
chatControllerInteraction.openInstantPage(message, data)

View File

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

View File

@ -0,0 +1,206 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 6.026367 4.379944 cm
0.000000 0.000000 0.000000 scn
10.631250 13.370056 m
10.631250 12.540056 l
11.089646 12.540056 11.461250 12.911659 11.461250 13.370056 c
10.631250 13.370056 l
h
10.631250 5.870056 m
11.461250 5.870056 l
11.461250 6.090186 11.373804 6.301300 11.218149 6.456955 c
11.062494 6.612610 10.851380 6.700056 10.631250 6.700056 c
10.631250 5.870056 l
h
1.188047 1.981661 m
0.561160 2.525639 l
1.188047 1.981661 l
h
12.315193 1.823753 m
12.882865 1.218237 l
12.315193 1.823753 l
h
19.853075 10.349594 m
19.285404 9.744079 l
19.853075 10.349594 l
h
19.853075 8.890519 m
19.285404 9.496035 l
19.853075 8.890519 l
h
12.315191 17.416361 m
12.882863 18.021877 l
12.315191 17.416361 l
h
11.461250 13.370056 m
11.461250 16.686825 l
9.801250 16.686825 l
9.801250 13.370056 l
11.461250 13.370056 l
h
11.747520 16.810844 m
19.285404 9.744079 l
20.420746 10.955110 l
12.882863 18.021877 l
11.747520 16.810844 l
h
19.285404 9.496035 m
11.747522 2.429268 l
12.882865 1.218237 l
20.420748 8.285003 l
19.285404 9.496035 l
h
11.461250 2.553288 m
11.461250 5.870056 l
9.801250 5.870056 l
9.801250 2.553288 l
11.461250 2.553288 l
h
10.631250 6.700056 m
5.229464 6.700056 2.125496 4.328404 0.561160 2.525639 c
1.814935 1.437683 l
3.118428 2.939848 5.787818 5.040056 10.631250 5.040056 c
10.631250 6.700056 l
h
0.915461 2.267101 m
1.091532 4.065034 1.607588 6.660966 3.036170 8.786676 c
4.431379 10.862727 6.724575 12.540056 10.631250 12.540056 c
10.631250 14.200056 l
6.139981 14.200056 3.343834 12.220509 1.658399 9.712606 c
0.006337 7.254363 -0.549554 4.339265 -0.736636 2.428890 c
0.915461 2.267101 l
h
0.561160 2.525639 m
0.611023 2.583103 0.667563 2.604307 0.696526 2.610254 c
0.721157 2.615311 0.734035 2.612202 0.741838 2.609715 c
0.747947 2.607769 0.788049 2.594332 0.833411 2.538847 c
0.888431 2.471550 0.925537 2.369989 0.915461 2.267101 c
-0.736636 2.428890 l
-0.813821 1.640717 -0.224644 1.175383 0.238040 1.028011 c
0.696962 0.881838 1.366337 0.920712 1.814935 1.437683 c
0.561160 2.525639 l
h
11.747522 2.429268 m
11.712430 2.396370 11.683138 2.386383 11.659966 2.382921 c
11.632549 2.378824 11.598459 2.382227 11.563670 2.397299 c
11.528881 2.412371 11.503082 2.434914 11.487321 2.457716 c
11.473999 2.476988 11.461250 2.505186 11.461250 2.553288 c
9.801250 2.553288 l
9.801250 0.951229 11.714108 0.122528 12.882865 1.218237 c
11.747522 2.429268 l
h
19.285404 9.744079 m
19.357044 9.676915 19.357044 9.563196 19.285404 9.496035 c
20.420748 8.285003 l
21.191931 9.007989 21.191929 10.232126 20.420746 10.955110 c
19.285404 9.744079 l
h
11.461250 16.686825 m
11.461250 16.734926 11.473999 16.763124 11.487321 16.782398 c
11.503083 16.805201 11.528882 16.827742 11.563670 16.842813 c
11.598459 16.857885 11.632549 16.861290 11.659966 16.857193 c
11.683137 16.853729 11.712428 16.843744 11.747520 16.810844 c
12.882863 18.021877 l
11.714105 19.117588 9.801250 18.288879 9.801250 16.686825 c
11.461250 16.686825 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 1.334961 19.835052 cm
0.000000 0.000000 0.000000 scn
2.165001 7.999987 m
1.703843 7.999987 1.330001 7.626144 1.330001 7.164987 c
1.330001 6.703829 1.703843 6.329987 2.165001 6.329987 c
2.626159 6.329987 3.000001 6.703829 3.000001 7.164987 c
3.000001 7.626144 2.626159 7.999987 2.165001 7.999987 c
h
0.000000 7.164987 m
0.000000 8.360683 0.969304 9.329987 2.165001 9.329987 c
3.128699 9.329987 3.945334 8.700336 4.225954 7.829987 c
9.665001 7.829987 l
10.032270 7.829987 10.330001 7.532256 10.330001 7.164987 c
10.330001 6.797717 10.032270 6.499987 9.665001 6.499987 c
4.225954 6.499987 l
3.945334 5.629638 3.128699 4.999987 2.165001 4.999987 c
0.969304 4.999987 0.000000 5.969290 0.000000 7.164987 c
h
1.165001 2.829987 m
0.797732 2.829987 0.500001 2.532256 0.500001 2.164987 c
0.500001 1.797717 0.797732 1.499987 1.165001 1.499987 c
6.104048 1.499987 l
6.384667 0.629638 7.201303 -0.000013 8.165001 -0.000013 c
9.360698 -0.000013 10.330001 0.969290 10.330001 2.164987 c
10.330001 3.360683 9.360698 4.329987 8.165001 4.329987 c
7.201303 4.329987 6.384668 3.700336 6.104048 2.829987 c
1.165001 2.829987 l
h
7.330001 2.164987 m
7.330001 2.626144 7.703843 2.999987 8.165001 2.999987 c
8.626159 2.999987 9.000001 2.626144 9.000001 2.164987 c
9.000001 1.703829 8.626159 1.329987 8.165001 1.329987 c
7.703843 1.329987 7.330001 1.703829 7.330001 2.164987 c
h
f*
n
Q
endstream
endobj
3 0 obj
4389
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004479 00000 n
0000004502 00000 n
0000004675 00000 n
0000004749 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4808
%%EOF

View File

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

View File

@ -0,0 +1,139 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.334961 19.835052 cm
0.000000 0.000000 0.000000 scn
2.165001 7.999987 m
1.703843 7.999987 1.330001 7.626144 1.330001 7.164987 c
1.330001 6.703829 1.703843 6.329987 2.165001 6.329987 c
2.626159 6.329987 3.000001 6.703829 3.000001 7.164987 c
3.000001 7.626144 2.626159 7.999987 2.165001 7.999987 c
h
0.000000 7.164987 m
0.000000 8.360683 0.969304 9.329987 2.165001 9.329987 c
3.128699 9.329987 3.945334 8.700336 4.225954 7.829987 c
9.665001 7.829987 l
10.032270 7.829987 10.330001 7.532256 10.330001 7.164987 c
10.330001 6.797717 10.032270 6.499987 9.665001 6.499987 c
4.225954 6.499987 l
3.945334 5.629638 3.128699 4.999987 2.165001 4.999987 c
0.969304 4.999987 0.000000 5.969290 0.000000 7.164987 c
h
1.165001 2.829987 m
0.797732 2.829987 0.500001 2.532256 0.500001 2.164987 c
0.500001 1.797717 0.797732 1.499987 1.165001 1.499987 c
6.104048 1.499987 l
6.384667 0.629638 7.201303 -0.000013 8.165001 -0.000013 c
9.360698 -0.000013 10.330001 0.969290 10.330001 2.164987 c
10.330001 3.360683 9.360698 4.329987 8.165001 4.329987 c
7.201303 4.329987 6.384668 3.700336 6.104048 2.829987 c
1.165001 2.829987 l
h
7.330001 2.164987 m
7.330001 2.626144 7.703843 2.999987 8.165001 2.999987 c
8.626159 2.999987 9.000001 2.626144 9.000001 2.164987 c
9.000001 1.703829 8.626159 1.329987 8.165001 1.329987 c
7.703843 1.329987 7.330001 1.703829 7.330001 2.164987 c
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 6.120117 1.915771 cm
0.000000 0.000000 0.000000 scn
12.468664 19.994034 m
14.215597 21.740967 17.047935 21.740967 18.794867 19.994034 c
20.541800 18.247101 20.541800 15.414764 18.794867 13.667831 c
16.294867 11.167831 l
14.547935 9.420898 11.715597 9.420898 9.968664 11.167831 c
9.736349 11.400146 9.535478 11.650909 9.365597 11.915075 c
9.117653 12.300628 8.604102 12.412183 8.218549 12.164239 c
7.832996 11.916295 7.721442 11.402744 7.969386 11.017192 c
8.202941 10.654012 8.478252 10.310648 8.794867 9.994034 c
11.190069 7.598831 15.073462 7.598831 17.468664 9.994034 c
19.968664 12.494034 l
22.363867 14.889237 22.363867 18.772629 19.968664 21.167831 c
17.573462 23.563034 13.690069 23.563034 11.294867 21.167831 c
8.794867 18.667831 l
8.470732 18.343697 8.470732 17.818169 8.794867 17.494034 c
9.119002 17.169899 9.644529 17.169899 9.968664 17.494034 c
12.468664 19.994034 l
h
9.296402 4.167830 m
7.549469 2.420897 4.717132 2.420897 2.970200 4.167830 c
1.223267 5.914762 1.223267 8.747099 2.970200 10.494032 c
5.470199 12.994032 l
7.217132 14.740965 10.049469 14.740965 11.796402 12.994032 c
12.028717 12.761717 12.229589 12.510954 12.399469 12.246788 c
12.647413 11.861236 13.160964 11.749681 13.546517 11.997624 c
13.932070 12.245568 14.043624 12.759119 13.795681 13.144671 c
13.562125 13.507852 13.286814 13.851215 12.970200 14.167830 c
10.574997 16.563032 6.691605 16.563032 4.296402 14.167830 c
1.796402 11.667830 l
-0.598801 9.272626 -0.598801 5.389235 1.796402 2.994032 c
4.191605 0.598829 8.074997 0.598829 10.470200 2.994032 c
12.970200 5.494032 l
13.294334 5.818167 13.294334 6.343695 12.970200 6.667830 c
12.646064 6.991964 12.120537 6.991964 11.796402 6.667830 c
9.296402 4.167830 l
h
f*
n
Q
endstream
endobj
3 0 obj
3174
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003264 00000 n
0000003287 00000 n
0000003460 00000 n
0000003534 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
3593
%%EOF

View File

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

View File

@ -0,0 +1,102 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.000000 0.572754 cm
0.000000 0.000000 0.000000 scn
2.171946 3.261456 m
3.602304 3.261456 l
3.736763 3.261456 3.803992 3.261456 3.869396 3.256588 c
4.171662 3.234089 4.463039 3.134116 4.715446 2.966301 c
4.770054 2.929994 4.823115 2.888726 4.929220 2.806200 c
4.929265 2.806165 l
6.192761 1.823446 l
7.167630 1.065215 7.655064 0.686099 8.064308 0.689581 c
8.420319 0.692611 8.755808 0.856692 8.976768 1.135851 c
9.230768 1.456751 9.230768 2.074262 9.230768 3.309284 c
9.230768 7.557522 l
9.230768 8.792542 9.230768 9.410053 8.976768 9.730952 c
8.755808 10.010111 8.420319 10.174193 8.064308 10.177222 c
7.655064 10.180704 7.167630 9.801589 6.192762 9.043358 c
4.929265 8.060638 l
4.929246 8.060623 l
4.929197 8.060584 l
4.823106 7.978071 4.770050 7.936806 4.715446 7.900502 c
4.463039 7.732687 4.171662 7.632714 3.869396 7.610216 c
3.803992 7.605347 3.736763 7.605347 3.602304 7.605347 c
2.171948 7.605347 l
0.972415 7.605347 0.000000 6.632934 0.000000 5.433402 c
0.000000 4.233869 0.972414 3.261456 2.171946 3.261456 c
h
2.206053 2.149403 m
2.171725 2.053373 2.171725 1.933668 2.171725 1.694258 c
2.171725 1.089490 l
2.171725 0.489724 2.657931 0.003517 3.257698 0.003517 c
3.857464 0.003517 4.343671 0.489724 4.343671 1.089491 c
4.343671 1.694259 l
4.343671 1.933668 4.343671 2.053373 4.309342 2.149403 c
4.250016 2.315361 4.119403 2.445974 3.953444 2.505301 c
3.857416 2.539629 3.737711 2.539629 3.498301 2.539629 c
3.017095 2.539629 l
2.777685 2.539629 2.657980 2.539629 2.561951 2.505301 c
2.395993 2.445974 2.265379 2.315361 2.206053 2.149403 c
h
f*
n
Q
endstream
endobj
3 0 obj
1626
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 12.000000 11.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001716 00000 n
0000001739 00000 n
0000001912 00000 n
0000001986 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2045
%%EOF

View File

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

View File

@ -0,0 +1,162 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 12.000000 11.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.549805 cm
0.000000 0.000000 0.000000 scn
4.210876 5.670196 m
5.254694 5.670196 6.100876 6.516377 6.100876 7.560195 c
6.100876 8.604013 5.254694 9.450195 4.210876 9.450195 c
3.167058 9.450195 2.320876 8.604013 2.320876 7.560195 c
2.320876 6.516377 3.167058 5.670196 4.210876 5.670196 c
h
8.530897 5.670156 m
9.425598 5.670156 10.150897 6.395455 10.150897 7.290156 c
10.150897 8.184857 9.425598 8.910156 8.530897 8.910156 c
7.636196 8.910156 6.910897 8.184857 6.910897 7.290156 c
6.910897 6.395455 7.636196 5.670156 8.530897 5.670156 c
h
4.211036 4.319980 m
6.565200 4.319980 7.711937 3.183578 8.270525 2.076473 c
8.807893 1.011423 7.833971 -0.000021 6.641036 -0.000021 c
1.781036 -0.000021 l
0.588101 -0.000021 -0.385821 1.011424 0.151547 2.076473 c
0.710135 3.183578 1.856872 4.319980 4.211036 4.319980 c
h
7.304841 4.215691 m
8.083344 3.710282 8.587383 3.042050 8.911377 2.399903 c
9.138542 1.949668 9.186308 1.495334 9.099791 1.080082 c
10.421161 1.080082 l
11.315862 1.080082 12.045328 1.837108 11.649654 2.639562 c
11.227840 3.495031 10.351560 4.383612 8.531160 4.383612 c
8.059293 4.383612 7.650861 4.323907 7.297337 4.220552 c
7.304841 4.215691 l
h
f*
n
Q
endstream
endobj
2 0 obj
1240
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 12.000000 11.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 11.000000 m
12.000000 11.000000 l
12.000000 0.000000 l
0.000000 0.000000 l
0.000000 11.000000 l
h
f
n
Q
endstream
endobj
4 0 obj
232
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
endstream
endobj
7 0 obj
46
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 12.000000 11.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000001498 00000 n
0000001521 00000 n
0000002001 00000 n
0000002023 00000 n
0000002321 00000 n
0000002423 00000 n
0000002444 00000 n
0000002617 00000 n
0000002691 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
2751
%%EOF

View File

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

View File

@ -0,0 +1,198 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
-1.000000 -0.000000 -0.000000 1.000000 23.973633 4.379944 cm
0.000000 0.000000 0.000000 scn
10.631250 13.370056 m
10.631250 12.540056 l
11.089646 12.540056 11.461250 12.911659 11.461250 13.370056 c
10.631250 13.370056 l
h
10.631250 5.870056 m
11.461250 5.870056 l
11.461250 6.090186 11.373804 6.301300 11.218149 6.456955 c
11.062494 6.612610 10.851380 6.700056 10.631250 6.700056 c
10.631250 5.870056 l
h
1.188047 1.981661 m
0.561160 2.525639 l
1.188047 1.981661 l
h
12.315193 1.823753 m
12.882865 1.218237 l
12.315193 1.823753 l
h
19.853075 10.349594 m
19.285404 9.744079 l
19.853075 10.349594 l
h
19.853075 8.890519 m
19.285404 9.496035 l
19.853075 8.890519 l
h
12.315191 17.416361 m
12.882863 18.021877 l
12.315191 17.416361 l
h
11.461250 13.370056 m
11.461250 16.686825 l
9.801250 16.686825 l
9.801250 13.370056 l
11.461250 13.370056 l
h
11.747520 16.810844 m
19.285404 9.744079 l
20.420746 10.955110 l
12.882863 18.021877 l
11.747520 16.810844 l
h
19.285404 9.496035 m
11.747522 2.429268 l
12.882865 1.218237 l
20.420748 8.285003 l
19.285404 9.496035 l
h
11.461250 2.553288 m
11.461250 5.870056 l
9.801250 5.870056 l
9.801250 2.553288 l
11.461250 2.553288 l
h
10.631250 6.700056 m
5.229464 6.700056 2.125496 4.328404 0.561160 2.525639 c
1.814935 1.437683 l
3.118428 2.939848 5.787818 5.040056 10.631250 5.040056 c
10.631250 6.700056 l
h
0.915461 2.267101 m
1.091532 4.065034 1.607588 6.660966 3.036170 8.786676 c
4.431379 10.862727 6.724575 12.540056 10.631250 12.540056 c
10.631250 14.200056 l
6.139981 14.200056 3.343834 12.220509 1.658399 9.712606 c
0.006337 7.254363 -0.549554 4.339265 -0.736636 2.428890 c
0.915461 2.267101 l
h
0.561160 2.525639 m
0.611023 2.583103 0.667563 2.604307 0.696526 2.610254 c
0.721157 2.615311 0.734035 2.612202 0.741838 2.609715 c
0.747947 2.607769 0.788049 2.594332 0.833411 2.538847 c
0.888431 2.471550 0.925537 2.369989 0.915461 2.267101 c
-0.736636 2.428890 l
-0.813821 1.640717 -0.224644 1.175383 0.238040 1.028011 c
0.696962 0.881838 1.366337 0.920712 1.814935 1.437683 c
0.561160 2.525639 l
h
11.747522 2.429268 m
11.712430 2.396370 11.683138 2.386383 11.659966 2.382921 c
11.632549 2.378824 11.598459 2.382227 11.563670 2.397299 c
11.528881 2.412371 11.503082 2.434914 11.487321 2.457716 c
11.473999 2.476988 11.461250 2.505186 11.461250 2.553288 c
9.801250 2.553288 l
9.801250 0.951229 11.714108 0.122528 12.882865 1.218237 c
11.747522 2.429268 l
h
19.285404 9.744079 m
19.357044 9.676915 19.357044 9.563196 19.285404 9.496035 c
20.420748 8.285003 l
21.191931 9.007989 21.191929 10.232126 20.420746 10.955110 c
19.285404 9.744079 l
h
11.461250 16.686825 m
11.461250 16.734926 11.473999 16.763124 11.487321 16.782398 c
11.503083 16.805201 11.528882 16.827742 11.563670 16.842813 c
11.598459 16.857885 11.632549 16.861290 11.659966 16.857193 c
11.683137 16.853729 11.712428 16.843744 11.747520 16.810844 c
12.882863 18.021877 l
11.714105 19.117588 9.801250 18.288879 9.801250 16.686825 c
11.461250 16.686825 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 19.000000 20.858582 cm
0.000000 0.000000 0.000000 scn
0.000000 5.141418 m
0.000000 6.245988 0.895431 7.141418 2.000000 7.141418 c
3.104569 7.141418 4.000000 6.245988 4.000000 5.141418 c
4.000000 4.557826 l
4.000000 3.471111 3.746984 2.399319 3.260991 1.427331 c
2.894427 0.694205 l
2.647438 0.200226 2.046765 0.000002 1.552786 0.246991 c
1.058808 0.493980 0.858584 1.094654 1.105573 1.588632 c
1.472136 2.321758 l
1.605720 2.588927 1.714662 2.866652 1.798144 3.151479 c
0.788369 3.252682 0.000000 4.104984 0.000000 5.141418 c
h
5.000000 5.141418 m
5.000000 6.245988 5.895431 7.141418 7.000000 7.141418 c
8.104569 7.141418 9.000000 6.245988 9.000000 5.141418 c
9.000000 4.557826 l
9.000000 3.471111 8.746984 2.399319 8.260990 1.427331 c
7.894427 0.694205 l
7.647438 0.200226 7.046765 0.000002 6.552786 0.246991 c
6.058808 0.493980 5.858583 1.094654 6.105573 1.588632 c
6.472136 2.321758 l
6.605721 2.588927 6.714662 2.866652 6.798144 3.151479 c
5.788369 3.252682 5.000000 4.104984 5.000000 5.141418 c
h
f*
n
Q
endstream
endobj
3 0 obj
4042
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004132 00000 n
0000004155 00000 n
0000004328 00000 n
0000004402 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4461
%%EOF

View File

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

View File

@ -0,0 +1,206 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
-1.000000 -0.000000 -0.000000 1.000000 23.973633 4.379944 cm
0.000000 0.000000 0.000000 scn
10.631250 13.370056 m
10.631250 12.540056 l
11.089646 12.540056 11.461250 12.911659 11.461250 13.370056 c
10.631250 13.370056 l
h
10.631250 5.870056 m
11.461250 5.870056 l
11.461250 6.090186 11.373804 6.301300 11.218149 6.456955 c
11.062494 6.612610 10.851380 6.700056 10.631250 6.700056 c
10.631250 5.870056 l
h
1.188047 1.981661 m
0.561160 2.525639 l
1.188047 1.981661 l
h
12.315193 1.823753 m
12.882865 1.218237 l
12.315193 1.823753 l
h
19.853075 10.349594 m
19.285404 9.744079 l
19.853075 10.349594 l
h
19.853075 8.890519 m
19.285404 9.496035 l
19.853075 8.890519 l
h
12.315191 17.416361 m
12.882863 18.021877 l
12.315191 17.416361 l
h
11.461250 13.370056 m
11.461250 16.686825 l
9.801250 16.686825 l
9.801250 13.370056 l
11.461250 13.370056 l
h
11.747520 16.810844 m
19.285404 9.744079 l
20.420746 10.955110 l
12.882863 18.021877 l
11.747520 16.810844 l
h
19.285404 9.496035 m
11.747522 2.429268 l
12.882865 1.218237 l
20.420748 8.285003 l
19.285404 9.496035 l
h
11.461250 2.553288 m
11.461250 5.870056 l
9.801250 5.870056 l
9.801250 2.553288 l
11.461250 2.553288 l
h
10.631250 6.700056 m
5.229464 6.700056 2.125496 4.328404 0.561160 2.525639 c
1.814935 1.437683 l
3.118428 2.939848 5.787818 5.040056 10.631250 5.040056 c
10.631250 6.700056 l
h
0.915461 2.267101 m
1.091532 4.065034 1.607588 6.660966 3.036170 8.786676 c
4.431379 10.862727 6.724575 12.540056 10.631250 12.540056 c
10.631250 14.200056 l
6.139981 14.200056 3.343834 12.220509 1.658399 9.712606 c
0.006337 7.254363 -0.549554 4.339265 -0.736636 2.428890 c
0.915461 2.267101 l
h
0.561160 2.525639 m
0.611023 2.583103 0.667563 2.604307 0.696526 2.610254 c
0.721157 2.615311 0.734035 2.612202 0.741838 2.609715 c
0.747947 2.607769 0.788049 2.594332 0.833411 2.538847 c
0.888431 2.471550 0.925537 2.369989 0.915461 2.267101 c
-0.736636 2.428890 l
-0.813821 1.640717 -0.224644 1.175383 0.238040 1.028011 c
0.696962 0.881838 1.366337 0.920712 1.814935 1.437683 c
0.561160 2.525639 l
h
11.747522 2.429268 m
11.712430 2.396370 11.683138 2.386383 11.659966 2.382921 c
11.632549 2.378824 11.598459 2.382227 11.563670 2.397299 c
11.528881 2.412371 11.503082 2.434914 11.487321 2.457716 c
11.473999 2.476988 11.461250 2.505186 11.461250 2.553288 c
9.801250 2.553288 l
9.801250 0.951229 11.714108 0.122528 12.882865 1.218237 c
11.747522 2.429268 l
h
19.285404 9.744079 m
19.357044 9.676915 19.357044 9.563196 19.285404 9.496035 c
20.420748 8.285003 l
21.191931 9.007989 21.191929 10.232126 20.420746 10.955110 c
19.285404 9.744079 l
h
11.461250 16.686825 m
11.461250 16.734926 11.473999 16.763124 11.487321 16.782398 c
11.503083 16.805201 11.528882 16.827742 11.563670 16.842813 c
11.598459 16.857885 11.632549 16.861290 11.659966 16.857193 c
11.683137 16.853729 11.712428 16.843744 11.747520 16.810844 c
12.882863 18.021877 l
11.714105 19.117588 9.801250 18.288879 9.801250 16.686825 c
11.461250 16.686825 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 18.334961 19.840027 cm
0.000000 0.000000 0.000000 scn
2.165001 7.999987 m
1.703843 7.999987 1.330001 7.626144 1.330001 7.164987 c
1.330001 6.703829 1.703843 6.329987 2.165001 6.329987 c
2.626159 6.329987 3.000001 6.703829 3.000001 7.164987 c
3.000001 7.626144 2.626159 7.999987 2.165001 7.999987 c
h
2.165001 9.329987 m
3.128699 9.329987 3.945334 8.700336 4.225954 7.829987 c
9.165001 7.829987 l
9.532270 7.829987 9.830001 7.532256 9.830001 7.164987 c
9.830001 6.797717 9.532270 6.499987 9.165001 6.499987 c
4.225954 6.499987 l
3.945334 5.629638 3.128699 4.999987 2.165001 4.999987 c
0.969304 4.999987 0.000000 5.969290 0.000000 7.164987 c
0.000000 8.360683 0.969304 9.329987 2.165001 9.329987 c
h
1.165001 2.829987 m
0.797732 2.829987 0.500001 2.532256 0.500001 2.164987 c
0.500001 1.797717 0.797732 1.499987 1.165001 1.499987 c
6.104048 1.499987 l
6.384667 0.629638 7.201303 -0.000013 8.165001 -0.000013 c
9.360698 -0.000013 10.330001 0.969290 10.330001 2.164987 c
10.330001 3.360683 9.360698 4.329987 8.165001 4.329987 c
7.201303 4.329987 6.384668 3.700336 6.104048 2.829987 c
1.165001 2.829987 l
h
7.330001 2.164987 m
7.330001 1.703829 7.703843 1.329987 8.165001 1.329987 c
8.626159 1.329987 9.000001 1.703829 9.000001 2.164987 c
9.000001 2.626144 8.626159 2.999987 8.165001 2.999987 c
7.703843 2.999987 7.330001 2.626144 7.330001 2.164987 c
h
f*
n
Q
endstream
endobj
3 0 obj
4388
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004478 00000 n
0000004501 00000 n
0000004674 00000 n
0000004748 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4807
%%EOF

View File

@ -2743,8 +2743,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.openPeer(peer: nil, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), subject: nil, peekData: nil), fromMessage: nil, peerTypes: peerTypes)
}
}
}, openUrl: { [weak self] url, concealed, _, message, progress in
}, openUrl: { [weak self] urlData in
if let strongSelf = self {
let url = urlData.url
let concealed = urlData.concealed
let message = urlData.message
let progress = urlData.progress
var skipConcealedAlert = false
if let author = message?.author, author.isVerified {
skipConcealedAlert = true
@ -2758,7 +2763,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
performOpenURL(message, url, progress)
} else {
progress?.set(.single(false))
strongSelf.openUrl(url, concealed: concealed, skipConcealedAlert: skipConcealedAlert, message: message)
strongSelf.openUrl(url, concealed: concealed, skipConcealedAlert: skipConcealedAlert, message: message, allowInlineWebpageResolution: urlData.allowInlineWebpageResolution)
}
}
}, shareCurrentLocation: { [weak self] in
@ -3365,7 +3370,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
items.append(ActionSheetButtonItem(title: url.title, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.controllerInteraction?.openUrl(url.url, false, false, message, nil)
strongSelf.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: false, external: false, message: message))
}
}))
}
@ -3444,7 +3449,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}, openSearch: {
}, setupReply: { [weak self] messageId in
self?.interfaceInteraction?.setupReplyMessage(messageId, { _ in })
self?.interfaceInteraction?.setupReplyMessage(messageId, { _, _ in })
}, canSetupReply: { [weak self] message in
if !message.flags.contains(.Incoming) {
if !message.flags.intersection([.Failed, .Sending, .Unsent]).isEmpty {
@ -4609,7 +4614,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case let .join(_, joinHash):
self.controllerInteraction?.openJoinLink(joinHash)
case let .webPage(_, url):
self.controllerInteraction?.openUrl(url, false, false, nil, nil)
self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: false, external: false))
}
}, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId in
guard let self else {
@ -8645,20 +8650,33 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
messageId: message.id,
quote: nil
))
}).updatedSearch(nil).updatedShowCommands(false) }, completion: completion)
}).updatedSearch(nil).updatedShowCommands(false) }, completion: { t in
completion(t, {})
})
strongSelf.updateItemNodesSearchTextHighlightStates()
strongSelf.chatDisplayNode.ensureInputViewFocused()
} else {
completion(.immediate)
completion(.immediate, {})
}
}, alertAction: {
completion(.immediate)
completion(.immediate, {})
}, delay: true)
} else {
completion(.immediate)
let replySubject = ChatInterfaceState.ReplyMessageSubject(
messageId: messageId,
quote: nil
)
completion(.immediate, {
guard let self else {
return
}
moveReplyMessageToAnotherChat(selfController: self, replySubject: replySubject)
})
}
} else {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil) }) }, completion: completion)
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil) }) }, completion: { t in
completion(t, {})
})
}
}, setupEditMessage: { [weak self] messageId, completion in
if let strongSelf = self, strongSelf.isNodeLoaded {
@ -17970,10 +17988,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}, contentContext: nil)
}
func openUrl(_ url: String, concealed: Bool, forceExternal: Bool = false, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, message: Message? = nil, commit: @escaping () -> Void = {}) {
func openUrl(_ url: String, concealed: Bool, forceExternal: Bool = false, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, message: Message? = nil, allowInlineWebpageResolution: Bool = false, commit: @escaping () -> Void = {}) {
self.commitPurposefulAction()
if let message, let webpage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.url == url {
if allowInlineWebpageResolution, let message, let webpage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.url == url {
if content.instantPage != nil {
if let navigationController = self.navigationController as? NavigationController {
switch instantPageType(of: content) {

View File

@ -206,7 +206,7 @@ extension ListMessageItemInteraction {
}, toggleMessagesSelection: { messageId, selected in
controllerInteraction.toggleMessagesSelection(messageId, selected)
}, openUrl: { url, param1, param2, message in
controllerInteraction.openUrl(url, param1, param2, message, nil)
controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: param1, external: param2, message: message))
}, openInstantPage: { message, data in
controllerInteraction.openInstantPage(message, data)
}, longTap: { action, message in

View File

@ -294,6 +294,9 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS
if case .member = channel.participationStatus {
canReply = channel.hasPermission(.sendSomething)
}
if case .broadcast = channel.info {
canReply = true
}
} else if let group = peer as? TelegramGroup {
if case .Member = group.membership {
canReply = true
@ -958,9 +961,11 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
if !isPinnedMessages, !isReplyThreadHead, data.canReply {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuReply, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
interfaceInteraction.setupReplyMessage(messages[0].id, { transition in
f(.custom(transition))
}, action: { c, _ in
interfaceInteraction.setupReplyMessage(messages[0].id, { transition, completed in
c.dismiss(result: .custom(transition), completion: {
completed()
})
})
})))
}

View File

@ -22,6 +22,7 @@ import TextNodeWithEntities
import AnimationCache
import MultiAnimationRenderer
import TranslateUI
import ChatControllerInteraction
private enum PinnedMessageAnimation {
case slideToTop
@ -858,7 +859,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
if url.hasPrefix("tg://") {
isConcealed = false
}
controllerInteraction.openUrl(url, isConcealed, nil, nil, nil)
controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: isConcealed))
case .requestMap:
controllerInteraction.shareCurrentLocation()
case .requestPhone:

View File

@ -98,7 +98,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, requestMessageActionCallback: { _, _, _, _ in
}, requestMessageActionUrlAuth: { _, _ in
}, activateSwitchInline: { _, _, _ in
}, openUrl: { _, _, _, _, _ in
}, openUrl: { _ in
}, shareCurrentLocation: {
}, shareAccountContact: {
}, sendBotCommand: { _, _ in

View File

@ -2779,11 +2779,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, requestMessageActionCallback: { _, _, _, _ in
}, requestMessageActionUrlAuth: { _, _ in
}, activateSwitchInline: { _, _, _ in
}, openUrl: { [weak self] url, concealed, external, _, _ in
}, openUrl: { [weak self] url in
guard let strongSelf = self else {
return
}
strongSelf.openUrl(url: url, concealed: concealed, external: external ?? false)
strongSelf.openUrl(url: url.url, concealed: url.concealed, external: url.external ?? false)
}, shareCurrentLocation: {
}, shareAccountContact: {
}, sendBotCommand: { _, _ in

View File

@ -1490,7 +1490,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
clickThroughMessage?()
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _, _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, presentController: { _, _ in
}, presentControllerInCurrent: { _, _ in
}, navigationController: {

View File

@ -9,6 +9,7 @@ import AccountContext
import TelegramStringFormatting
import ChatPresentationInterfaceState
import AccessoryPanelNode
import AppBundle
final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode {
private let webpageDisposable = MetaDisposable()
@ -18,7 +19,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode {
let closeButton: HighlightableButtonNode
let lineNode: ASImageNode
let iconNode: ASImageNode
let iconView: UIImageView
let titleNode: TextNode
private var titleString: NSAttributedString?
@ -46,10 +47,9 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode {
self.lineNode.displaysAsynchronously = false
self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme)
self.iconNode = ASImageNode()
self.iconNode.displayWithoutProcessing = false
self.iconNode.displaysAsynchronously = false
self.iconNode.image = PresentationResourcesChat.chatInputPanelWebpageIconImage(theme)
self.iconView = UIImageView()
self.iconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/LinkSettingsIcon")?.withRenderingMode(.alwaysTemplate)
self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor
self.titleNode = TextNode()
self.titleNode.displaysAsynchronously = false
@ -63,7 +63,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode {
self.addSubnode(self.closeButton)
self.addSubnode(self.lineNode)
self.addSubnode(self.iconNode)
self.view.addSubview(self.iconView)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
@ -75,11 +75,11 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode {
}
override func animateIn() {
self.iconNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
self.iconView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
}
override func animateOut() {
self.iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
self.iconView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
}
override public func didLoad() {
@ -101,7 +101,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode {
self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: [])
self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme)
self.iconNode.image = PresentationResourcesChat.chatInputPanelWebpageIconImage(theme)
self.iconView.tintColor = theme.chat.inputPanel.panelControlAccentColor
}
if let text = self.titleString?.string {
@ -194,8 +194,8 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode {
self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0))
if let icon = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size)
if let icon = self.iconView.image {
self.iconView.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size)
}
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)