mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 06:10:03 +00:00
Quotes experiment
This commit is contained in:
parent
68a640dc44
commit
bab2b39725
@ -321,6 +321,7 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
|||||||
case strikethrough
|
case strikethrough
|
||||||
case underline
|
case underline
|
||||||
case spoiler
|
case spoiler
|
||||||
|
case quote
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||||
@ -348,6 +349,8 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
|||||||
self = .underline
|
self = .underline
|
||||||
case 8:
|
case 8:
|
||||||
self = .spoiler
|
self = .spoiler
|
||||||
|
case 9:
|
||||||
|
self = .quote
|
||||||
default:
|
default:
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
self = .bold
|
self = .bold
|
||||||
@ -379,6 +382,8 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
|||||||
try container.encode(7 as Int32, forKey: "t")
|
try container.encode(7 as Int32, forKey: "t")
|
||||||
case .spoiler:
|
case .spoiler:
|
||||||
try container.encode(8 as Int32, forKey: "t")
|
try container.encode(8 as Int32, forKey: "t")
|
||||||
|
case .quote:
|
||||||
|
try container.encode(0 as Int32, forKey: "t")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -452,6 +457,9 @@ public struct ChatTextInputStateText: Codable, Equatable {
|
|||||||
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .underline, range: range.location ..< (range.location + range.length)))
|
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .underline, range: range.location ..< (range.location + range.length)))
|
||||||
} else if key == ChatTextInputAttributes.spoiler {
|
} else if key == ChatTextInputAttributes.spoiler {
|
||||||
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .spoiler, range: range.location ..< (range.location + range.length)))
|
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .spoiler, range: range.location ..< (range.location + range.length)))
|
||||||
|
} else if key == ChatTextInputAttributes.quote, let value = value as? ChatTextInputTextQuoteAttribute {
|
||||||
|
let _ = value
|
||||||
|
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .quote, range: range.location ..< (range.location + range.length)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -496,6 +504,8 @@ public struct ChatTextInputStateText: Codable, Equatable {
|
|||||||
result.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
result.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||||
case .spoiler:
|
case .spoiler:
|
||||||
result.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
result.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||||
|
case .quote:
|
||||||
|
result.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@ -16,6 +16,13 @@
|
|||||||
|
|
||||||
@implementation ASCustomTextContainer
|
@implementation ASCustomTextContainer
|
||||||
|
|
||||||
|
- (instancetype)initWithSize:(CGSize)size textStorage:(NSTextStorage *)textStorage {
|
||||||
|
self = [super initWithSize:size];
|
||||||
|
if (self != nil) {
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
- (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(nullable CGRect *)remainingRect {
|
- (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(nullable CGRect *)remainingRect {
|
||||||
CGRect result = [super lineFragmentRectForProposedRect:proposedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect];
|
CGRect result = [super lineFragmentRectForProposedRect:proposedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect];
|
||||||
|
|
||||||
@ -139,8 +146,7 @@
|
|||||||
components.layoutManager = layoutManager;
|
components.layoutManager = layoutManager;
|
||||||
[components.textStorage addLayoutManager:components.layoutManager];
|
[components.textStorage addLayoutManager:components.layoutManager];
|
||||||
|
|
||||||
components.textContainer = [[ASCustomTextContainer alloc] initWithSize:textContainerSize];
|
components.textContainer = [[ASCustomTextContainer alloc] initWithSize:textContainerSize textStorage:textStorage];
|
||||||
//components.textContainer.exclusionPaths = @[[UIBezierPath bezierPathWithRect:CGRectMake(textContainerSize.width - 60.0, 0.0, 60.0, 40.0)]];
|
|
||||||
components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view.
|
components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view.
|
||||||
[components.layoutManager addTextContainer:components.textContainer];
|
[components.layoutManager addTextContainer:components.textContainer];
|
||||||
|
|
||||||
|
|||||||
@ -52,6 +52,8 @@ AS_SUBCLASSING_RESTRICTED
|
|||||||
|
|
||||||
@interface ASCustomTextContainer : NSTextContainer
|
@interface ASCustomTextContainer : NSTextContainer
|
||||||
|
|
||||||
|
- (instancetype)initWithSize:(CGSize)size textStorage:(NSTextStorage *)textStorage;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -58,7 +58,7 @@
|
|||||||
[_textStorage setAttributedString:attributedString];
|
[_textStorage setAttributedString:attributedString];
|
||||||
}
|
}
|
||||||
|
|
||||||
_textContainer = [[ASCustomTextContainer alloc] initWithSize:constrainedSize];
|
_textContainer = [[ASCustomTextContainer alloc] initWithSize:constrainedSize textStorage:nil];
|
||||||
// We want the text laid out up to the very edges of the container.
|
// We want the text laid out up to the very edges of the container.
|
||||||
_textContainer.lineFragmentPadding = 0;
|
_textContainer.lineFragmentPadding = 0;
|
||||||
_textContainer.lineBreakMode = lineBreakMode;
|
_textContainer.lineBreakMode = lineBreakMode;
|
||||||
|
|||||||
@ -106,3 +106,29 @@ public func chatTextInputAddMentionAttribute(_ state: ChatTextInputState, peer:
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func chatTextInputAddQuoteAttribute(_ state: ChatTextInputState, selectionRange: Range<Int>) -> ChatTextInputState {
|
||||||
|
if selectionRange.isEmpty {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
let nsRange = NSRange(location: selectionRange.lowerBound, length: selectionRange.count)
|
||||||
|
var quoteRange = nsRange
|
||||||
|
var attributesToRemove: [(NSAttributedString.Key, NSRange)] = []
|
||||||
|
state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
|
||||||
|
for (key, _) in attributes {
|
||||||
|
if key == ChatTextInputAttributes.quote {
|
||||||
|
attributesToRemove.append((key, range))
|
||||||
|
quoteRange = quoteRange.union(range)
|
||||||
|
} else {
|
||||||
|
attributesToRemove.append((key, nsRange))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = NSMutableAttributedString(attributedString: state.inputText)
|
||||||
|
for (attribute, range) in attributesToRemove {
|
||||||
|
result.removeAttribute(attribute, range: range)
|
||||||
|
}
|
||||||
|
result.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: nsRange)
|
||||||
|
return ChatTextInputState(inputText: result, selectionRange: selectionRange)
|
||||||
|
}
|
||||||
|
|||||||
@ -61,8 +61,11 @@ public enum InteractiveTransitionGestureRecognizerEdgeWidth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
||||||
private let edgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth
|
private let staticEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth
|
||||||
private let allowedDirections: (CGPoint) -> InteractiveTransitionGestureRecognizerDirections
|
private let allowedDirections: (CGPoint) -> InteractiveTransitionGestureRecognizerDirections
|
||||||
|
public var dynamicEdgeWidth: ((CGPoint) -> InteractiveTransitionGestureRecognizerEdgeWidth)?
|
||||||
|
|
||||||
|
private var currentEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth
|
||||||
|
|
||||||
private var validatedGesture = false
|
private var validatedGesture = false
|
||||||
private var firstLocation: CGPoint = CGPoint()
|
private var firstLocation: CGPoint = CGPoint()
|
||||||
@ -70,7 +73,8 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
|
|
||||||
public init(target: Any?, action: Selector?, allowedDirections: @escaping (CGPoint) -> InteractiveTransitionGestureRecognizerDirections, edgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth = .constant(16.0)) {
|
public init(target: Any?, action: Selector?, allowedDirections: @escaping (CGPoint) -> InteractiveTransitionGestureRecognizerDirections, edgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth = .constant(16.0)) {
|
||||||
self.allowedDirections = allowedDirections
|
self.allowedDirections = allowedDirections
|
||||||
self.edgeWidth = edgeWidth
|
self.staticEdgeWidth = edgeWidth
|
||||||
|
self.currentEdgeWidth = edgeWidth
|
||||||
|
|
||||||
super.init(target: target, action: action)
|
super.init(target: target, action: action)
|
||||||
|
|
||||||
@ -99,6 +103,10 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let dynamicEdgeWidth = self.dynamicEdgeWidth {
|
||||||
|
self.currentEdgeWidth = dynamicEdgeWidth(point)
|
||||||
|
}
|
||||||
|
|
||||||
super.touchesBegan(touches, with: event)
|
super.touchesBegan(touches, with: event)
|
||||||
|
|
||||||
self.firstLocation = point
|
self.firstLocation = point
|
||||||
@ -151,7 +159,7 @@ public class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let edgeWidth: CGFloat
|
let edgeWidth: CGFloat
|
||||||
switch self.edgeWidth {
|
switch self.currentEdgeWidth {
|
||||||
case let .constant(value):
|
case let .constant(value):
|
||||||
edgeWidth = value
|
edgeWidth = value
|
||||||
case let .widthMultiplier(factor, minValue, maxValue):
|
case let .widthMultiplier(factor, minValue, maxValue):
|
||||||
|
|||||||
@ -142,6 +142,12 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
|
|||||||
}
|
}
|
||||||
return .right
|
return .right
|
||||||
})
|
})
|
||||||
|
panRecognizer.dynamicEdgeWidth = { [weak self] _ in
|
||||||
|
guard let self, let controller = self.controllers.last, let value = controller.interactiveNavivationGestureEdgeWidth else {
|
||||||
|
return .constant(16.0)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
if #available(iOS 13.4, *) {
|
if #available(iOS 13.4, *) {
|
||||||
panRecognizer.allowedScrollTypesMask = .continuous
|
panRecognizer.allowedScrollTypesMask = .continuous
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -227,6 +227,10 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var navigationBarOrigin: CGFloat = 0.0
|
private var navigationBarOrigin: CGFloat = 0.0
|
||||||
|
|
||||||
|
open var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
open func navigationLayout(layout: ContainerViewLayout) -> NavigationLayout {
|
open func navigationLayout(layout: ContainerViewLayout) -> NavigationLayout {
|
||||||
let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0
|
let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0
|
||||||
|
|||||||
@ -64,6 +64,9 @@ private func chatInputStateString(attributedString: NSAttributedString) -> NSAtt
|
|||||||
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||||
string.addAttribute(ChatTextInputAttributes.customEmoji, value: value, range: range)
|
string.addAttribute(ChatTextInputAttributes.customEmoji, value: value, range: range)
|
||||||
}
|
}
|
||||||
|
if let value = attributes[ChatTextInputAttributes.quote] as? ChatTextInputTextQuoteAttribute {
|
||||||
|
string.addAttribute(ChatTextInputAttributes.quote, value: value, range: range)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -155,6 +155,9 @@ public extension TelegramEngine {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*public func subscribe<each T: TelegramEngineDataItem>(_ ts: repeat each T) -> Signal<repeat each T, NoError> {
|
||||||
|
}*/
|
||||||
|
|
||||||
public func subscribe<T0: TelegramEngineDataItem>(_ t0: T0) -> Signal<T0.Result, NoError> {
|
public func subscribe<T0: TelegramEngineDataItem>(_ t0: T0) -> Signal<T0.Result, NoError> {
|
||||||
return self._subscribe(items: [t0 as! AnyPostboxViewDataItem])
|
return self._subscribe(items: [t0 as! AnyPostboxViewDataItem])
|
||||||
|
|||||||
@ -34,6 +34,8 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
|
||||||
"//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode",
|
"//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
||||||
|
"//submodules/TelegramUI/Components/Chat/MessageQuoteComponent",
|
||||||
|
"//submodules/TelegramUI/Components/RichTextView",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import ChatMessageDateAndStatusNode
|
|||||||
import ChatMessageBubbleContentNode
|
import ChatMessageBubbleContentNode
|
||||||
import ShimmeringLinkNode
|
import ShimmeringLinkNode
|
||||||
import ChatMessageItemCommon
|
import ChatMessageItemCommon
|
||||||
|
import RichTextView
|
||||||
|
|
||||||
private final class CachedChatMessageText {
|
private final class CachedChatMessageText {
|
||||||
let text: String
|
let text: String
|
||||||
@ -343,7 +344,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
let textFont = item.presentationData.messageFont
|
let textFont = item.presentationData.messageFont
|
||||||
|
|
||||||
if let entities = entities {
|
if let entities = entities {
|
||||||
attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, message: item.message)
|
attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseQuoteTintColor: messageTheme.accentControlColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, message: item.message)
|
||||||
} else if !rawText.isEmpty {
|
} else if !rawText.isEmpty {
|
||||||
attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.primaryTextColor)
|
attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.primaryTextColor)
|
||||||
} else {
|
} else {
|
||||||
@ -609,6 +610,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
return .bankCard(bankCard)
|
return .bankCard(bankCard)
|
||||||
} else if let pre = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre)] as? String {
|
} else if let pre = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre)] as? String {
|
||||||
return .copy(pre)
|
return .copy(pre)
|
||||||
|
} else if let code = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Code)] as? String {
|
||||||
|
return .copy(code)
|
||||||
} else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file {
|
} else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file {
|
||||||
return .customEmoji(file)
|
return .customEmoji(file)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "MessageQuoteComponent",
|
||||||
|
module_name = "MessageQuoteComponent",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/Display",
|
||||||
|
"//submodules/ComponentFlow",
|
||||||
|
"//submodules/TelegramPresentationData",
|
||||||
|
"//submodules/TelegramCore",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import TelegramPresentationData
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
|
private let lineImage: UIImage = {
|
||||||
|
let radius: CGFloat = 4.0
|
||||||
|
return generateImage(CGSize(width: radius, height: radius * 2.0), rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.setFillColor(UIColor.white.cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||||
|
})!.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(radius)).withRenderingMode(.alwaysTemplate)
|
||||||
|
}()
|
||||||
|
|
||||||
|
public final class MessageQuoteView: UIView {
|
||||||
|
public struct Params {
|
||||||
|
let presentationData: ChatPresentationData
|
||||||
|
let authorName: String?
|
||||||
|
let text: String
|
||||||
|
let entities: [MessageTextEntity]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
presentationData: ChatPresentationData,
|
||||||
|
authorName: String?,
|
||||||
|
text: String,
|
||||||
|
entities: [MessageTextEntity]
|
||||||
|
) {
|
||||||
|
self.presentationData = presentationData
|
||||||
|
self.authorName = authorName
|
||||||
|
self.text = text
|
||||||
|
self.entities = entities
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let lineView: UIImageView
|
||||||
|
|
||||||
|
override private init(frame: CGRect) {
|
||||||
|
self.lineView = UIImageView()
|
||||||
|
self.lineView.image = lineImage
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.lineView)
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func asyncLayout(_ view: MessageQuoteView?) -> (Params) -> (CGSize, (CGSize) -> MessageQuoteView) {
|
||||||
|
return { params in
|
||||||
|
var minSize = CGSize()
|
||||||
|
|
||||||
|
minSize.height = 100.0
|
||||||
|
|
||||||
|
return (minSize, { size in
|
||||||
|
let view = view ?? MessageQuoteView(frame: CGRect())
|
||||||
|
|
||||||
|
view.lineView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: lineImage.size.width, height: size.height))
|
||||||
|
|
||||||
|
return view
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
submodules/TelegramUI/Components/RichTextView/BUILD
Normal file
17
submodules/TelegramUI/Components/RichTextView/BUILD
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "RichTextView",
|
||||||
|
module_name = "RichTextView",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public final class RichTextView: UIView {
|
||||||
|
public final class Params: Equatable {
|
||||||
|
let string: NSAttributedString
|
||||||
|
let constrainedSize: CGSize
|
||||||
|
|
||||||
|
public init(
|
||||||
|
string: NSAttributedString,
|
||||||
|
constrainedSize: CGSize
|
||||||
|
) {
|
||||||
|
self.string = string
|
||||||
|
self.constrainedSize = constrainedSize
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: Params, rhs: Params) -> Bool {
|
||||||
|
if !lhs.string.isEqual(to: rhs.string) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.constrainedSize != rhs.constrainedSize {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class LayoutData: Equatable {
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: LayoutData, rhs: LayoutData) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class AsyncResult {
|
||||||
|
public let view: () -> RichTextView
|
||||||
|
public let layoutData: LayoutData
|
||||||
|
|
||||||
|
init(view: @escaping () -> RichTextView, layoutData: LayoutData) {
|
||||||
|
self.view = view
|
||||||
|
self.layoutData = layoutData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func performLayout(params: Params) -> LayoutData {
|
||||||
|
return LayoutData()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func updateAsync(_ view: RichTextView?) -> (Params) -> AsyncResult {
|
||||||
|
return { params in
|
||||||
|
let layoutData = performLayout(params: params)
|
||||||
|
|
||||||
|
return AsyncResult(
|
||||||
|
view: {
|
||||||
|
let view = view ?? RichTextView(frame: CGRect())
|
||||||
|
view.layoutData = layoutData
|
||||||
|
return view
|
||||||
|
},
|
||||||
|
layoutData: layoutData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var layoutData: LayoutData?
|
||||||
|
|
||||||
|
override public init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func draw(_ rect: CGRect) {
|
||||||
|
guard let layoutData = self.layoutData else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = layoutData
|
||||||
|
}
|
||||||
|
}
|
||||||
12
submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "quotemini.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
158
submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/quotemini.pdf
vendored
Normal file
158
submodules/TelegramUI/Images.xcassets/Chat/Message/ReplyQuoteIcon.imageset/quotemini.pdf
vendored
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
%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 9.000000 7.000244 ]
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.141113 cm
|
||||||
|
0.000000 0.000000 0.000000 scn
|
||||||
|
0.000000 5.141357 m
|
||||||
|
0.000000 6.245927 0.895431 7.141357 2.000000 7.141357 c
|
||||||
|
3.104569 7.141357 4.000000 6.245927 4.000000 5.141357 c
|
||||||
|
4.000000 4.557765 l
|
||||||
|
4.000000 3.471050 3.746984 2.399258 3.260991 1.427270 c
|
||||||
|
2.894427 0.694144 l
|
||||||
|
2.647438 0.200165 2.046765 -0.000059 1.552786 0.246930 c
|
||||||
|
1.058808 0.493919 0.858584 1.094593 1.105573 1.588571 c
|
||||||
|
1.472136 2.321697 l
|
||||||
|
1.605720 2.588866 1.714662 2.866591 1.798144 3.151417 c
|
||||||
|
0.788369 3.252621 0.000000 4.104923 0.000000 5.141357 c
|
||||||
|
h
|
||||||
|
5.000000 5.141357 m
|
||||||
|
5.000000 6.245927 5.895431 7.141357 7.000000 7.141357 c
|
||||||
|
8.104569 7.141357 9.000000 6.245927 9.000000 5.141357 c
|
||||||
|
9.000000 4.557765 l
|
||||||
|
9.000000 3.471050 8.746984 2.399258 8.260990 1.427270 c
|
||||||
|
7.894427 0.694144 l
|
||||||
|
7.647438 0.200165 7.046765 -0.000059 6.552786 0.246930 c
|
||||||
|
6.058808 0.493919 5.858583 1.094593 6.105573 1.588571 c
|
||||||
|
6.472136 2.321697 l
|
||||||
|
6.605721 2.588866 6.714662 2.866591 6.798144 3.151417 c
|
||||||
|
5.788369 3.252621 5.000000 4.104923 5.000000 5.141357 c
|
||||||
|
h
|
||||||
|
f*
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
1077
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
<< /Type /XObject
|
||||||
|
/Length 4 0 R
|
||||||
|
/Group << /Type /Group
|
||||||
|
/S /Transparency
|
||||||
|
>>
|
||||||
|
/Subtype /Form
|
||||||
|
/Resources << >>
|
||||||
|
/BBox [ 0.000000 0.000000 9.000000 7.000244 ]
|
||||||
|
>>
|
||||||
|
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 7.000244 m
|
||||||
|
9.000000 7.000244 l
|
||||||
|
9.000000 0.000025 l
|
||||||
|
0.000000 0.000025 l
|
||||||
|
0.000000 7.000244 l
|
||||||
|
h
|
||||||
|
f
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
227
|
||||||
|
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 9.000000 7.000244 ]
|
||||||
|
/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
|
||||||
|
0000001333 00000 n
|
||||||
|
0000001356 00000 n
|
||||||
|
0000001829 00000 n
|
||||||
|
0000001851 00000 n
|
||||||
|
0000002149 00000 n
|
||||||
|
0000002251 00000 n
|
||||||
|
0000002272 00000 n
|
||||||
|
0000002443 00000 n
|
||||||
|
0000002517 00000 n
|
||||||
|
trailer
|
||||||
|
<< /ID [ (some) (id) ]
|
||||||
|
/Root 10 0 R
|
||||||
|
/Size 11
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2577
|
||||||
|
%%EOF
|
||||||
@ -529,6 +529,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override public var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? {
|
||||||
|
return .widthMultiplier(factor: 0.35, min: 16.0, max: 200.0)
|
||||||
|
}
|
||||||
|
|
||||||
private var scheduledScrollToMessageId: (MessageId, Double?)?
|
private var scheduledScrollToMessageId: (MessageId, Double?)?
|
||||||
|
|
||||||
public var purposefulAction: (() -> Void)?
|
public var purposefulAction: (() -> Void)?
|
||||||
|
|||||||
@ -1,122 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import Display
|
|
||||||
import AsyncDisplayKit
|
|
||||||
import UIKit
|
|
||||||
import SwiftSignalKit
|
|
||||||
import Photos
|
|
||||||
import TelegramPresentationData
|
|
||||||
import UIKitRuntimeUtils
|
|
||||||
|
|
||||||
final class ChatDateSelectionSheet: ActionSheetController {
|
|
||||||
private let strings: PresentationStrings
|
|
||||||
|
|
||||||
private let _ready = Promise<Bool>()
|
|
||||||
override var ready: Promise<Bool> {
|
|
||||||
return self._ready
|
|
||||||
}
|
|
||||||
|
|
||||||
init(presentationData: PresentationData, completion: @escaping (Int32) -> Void) {
|
|
||||||
self.strings = presentationData.strings
|
|
||||||
|
|
||||||
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
|
|
||||||
|
|
||||||
self._ready.set(.single(true))
|
|
||||||
|
|
||||||
var updatedValue: Int32?
|
|
||||||
self.setItemGroups([
|
|
||||||
ActionSheetItemGroup(items: [
|
|
||||||
ChatDateSelectorItem(strings: self.strings, valueChanged: { value in
|
|
||||||
updatedValue = value
|
|
||||||
}),
|
|
||||||
ActionSheetButtonItem(title: self.strings.Common_Search, action: { [weak self] in
|
|
||||||
self?.dismissAnimated()
|
|
||||||
if let updatedValue = updatedValue {
|
|
||||||
completion(updatedValue)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
ActionSheetItemGroup(items: [
|
|
||||||
ActionSheetButtonItem(title: self.strings.Common_Cancel, action: { [weak self] in
|
|
||||||
self?.dismissAnimated()
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(coder aDecoder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class ChatDateSelectorItem: ActionSheetItem {
|
|
||||||
let strings: PresentationStrings
|
|
||||||
|
|
||||||
let valueChanged: (Int32) -> Void
|
|
||||||
|
|
||||||
init(strings: PresentationStrings, valueChanged: @escaping (Int32) -> Void) {
|
|
||||||
self.strings = strings
|
|
||||||
self.valueChanged = valueChanged
|
|
||||||
}
|
|
||||||
|
|
||||||
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
|
|
||||||
return ChatDateSelectorItemNode(theme: theme, strings: self.strings, valueChanged: self.valueChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateNode(_ node: ActionSheetItemNode) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class ChatDateSelectorItemNode: ActionSheetItemNode {
|
|
||||||
private let theme: ActionSheetControllerTheme
|
|
||||||
private let strings: PresentationStrings
|
|
||||||
|
|
||||||
private let pickerView: UIDatePicker
|
|
||||||
|
|
||||||
private let valueChanged: (Int32) -> Void
|
|
||||||
|
|
||||||
private var currentValue: Int32 {
|
|
||||||
return Int32(self.pickerView.date.timeIntervalSince1970)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(theme: ActionSheetControllerTheme, strings: PresentationStrings, valueChanged: @escaping (Int32) -> Void) {
|
|
||||||
self.theme = theme
|
|
||||||
self.strings = strings
|
|
||||||
self.valueChanged = valueChanged
|
|
||||||
|
|
||||||
UILabel.setDateLabel(theme.primaryTextColor)
|
|
||||||
|
|
||||||
self.pickerView = UIDatePicker()
|
|
||||||
self.pickerView.datePickerMode = .countDownTimer
|
|
||||||
self.pickerView.datePickerMode = .date
|
|
||||||
self.pickerView.locale = Locale(identifier: strings.baseLanguageCode)
|
|
||||||
|
|
||||||
self.pickerView.minimumDate = Date(timeIntervalSince1970: 1376438400.0)
|
|
||||||
self.pickerView.maximumDate = Date(timeIntervalSinceNow: 2.0)
|
|
||||||
|
|
||||||
if #available(iOS 13.4, *) {
|
|
||||||
self.pickerView.preferredDatePickerStyle = .wheels
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor")
|
|
||||||
self.pickerView.setValue(theme.primaryTextColor, forKey: "highlightColor")
|
|
||||||
|
|
||||||
super.init(theme: theme)
|
|
||||||
|
|
||||||
self.view.addSubview(self.pickerView)
|
|
||||||
self.pickerView.addTarget(self, action: #selector(self.pickerChanged), for: .valueChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
|
||||||
let size = CGSize(width: constrainedSize.width, height: 157.0)
|
|
||||||
|
|
||||||
self.pickerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 180.0))
|
|
||||||
|
|
||||||
self.updateInternalLayout(size, constrainedSize: constrainedSize)
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func pickerChanged() {
|
|
||||||
self.valueChanged(self.currentValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3568,7 +3568,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
return ASEditableTextNodeTargetForAction(target: nil)
|
return ASEditableTextNodeTargetForAction(target: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) || action == #selector(self.formatAttributesSpoiler(_:)) {
|
} else if action == #selector(self.formatAttributesBold(_:)) || action == #selector(self.formatAttributesItalic(_:)) || action == #selector(self.formatAttributesMonospace(_:)) || action == #selector(self.formatAttributesLink(_:)) || action == #selector(self.formatAttributesStrikethrough(_:)) || action == #selector(self.formatAttributesUnderline(_:)) || action == #selector(self.formatAttributesSpoiler(_:)) || action == #selector(self.formatAttributesQuote(_:)) {
|
||||||
if case .format = self.inputMenu.state {
|
if case .format = self.inputMenu.state {
|
||||||
if action == #selector(self.formatAttributesSpoiler(_:)), let selectedRange = self.textInputNode?.selectedRange {
|
if action == #selector(self.formatAttributesSpoiler(_:)), let selectedRange = self.textInputNode?.selectedRange {
|
||||||
var intersectsMonospace = false
|
var intersectsMonospace = false
|
||||||
@ -3582,6 +3582,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
} else {
|
} else {
|
||||||
return ASEditableTextNodeTargetForAction(target: nil)
|
return ASEditableTextNodeTargetForAction(target: nil)
|
||||||
}
|
}
|
||||||
|
} else if action == #selector(self.formatAttributesQuote(_:)), let selectedRange = self.textInputNode?.selectedRange {
|
||||||
|
let _ = selectedRange
|
||||||
|
return ASEditableTextNodeTargetForAction(target: self)
|
||||||
} else if action == #selector(self.formatAttributesMonospace(_:)), let selectedRange = self.textInputNode?.selectedRange {
|
} else if action == #selector(self.formatAttributesMonospace(_:)), let selectedRange = self.textInputNode?.selectedRange {
|
||||||
var intersectsSpoiler = false
|
var intersectsSpoiler = false
|
||||||
self.inputTextState.inputText.enumerateAttributes(in: selectedRange, options: [], using: { attributes, _, _ in
|
self.inputTextState.inputText.enumerateAttributes(in: selectedRange, options: [], using: { attributes, _, _ in
|
||||||
@ -3614,7 +3617,29 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 {
|
if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
var children: [UIAction] = [
|
var children: [UIAction] = []
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
children.append(UIAction(title: "Quote", image: nil) { [weak self] (action) in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.formatAttributesQuote(strongSelf)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var hasSpoilers = true
|
||||||
|
if self.presentationInterfaceState?.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
|
||||||
|
hasSpoilers = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasSpoilers {
|
||||||
|
children.append(UIAction(title: self.strings?.TextFormat_Spoiler ?? "Spoiler", image: nil) { [weak self] (action) in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.formatAttributesSpoiler(strongSelf)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
children.append(contentsOf: [
|
||||||
UIAction(title: self.strings?.TextFormat_Bold ?? "Bold", image: nil) { [weak self] (action) in
|
UIAction(title: self.strings?.TextFormat_Bold ?? "Bold", image: nil) { [weak self] (action) in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.formatAttributesBold(strongSelf)
|
strongSelf.formatAttributesBold(strongSelf)
|
||||||
@ -3645,20 +3670,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
strongSelf.formatAttributesUnderline(strongSelf)
|
strongSelf.formatAttributesUnderline(strongSelf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
] as [UIAction])
|
||||||
|
|
||||||
var hasSpoilers = true
|
|
||||||
if self.presentationInterfaceState?.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
|
|
||||||
hasSpoilers = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasSpoilers {
|
|
||||||
children.append(UIAction(title: self.strings?.TextFormat_Spoiler ?? "Spoiler", image: nil) { [weak self] (action) in
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.formatAttributesSpoiler(strongSelf)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let formatMenu = UIMenu(title: self.strings?.TextFormat_Format ?? "Format", image: nil, children: children)
|
let formatMenu = UIMenu(title: self.strings?.TextFormat_Format ?? "Format", image: nil, children: children)
|
||||||
actions.insert(formatMenu, at: 3)
|
actions.insert(formatMenu, at: 3)
|
||||||
@ -3737,6 +3749,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func formatAttributesQuote(_ sender: Any) {
|
||||||
|
self.inputMenu.back()
|
||||||
|
|
||||||
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||||||
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.quote), inputMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc func formatAttributesSpoiler(_ sender: Any) {
|
@objc func formatAttributesSpoiler(_ sender: Any) {
|
||||||
self.inputMenu.back()
|
self.inputMenu.back()
|
||||||
|
|
||||||
|
|||||||
@ -19,8 +19,10 @@ public struct ChatTextInputAttributes {
|
|||||||
public static let textUrl = NSAttributedString.Key(rawValue: "Attribute__TextUrl")
|
public static let textUrl = NSAttributedString.Key(rawValue: "Attribute__TextUrl")
|
||||||
public static let spoiler = NSAttributedString.Key(rawValue: "Attribute__Spoiler")
|
public static let spoiler = NSAttributedString.Key(rawValue: "Attribute__Spoiler")
|
||||||
public static let customEmoji = NSAttributedString.Key(rawValue: "Attribute__CustomEmoji")
|
public static let customEmoji = NSAttributedString.Key(rawValue: "Attribute__CustomEmoji")
|
||||||
|
public static let code = NSAttributedString.Key(rawValue: "Attribute__Code")
|
||||||
|
public static let quote = NSAttributedString.Key(rawValue: "Attribute__Blockquote")
|
||||||
|
|
||||||
public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler, ChatTextInputAttributes.customEmoji]
|
public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler, ChatTextInputAttributes.customEmoji, ChatTextInputAttributes.code, ChatTextInputAttributes.quote]
|
||||||
}
|
}
|
||||||
|
|
||||||
public let originalTextAttributeKey = NSAttributedString.Key(rawValue: "Attribute__OriginalText")
|
public let originalTextAttributeKey = NSAttributedString.Key(rawValue: "Attribute__OriginalText")
|
||||||
@ -115,6 +117,13 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo
|
|||||||
} else if key == ChatTextInputAttributes.customEmoji {
|
} else if key == ChatTextInputAttributes.customEmoji {
|
||||||
result.addAttribute(key, value: value, range: range)
|
result.addAttribute(key, value: value, range: range)
|
||||||
result.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
|
result.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
|
||||||
|
} else if key == ChatTextInputAttributes.quote {
|
||||||
|
result.addAttribute(key, value: value, range: range)
|
||||||
|
result.addAttribute(NSAttributedString.Key.backgroundColor, value: accentTextColor.withAlphaComponent(0.15), range: range)
|
||||||
|
let paragraphStyle = NSMutableParagraphStyle()
|
||||||
|
paragraphStyle.headIndent = 8.0
|
||||||
|
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])]
|
||||||
|
result.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,6 +202,22 @@ public final class ChatTextInputTextUrlAttribute: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class ChatTextInputTextQuoteAttribute: NSObject {
|
||||||
|
override public init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func isEqual(_ object: Any?) -> Bool {
|
||||||
|
guard let other = object as? ChatTextInputTextQuoteAttribute else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = other
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable {
|
public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable {
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case interactivelySelectedFromPackId
|
case interactivelySelectedFromPackId
|
||||||
@ -506,6 +531,135 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func quoteRangesEqual(_ lhs: [(NSRange, ChatTextInputTextQuoteAttribute)], _ rhs: [(NSRange, ChatTextInputTextQuoteAttribute)]) -> Bool {
|
||||||
|
if lhs.count != rhs.count {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i in 0 ..< lhs.count {
|
||||||
|
if lhs[i].0 != rhs[i].0 || !lhs[i].1.isEqual(rhs[i].1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshBlockQuotes(text: NSString, initialAttributedText: NSAttributedString, attributedText: NSMutableAttributedString, fullRange: NSRange) {
|
||||||
|
var quoteRanges: [(NSRange, ChatTextInputTextQuoteAttribute)] = []
|
||||||
|
initialAttributedText.enumerateAttribute(ChatTextInputAttributes.quote, in: fullRange, options: [], using: { value, range, _ in
|
||||||
|
if let value = value as? ChatTextInputTextQuoteAttribute {
|
||||||
|
quoteRanges.append((range, value))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
quoteRanges.sort(by: { $0.0.location < $1.0.location })
|
||||||
|
let initialQuoteRanges = quoteRanges
|
||||||
|
|
||||||
|
for i in 0 ..< quoteRanges.count {
|
||||||
|
let range = quoteRanges[i].0
|
||||||
|
|
||||||
|
var validLower = range.lowerBound
|
||||||
|
inner1: for i in range.lowerBound ..< range.upperBound {
|
||||||
|
if let c = UnicodeScalar(text.character(at: i)) {
|
||||||
|
if textUrlCharacters.contains(c) {
|
||||||
|
validLower = i
|
||||||
|
break inner1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break inner1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var validUpper = range.upperBound
|
||||||
|
inner2: for i in (validLower ..< range.upperBound).reversed() {
|
||||||
|
if let c = UnicodeScalar(text.character(at: i)) {
|
||||||
|
if textUrlCharacters.contains(c) {
|
||||||
|
validUpper = i + 1
|
||||||
|
break inner2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break inner2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let minLower = (i == 0) ? fullRange.lowerBound : quoteRanges[i - 1].0.upperBound
|
||||||
|
inner3: for i in (minLower ..< validLower).reversed() {
|
||||||
|
if let c = UnicodeScalar(text.character(at: i)) {
|
||||||
|
if textUrlEdgeCharacters.contains(c) {
|
||||||
|
validLower = i
|
||||||
|
} else {
|
||||||
|
break inner3
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break inner3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxUpper = (i == quoteRanges.count - 1) ? fullRange.upperBound : quoteRanges[i + 1].0.lowerBound
|
||||||
|
inner3: for i in validUpper ..< maxUpper {
|
||||||
|
if let c = UnicodeScalar(text.character(at: i)) {
|
||||||
|
if textUrlEdgeCharacters.contains(c) {
|
||||||
|
validUpper = i + 1
|
||||||
|
} else {
|
||||||
|
break inner3
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break inner3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteRanges[i] = (NSRange(location: validLower, length: validUpper - validLower), quoteRanges[i].1)
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteRanges = quoteRanges.filter({ $0.0.length > 0 })
|
||||||
|
|
||||||
|
while quoteRanges.count > 1 {
|
||||||
|
var hadReductions = false
|
||||||
|
outer: for i in 0 ..< quoteRanges.count - 1 {
|
||||||
|
if quoteRanges[i].1 === quoteRanges[i + 1].1 {
|
||||||
|
var combine = true
|
||||||
|
inner: for j in quoteRanges[i].0.upperBound ..< quoteRanges[i + 1].0.lowerBound {
|
||||||
|
if let c = UnicodeScalar(text.character(at: j)) {
|
||||||
|
if textUrlCharacters.contains(c) {
|
||||||
|
} else {
|
||||||
|
combine = false
|
||||||
|
break inner
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
combine = false
|
||||||
|
break inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if combine {
|
||||||
|
hadReductions = true
|
||||||
|
quoteRanges[i] = (NSRange(location: quoteRanges[i].0.lowerBound, length: quoteRanges[i + 1].0.upperBound - quoteRanges[i].0.lowerBound), quoteRanges[i].1)
|
||||||
|
quoteRanges.remove(at: i + 1)
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hadReductions {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if quoteRanges.count > 1 {
|
||||||
|
outer: for i in (1 ..< quoteRanges.count).reversed() {
|
||||||
|
for j in 0 ..< i {
|
||||||
|
if quoteRanges[j].1 === quoteRanges[i].1 {
|
||||||
|
quoteRanges.remove(at: i)
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !quoteRangesEqual(quoteRanges, initialQuoteRanges) {
|
||||||
|
attributedText.removeAttribute(ChatTextInputAttributes.quote, range: fullRange)
|
||||||
|
for (range, attribute) in quoteRanges {
|
||||||
|
let _ = attribute
|
||||||
|
attributedText.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set<String>, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) {
|
public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set<String>, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) {
|
||||||
refreshChatTextInputAttributes(textView: textNode.textView, primaryTextColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, baseFontSize: baseFontSize, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider)
|
refreshChatTextInputAttributes(textView: textNode.textView, primaryTextColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, baseFontSize: baseFontSize, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider)
|
||||||
}
|
}
|
||||||
@ -534,6 +688,13 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo
|
|||||||
|
|
||||||
resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider)
|
resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider)
|
||||||
|
|
||||||
|
text = resultAttributedText.string as NSString
|
||||||
|
fullRange = NSRange(location: 0, length: text.length)
|
||||||
|
attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText))
|
||||||
|
refreshBlockQuotes(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange)
|
||||||
|
|
||||||
|
resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: primaryTextColor, accentTextColor: accentTextColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed, availableEmojis: availableEmojis, emojiViewProvider: emojiViewProvider)
|
||||||
|
|
||||||
if !resultAttributedText.isEqual(to: initialAttributedText) {
|
if !resultAttributedText.isEqual(to: initialAttributedText) {
|
||||||
fullRange = NSRange(location: 0, length: textView.textStorage.length)
|
fullRange = NSRange(location: 0, length: textView.textStorage.length)
|
||||||
|
|
||||||
@ -546,6 +707,7 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo
|
|||||||
textView.textStorage.removeAttribute(ChatTextInputAttributes.textUrl, range: fullRange)
|
textView.textStorage.removeAttribute(ChatTextInputAttributes.textUrl, range: fullRange)
|
||||||
textView.textStorage.removeAttribute(ChatTextInputAttributes.spoiler, range: fullRange)
|
textView.textStorage.removeAttribute(ChatTextInputAttributes.spoiler, range: fullRange)
|
||||||
textView.textStorage.removeAttribute(ChatTextInputAttributes.customEmoji, range: fullRange)
|
textView.textStorage.removeAttribute(ChatTextInputAttributes.customEmoji, range: fullRange)
|
||||||
|
textView.textStorage.removeAttribute(ChatTextInputAttributes.quote, range: fullRange)
|
||||||
|
|
||||||
textView.textStorage.addAttribute(NSAttributedString.Key.font, value: Font.regular(baseFontSize), range: fullRange)
|
textView.textStorage.addAttribute(NSAttributedString.Key.font, value: Font.regular(baseFontSize), range: fullRange)
|
||||||
textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: primaryTextColor, range: fullRange)
|
textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: primaryTextColor, range: fullRange)
|
||||||
@ -589,6 +751,13 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo
|
|||||||
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||||
textView.textStorage.addAttribute(key, value: value, range: range)
|
textView.textStorage.addAttribute(key, value: value, range: range)
|
||||||
textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
|
textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
|
||||||
|
} else if key == ChatTextInputAttributes.quote {
|
||||||
|
textView.textStorage.addAttribute(key, value: value, range: range)
|
||||||
|
textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: accentTextColor.withAlphaComponent(0.15), range: range)
|
||||||
|
let paragraphStyle = NSMutableParagraphStyle()
|
||||||
|
paragraphStyle.headIndent = 8.0
|
||||||
|
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])]
|
||||||
|
textView.textStorage.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -690,6 +859,12 @@ public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, th
|
|||||||
} else {
|
} else {
|
||||||
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
|
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
|
||||||
}
|
}
|
||||||
|
} else if key == ChatTextInputAttributes.quote {
|
||||||
|
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.15), range: range)
|
||||||
|
let paragraphStyle = NSMutableParagraphStyle()
|
||||||
|
paragraphStyle.headIndent = 8.0
|
||||||
|
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])]
|
||||||
|
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -882,7 +1057,7 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu
|
|||||||
stringOffset -= match.range(at: 2).length + match.range(at: 4).length
|
stringOffset -= match.range(at: 2).length + match.range(at: 4).length
|
||||||
|
|
||||||
let substring = string.substring(with: match.range(at: 1)) + text + string.substring(with: match.range(at: 5))
|
let substring = string.substring(with: match.range(at: 1)) + text + string.substring(with: match.range(at: 5))
|
||||||
result.append(NSAttributedString(string: substring, attributes: [ChatTextInputAttributes.monospace: true as NSNumber]))
|
result.append(NSAttributedString(string: substring, attributes: [ChatTextInputAttributes.code: true as NSNumber]))
|
||||||
offsetRanges.append((NSMakeRange(matchIndex + match.range(at: 1).length, text.count), 6))
|
offsetRanges.append((NSMakeRange(matchIndex + match.range(at: 1).length, text.count), 6))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -902,13 +1077,20 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu
|
|||||||
} else {
|
} else {
|
||||||
let text = string.substring(with: pre)
|
let text = string.substring(with: pre)
|
||||||
|
|
||||||
let entity = string.substring(with: match.range(at: 7))
|
var entity = string.substring(with: match.range(at: 7))
|
||||||
let substring = string.substring(with: match.range(at: 6)) + text + string.substring(with: match.range(at: 9))
|
var substring = string.substring(with: match.range(at: 6)) + text + string.substring(with: match.range(at: 9))
|
||||||
|
|
||||||
|
if entity == "`" && substring.hasPrefix("``") && substring.hasSuffix("``") {
|
||||||
|
entity = "```"
|
||||||
|
substring = String(substring[substring.index(substring.startIndex, offsetBy: 2) ..< substring.index(substring.endIndex, offsetBy: -2)])
|
||||||
|
}
|
||||||
|
|
||||||
let textInputAttribute: NSAttributedString.Key?
|
let textInputAttribute: NSAttributedString.Key?
|
||||||
switch entity {
|
switch entity {
|
||||||
case "`":
|
case "`":
|
||||||
textInputAttribute = ChatTextInputAttributes.monospace
|
textInputAttribute = ChatTextInputAttributes.monospace
|
||||||
|
case "```":
|
||||||
|
textInputAttribute = ChatTextInputAttributes.code
|
||||||
case "**":
|
case "**":
|
||||||
textInputAttribute = ChatTextInputAttributes.bold
|
textInputAttribute = ChatTextInputAttributes.bold
|
||||||
case "__":
|
case "__":
|
||||||
|
|||||||
@ -167,6 +167,10 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate
|
|||||||
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Spoiler))
|
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Spoiler))
|
||||||
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||||
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .CustomEmoji(stickerPack: nil, fileId: value.fileId)))
|
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .CustomEmoji(stickerPack: nil, fileId: value.fileId)))
|
||||||
|
} else if key == ChatTextInputAttributes.code {
|
||||||
|
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Code))
|
||||||
|
} else if key == ChatTextInputAttributes.quote {
|
||||||
|
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .BlockQuote))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
import Postbox
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
import Display
|
||||||
|
|
||||||
public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [MessageTextEntity]) -> NSAttributedString {
|
public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [MessageTextEntity]) -> NSAttributedString {
|
||||||
var nsString: NSString?
|
var nsString: NSString?
|
||||||
@ -45,6 +46,8 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
|
|||||||
string.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: range)
|
string.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: range)
|
||||||
case let .CustomEmoji(_, fileId):
|
case let .CustomEmoji(_, fileId):
|
||||||
string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: range)
|
string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: range)
|
||||||
|
case .BlockQuote:
|
||||||
|
string.addAttribute(ChatTextInputAttributes.quote, value: ChatTextInputTextQuoteAttribute(), range: range)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -52,7 +55,9 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
|
|||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
|
|
||||||
public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:]) -> NSAttributedString {
|
public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:]) -> NSAttributedString {
|
||||||
|
let baseQuoteTintColor = baseQuoteTintColor ?? baseColor
|
||||||
|
|
||||||
var nsString: NSString?
|
var nsString: NSString?
|
||||||
let string = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor])
|
let string = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor])
|
||||||
var skipEntity = false
|
var skipEntity = false
|
||||||
@ -62,6 +67,8 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
|
|||||||
}
|
}
|
||||||
var fontAttributes: [NSRange: ChatTextFontAttributes] = [:]
|
var fontAttributes: [NSRange: ChatTextFontAttributes] = [:]
|
||||||
|
|
||||||
|
var nextBlockId = 0
|
||||||
|
|
||||||
var rangeOffset: Int = 0
|
var rangeOffset: Int = 0
|
||||||
for i in 0 ..< entities.count {
|
for i in 0 ..< entities.count {
|
||||||
if skipEntity {
|
if skipEntity {
|
||||||
@ -197,13 +204,13 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
|
|||||||
nsString = text as NSString
|
nsString = text as NSString
|
||||||
}
|
}
|
||||||
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand), value: nsString!.substring(with: range), range: range)
|
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand), value: nsString!.substring(with: range), range: range)
|
||||||
case .Code, .Pre:
|
case .Pre:
|
||||||
string.addAttribute(NSAttributedString.Key.font, value: fixedFont, range: range)
|
string.addAttribute(NSAttributedString.Key.font, value: fixedFont, range: range)
|
||||||
if nsString == nil {
|
if nsString == nil {
|
||||||
nsString = text as NSString
|
nsString = text as NSString
|
||||||
}
|
}
|
||||||
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre), value: nsString!.substring(with: range), range: range)
|
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre), value: nsString!.substring(with: range), range: range)
|
||||||
case .BlockQuote:
|
case .BlockQuote, .Code:
|
||||||
if let fontAttribute = fontAttributes[range] {
|
if let fontAttribute = fontAttributes[range] {
|
||||||
fontAttributes[range] = fontAttribute.union(.blockQuote)
|
fontAttributes[range] = fontAttribute.union(.blockQuote)
|
||||||
} else {
|
} else {
|
||||||
@ -211,17 +218,31 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
|
|||||||
}
|
}
|
||||||
|
|
||||||
let paragraphBreak = "\n"
|
let paragraphBreak = "\n"
|
||||||
string.insert(NSAttributedString(string: paragraphBreak), at: range.lowerBound)
|
|
||||||
|
|
||||||
let paragraphRange = NSRange(location: range.lowerBound + paragraphBreak.count, length: range.upperBound - range.lowerBound)
|
|
||||||
|
|
||||||
let paragraphStyle = NSMutableParagraphStyle()
|
let paragraphRange: NSRange
|
||||||
paragraphStyle.headIndent = 10.0
|
if range.lowerBound == 0 {
|
||||||
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])]
|
paragraphRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)
|
||||||
string.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: paragraphRange)
|
} else if string.string[string.string.index(string.string.startIndex, offsetBy: range.lowerBound)] == "\n" {
|
||||||
|
paragraphRange = NSRange(location: range.lowerBound + 1, length: range.upperBound - range.lowerBound - 1)
|
||||||
|
} else if string.string[string.string.index(string.string.startIndex, offsetBy: range.lowerBound - 1)] == "\n" {
|
||||||
|
paragraphRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)
|
||||||
|
} else {
|
||||||
|
string.insert(NSAttributedString(string: paragraphBreak), at: range.lowerBound)
|
||||||
|
paragraphRange = NSRange(location: range.lowerBound + paragraphBreak.count, length: range.upperBound - range.lowerBound)
|
||||||
|
}
|
||||||
|
|
||||||
|
string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(id: nextBlockId, title: nil, color: baseQuoteTintColor), range: paragraphRange)
|
||||||
|
nextBlockId += 1
|
||||||
|
|
||||||
string.insert(NSAttributedString(string: paragraphBreak), at: paragraphRange.upperBound)
|
if string.string.index(string.string.startIndex, offsetBy: paragraphRange.upperBound) != string.string.endIndex {
|
||||||
rangeOffset += paragraphBreak.count
|
if string.string[string.string.index(string.string.startIndex, offsetBy: paragraphRange.upperBound)] == "\n" {
|
||||||
|
string.replaceCharacters(in: NSMakeRange(paragraphRange.upperBound, 1), with: "")
|
||||||
|
rangeOffset -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rangeOffset += 0
|
||||||
|
//rangeOffset += paragraphBreak.count
|
||||||
case .BankCard:
|
case .BankCard:
|
||||||
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
|
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
|
||||||
if underlineLinks && underlineAllLinks {
|
if underlineLinks && underlineAllLinks {
|
||||||
@ -268,7 +289,9 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
|
|||||||
func addFont(ranges: [NSRange], fontAttributes: ChatTextFontAttributes) {
|
func addFont(ranges: [NSRange], fontAttributes: ChatTextFontAttributes) {
|
||||||
for range in ranges {
|
for range in ranges {
|
||||||
var font: UIFont?
|
var font: UIFont?
|
||||||
if fontAttributes == [.bold, .italic] {
|
if fontAttributes.contains(.blockQuote) {
|
||||||
|
font = baseFont.withSize(round(baseFont.pointSize * 0.8235294117647058))
|
||||||
|
} else if fontAttributes == [.bold, .italic] {
|
||||||
font = boldItalicFont
|
font = boldItalicFont
|
||||||
} else if fontAttributes == [.bold] {
|
} else if fontAttributes == [.bold] {
|
||||||
font = boldFont
|
font = boldFont
|
||||||
|
|||||||
@ -42,4 +42,5 @@ public struct TelegramTextAttributes {
|
|||||||
public static let BlockQuote = "TelegramBlockQuote"
|
public static let BlockQuote = "TelegramBlockQuote"
|
||||||
public static let Pre = "TelegramPre"
|
public static let Pre = "TelegramPre"
|
||||||
public static let Spoiler = "TelegramSpoiler"
|
public static let Spoiler = "TelegramSpoiler"
|
||||||
|
public static let Code = "TelegramCode"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user