[WIP] Quotes

This commit is contained in:
Ali 2023-10-19 00:10:34 +04:00
parent 3bd3bbc7eb
commit 7d58ff6873
7 changed files with 173 additions and 14 deletions

View File

@ -574,9 +574,11 @@ public enum ChatControllerSubject: Equatable {
}
public struct SelectionState: Equatable {
public var canQuote: Bool
public var quote: Quote?
public init(quote: Quote?) {
public init(canQuote: Bool, quote: Quote?) {
self.canQuote = canQuote
self.quote = quote
}
}

View File

@ -105,11 +105,11 @@ private final class TextNodeLine {
let descent: CGFloat
let range: NSRange?
let isRTL: Bool
let strikethroughs: [TextNodeStrikethrough]
let spoilers: [TextNodeSpoiler]
let spoilerWords: [TextNodeSpoiler]
let embeddedItems: [TextNodeEmbeddedItem]
let attachments: [TextNodeAttachment]
var strikethroughs: [TextNodeStrikethrough]
var spoilers: [TextNodeSpoiler]
var spoilerWords: [TextNodeSpoiler]
var embeddedItems: [TextNodeEmbeddedItem]
var attachments: [TextNodeAttachment]
let additionalTrailingLine: (CTLine, Double)?
init(line: CTLine, frame: CGRect, ascent: CGFloat, descent: CGFloat, range: NSRange?, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler], spoilerWords: [TextNodeSpoiler], embeddedItems: [TextNodeEmbeddedItem], attachments: [TextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) {
@ -1039,6 +1039,78 @@ public final class TextAccessibilityOverlayNode: ASDisplayNode {
}
}
private func addSpoiler(line: TextNodeLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int) {
var secondaryLeftOffset: CGFloat = 0.0
let rawLeftOffset = CTLineGetOffsetForStringIndex(line.line, startIndex, &secondaryLeftOffset)
var leftOffset = floor(rawLeftOffset)
if !rawLeftOffset.isEqual(to: secondaryLeftOffset) {
leftOffset = floor(secondaryLeftOffset)
}
var secondaryRightOffset: CGFloat = 0.0
let rawRightOffset = CTLineGetOffsetForStringIndex(line.line, endIndex, &secondaryRightOffset)
var rightOffset = ceil(rawRightOffset)
if !rawRightOffset.isEqual(to: secondaryRightOffset) {
rightOffset = ceil(secondaryRightOffset)
}
line.spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent)))
}
private func addSpoilerWord(line: TextNodeLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) {
var secondaryLeftOffset: CGFloat = 0.0
let rawLeftOffset = CTLineGetOffsetForStringIndex(line.line, startIndex, &secondaryLeftOffset)
var leftOffset = floor(rawLeftOffset)
if !rawLeftOffset.isEqual(to: secondaryLeftOffset) {
leftOffset = floor(secondaryLeftOffset)
}
var secondaryRightOffset: CGFloat = 0.0
let rawRightOffset = CTLineGetOffsetForStringIndex(line.line, endIndex, &secondaryRightOffset)
var rightOffset = ceil(rawRightOffset)
if !rawRightOffset.isEqual(to: secondaryRightOffset) {
rightOffset = ceil(secondaryRightOffset)
}
line.spoilerWords.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent)))
}
private func addEmbeddedItem(item: AnyHashable, line: TextNodeLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) {
var secondaryLeftOffset: CGFloat = 0.0
let rawLeftOffset = CTLineGetOffsetForStringIndex(line.line, startIndex, &secondaryLeftOffset)
var leftOffset = floor(rawLeftOffset)
if !rawLeftOffset.isEqual(to: secondaryLeftOffset) {
leftOffset = floor(secondaryLeftOffset)
}
var secondaryRightOffset: CGFloat = 0.0
let rawRightOffset = CTLineGetOffsetForStringIndex(line.line, endIndex, &secondaryRightOffset)
var rightOffset = ceil(rawRightOffset)
if !rawRightOffset.isEqual(to: secondaryRightOffset) {
rightOffset = ceil(secondaryRightOffset)
}
line.embeddedItems.append(TextNodeEmbeddedItem(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), item: item))
}
private func addAttachment(attachment: UIImage, line: TextNodeLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) {
var secondaryLeftOffset: CGFloat = 0.0
let rawLeftOffset = CTLineGetOffsetForStringIndex(line.line, startIndex, &secondaryLeftOffset)
var leftOffset = floor(rawLeftOffset)
if !rawLeftOffset.isEqual(to: secondaryLeftOffset) {
leftOffset = floor(secondaryLeftOffset)
}
var secondaryRightOffset: CGFloat = 0.0
let rawRightOffset = CTLineGetOffsetForStringIndex(line.line, endIndex, &secondaryRightOffset)
var rightOffset = ceil(rawRightOffset)
if !rawRightOffset.isEqual(to: secondaryRightOffset) {
rightOffset = ceil(secondaryRightOffset)
}
line.attachments.append(TextNodeAttachment(range: NSMakeRange(startIndex, endIndex - startIndex), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), attachment: attachment))
}
open class TextNode: ASDisplayNode {
public internal(set) var cachedLayout: TextNodeLayout?
@ -1304,7 +1376,9 @@ open class TextNode: ASDisplayNode {
}
var lines: [TextNodeLine] = []
var blockQuotes: [TextNodeBlockQuote] = []
for i in 0 ..< calculatedSegments.count {
let segment = calculatedSegments[i]
if i != 0 {
@ -1336,6 +1410,64 @@ open class TextNode: ASDisplayNode {
size.height += line.frame.height
blockWidth = max(blockWidth, line.frame.origin.x + line.frame.width)
if let range = line.range {
attributedString.enumerateAttributes(in: range, options: []) { attributes, range, _ in
if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil {
var ascent: CGFloat = 0.0
var descent: CGFloat = 0.0
CTLineGetTypographicBounds(line.line, &ascent, &descent, nil)
var startIndex: Int?
var currentIndex: Int?
let nsString = (attributedString.string as NSString)
nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in
if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil {
if let currentStartIndex = startIndex {
startIndex = nil
let endIndex = range.location
addSpoilerWord(line: line, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex)
}
} else if startIndex == nil {
startIndex = range.location
}
currentIndex = range.location + range.length
}
if let currentStartIndex = startIndex, let currentIndex = currentIndex {
startIndex = nil
let endIndex = currentIndex
addSpoilerWord(line: line, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: 0.0)
}
addSpoiler(line: line, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
let lowerX = floor(CTLineGetOffsetForStringIndex(line.line, range.location, nil))
let upperX = ceil(CTLineGetOffsetForStringIndex(line.line, range.location + range.length, nil))
let x = lowerX < upperX ? lowerX : upperX
line.strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: line.frame.height)))
}
if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) {
if displayEmbeddedItemsUnderSpoilers || (attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] == nil && attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] == nil) {
var ascent: CGFloat = 0.0
var descent: CGFloat = 0.0
CTLineGetTypographicBounds(line.line, &ascent, &descent, nil)
addEmbeddedItem(item: embeddedItem, line: line, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
}
}
if let attachment = attributes[NSAttributedString.Key.attachment] as? UIImage {
var ascent: CGFloat = 0.0
var descent: CGFloat = 0.0
CTLineGetTypographicBounds(line.line, &ascent, &descent, nil)
addAttachment(attachment: attachment, line: line, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
}
}
}
lines.append(line)
}

View File

@ -17,6 +17,11 @@ private func rtfStringWithAppliedEntities(_ text: String, entities: [MessageText
}
})
test.removeAttribute(ChatTextInputAttributes.customEmoji, range: NSRange(location: 0, length: test.length))
test.enumerateAttribute(ChatTextInputAttributes.quote, in: NSRange(location: 0, length: sourceString.length), using: { value, range, _ in
if value != nil {
}
})
if let data = try? test.data(from: NSRange(location: 0, length: test.length), documentAttributes: [NSAttributedString.DocumentAttributeKey.documentType: NSAttributedString.DocumentType.rtf]) {
if var rtf = String(data: data, encoding: .windowsCP1252) {

View File

@ -412,7 +412,10 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
cutout = TextNodeCutout(topRight: CGSize(width: cutoutWidth, height: remainingCutoutHeight))
}
let subtitleString = subtitle
let subtitleString = NSMutableAttributedString(attributedString: subtitle)
subtitleString.addAttribute(.foregroundColor, value: messageTheme.primaryTextColor, range: NSMakeRange(0, subtitle.length))
subtitleString.addAttribute(.font, value: titleFont, range: NSMakeRange(0, subtitle.length))
let subtitleLayoutAndApplyValue = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets()))
subtitleLayoutAndApply = subtitleLayoutAndApplyValue

View File

@ -1137,7 +1137,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
if let item = self.item, let range, let selection = self.getCurrentTextSelection(customRange: range) {
quote = ChatControllerSubject.MessageOptionsInfo.Quote(messageId: item.message.id, text: selection.text)
}
return ChatControllerSubject.MessageOptionsInfo.SelectionState(quote: quote)
return ChatControllerSubject.MessageOptionsInfo.SelectionState(canQuote: true, quote: quote)
}
public func getCurrentTextSelection(customRange: NSRange? = nil) -> (text: String, entities: [MessageTextEntity])? {

View File

@ -27,7 +27,7 @@ private func presentChatInputOptions(selfController: ChatControllerImpl, sourceN
var sources: [ContextController.Source] = []
let replySelectionState = Promise<ChatControllerSubject.MessageOptionsInfo.SelectionState>(ChatControllerSubject.MessageOptionsInfo.SelectionState(quote: nil))
let replySelectionState = Promise<ChatControllerSubject.MessageOptionsInfo.SelectionState>(ChatControllerSubject.MessageOptionsInfo.SelectionState(canQuote: false, quote: nil))
if let source = chatReplyOptions(selfController: selfController, sourceNode: sourceNode, getContextController: {
return getContextController?()
@ -343,7 +343,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch
f(.default)
})))
} else {
} else if let message = messages.first, !message.text.isEmpty {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Select Specific Quote", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Quote"), color: theme.contextMenu.primaryColor) }, action: { [weak selfController, weak chatController] c, _ in
guard let selfController, let chatController else {
@ -448,7 +448,18 @@ private func chatReplyOptions(selfController: ChatControllerImpl, sourceNode: AS
if let quote = replySubject.quote {
replyQuote = ChatControllerSubject.MessageOptionsInfo.Quote(messageId: replySubject.messageId, text: quote.text)
}
selectionState.set(.single(ChatControllerSubject.MessageOptionsInfo.SelectionState(quote: replyQuote)))
selectionState.set(selfController.context.account.postbox.messagesAtIds([replySubject.messageId])
|> map { messages -> ChatControllerSubject.MessageOptionsInfo.SelectionState in
var canQuote = false
if let message = messages.first, !message.text.isEmpty {
canQuote = true
}
return ChatControllerSubject.MessageOptionsInfo.SelectionState(
canQuote: canQuote,
quote: replyQuote
)
}
|> distinctUntilChanged)
guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [replySubject.messageId.peerId], ids: [replySubject.messageId], info: .reply(ChatControllerSubject.MessageOptionsInfo.Reply(quote: replyQuote, selectionState: selectionState))), botStart: nil, mode: .standard(previewing: true)) as? ChatControllerImpl else {
return nil

View File

@ -5311,9 +5311,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return nil
}
}
case .reply:
//TODO:localize
subtitleTextSignal = .single("You can select a specific part to quote")
case let .reply(reply):
subtitleTextSignal = reply.selectionState.get()
|> map { selectionState -> String? in
if !selectionState.canQuote {
return nil
}
//TODO:localize
return "You can select a specific part to quote"
}
case let .link(link):
subtitleTextSignal = link.options
|> map { options -> String? in