mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
[WIP] Quotes
This commit is contained in:
parent
3bd3bbc7eb
commit
7d58ff6873
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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])? {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user