Quotes experiment

This commit is contained in:
Ali
2023-10-03 23:20:45 +04:00
parent 68a640dc44
commit bab2b39725
26 changed files with 1654 additions and 692 deletions

View File

@@ -19,8 +19,10 @@ public struct ChatTextInputAttributes {
public static let textUrl = NSAttributedString.Key(rawValue: "Attribute__TextUrl")
public static let spoiler = NSAttributedString.Key(rawValue: "Attribute__Spoiler")
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")
@@ -115,6 +117,13 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo
} else if key == ChatTextInputAttributes.customEmoji {
result.addAttribute(key, value: value, 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 {
private enum CodingKeys: String, CodingKey {
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)?) {
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)
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) {
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.spoiler, 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.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 {
textView.textStorage.addAttribute(key, value: value, 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 {
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
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))
}
}
@@ -902,13 +1077,20 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu
} else {
let text = string.substring(with: pre)
let 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 entity = string.substring(with: match.range(at: 7))
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?
switch entity {
case "`":
textInputAttribute = ChatTextInputAttributes.monospace
case "```":
textInputAttribute = ChatTextInputAttributes.code
case "**":
textInputAttribute = ChatTextInputAttributes.bold
case "__":

View File

@@ -167,6 +167,10 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Spoiler))
} 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)))
} 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))
}
}
})

View File

@@ -2,6 +2,7 @@ import Foundation
import UIKit
import Postbox
import TelegramCore
import Display
public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [MessageTextEntity]) -> NSAttributedString {
var nsString: NSString?
@@ -45,6 +46,8 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
string.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: range)
case let .CustomEmoji(_, fileId):
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:
break
}
@@ -52,7 +55,9 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
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?
let string = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: baseFont, NSAttributedString.Key.foregroundColor: baseColor])
var skipEntity = false
@@ -62,6 +67,8 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
}
var fontAttributes: [NSRange: ChatTextFontAttributes] = [:]
var nextBlockId = 0
var rangeOffset: Int = 0
for i in 0 ..< entities.count {
if skipEntity {
@@ -197,13 +204,13 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
nsString = text as NSString
}
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)
if nsString == nil {
nsString = text as NSString
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre), value: nsString!.substring(with: range), range: range)
case .BlockQuote:
case .BlockQuote, .Code:
if let fontAttribute = fontAttributes[range] {
fontAttributes[range] = fontAttribute.union(.blockQuote)
} else {
@@ -211,17 +218,31 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
}
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()
paragraphStyle.headIndent = 10.0
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: paragraphStyle.headIndent, options: [:])]
string.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: paragraphRange)
let paragraphRange: NSRange
if range.lowerBound == 0 {
paragraphRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)
} 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)
rangeOffset += paragraphBreak.count
if string.string.index(string.string.startIndex, offsetBy: paragraphRange.upperBound) != string.string.endIndex {
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:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {
@@ -268,7 +289,9 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
func addFont(ranges: [NSRange], fontAttributes: ChatTextFontAttributes) {
for range in ranges {
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
} else if fontAttributes == [.bold] {
font = boldFont

View File

@@ -42,4 +42,5 @@ public struct TelegramTextAttributes {
public static let BlockQuote = "TelegramBlockQuote"
public static let Pre = "TelegramPre"
public static let Spoiler = "TelegramSpoiler"
public static let Code = "TelegramCode"
}