mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-25 01:22:41 +00:00
Merge commit '1a8b151446d3a088e7c5431f24c660210a22f968'
This commit is contained in:
commit
74d9c29d97
@ -1757,7 +1757,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
strongSelf.dustNode = dustNode
|
strongSelf.dustNode = dustNode
|
||||||
strongSelf.contextContainer.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode)
|
strongSelf.contextContainer.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode)
|
||||||
}
|
}
|
||||||
dustNode.update(size: textNodeFrame.size, color: theme.messageTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) })
|
dustNode.update(size: textNodeFrame.size, color: theme.messageTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) })
|
||||||
dustNode.frame = textNodeFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
dustNode.frame = textNodeFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
||||||
|
|
||||||
} else if let dustNode = strongSelf.dustNode {
|
} else if let dustNode = strongSelf.dustNode {
|
||||||
@ -1786,11 +1786,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
strongSelf.inputActivitiesNode.alpha = 1.0
|
strongSelf.inputActivitiesNode.alpha = 1.0
|
||||||
strongSelf.textNode.alpha = 0.0
|
strongSelf.textNode.alpha = 0.0
|
||||||
strongSelf.authorNode.alpha = 0.0
|
strongSelf.authorNode.alpha = 0.0
|
||||||
|
strongSelf.dustNode?.alpha = 0.0
|
||||||
|
|
||||||
if animated || animateContent {
|
if animated || animateContent {
|
||||||
strongSelf.inputActivitiesNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
strongSelf.inputActivitiesNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||||
strongSelf.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
|
strongSelf.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
|
||||||
strongSelf.authorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
|
strongSelf.authorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
|
||||||
|
strongSelf.dustNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1798,6 +1800,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
strongSelf.inputActivitiesNode.alpha = 0.0
|
strongSelf.inputActivitiesNode.alpha = 0.0
|
||||||
strongSelf.textNode.alpha = 1.0
|
strongSelf.textNode.alpha = 1.0
|
||||||
strongSelf.authorNode.alpha = 1.0
|
strongSelf.authorNode.alpha = 1.0
|
||||||
|
strongSelf.dustNode?.alpha = 1.0
|
||||||
if animated || animateContent {
|
if animated || animateContent {
|
||||||
strongSelf.inputActivitiesNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { value in
|
strongSelf.inputActivitiesNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { value in
|
||||||
if let strongSelf = self, value {
|
if let strongSelf = self, value {
|
||||||
@ -1806,6 +1809,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
})
|
})
|
||||||
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||||
strongSelf.authorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
strongSelf.authorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||||
|
strongSelf.dustNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||||
} else {
|
} else {
|
||||||
strongSelf.inputActivitiesNode.removeFromSupernode()
|
strongSelf.inputActivitiesNode.removeFromSupernode()
|
||||||
}
|
}
|
||||||
@ -2016,6 +2020,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
textFrame.origin.x = contentRect.origin.x
|
textFrame.origin.x = contentRect.origin.x
|
||||||
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
||||||
|
|
||||||
|
if let dustNode = self.dustNode {
|
||||||
|
transition.updateFrameAdditive(node: dustNode, frame: textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0))
|
||||||
|
}
|
||||||
|
|
||||||
var mediaPreviewOffsetX = textFrame.origin.x + 1.0
|
var mediaPreviewOffsetX = textFrame.origin.x + 1.0
|
||||||
let contentImageSpacing: CGFloat = 2.0
|
let contentImageSpacing: CGFloat = 2.0
|
||||||
for (_, media, mediaSize) in self.currentMediaPreviewSpecs {
|
for (_, media, mediaSize) in self.currentMediaPreviewSpecs {
|
||||||
|
|||||||
@ -44,14 +44,16 @@ private final class TextNodeLine {
|
|||||||
let isRTL: Bool
|
let isRTL: Bool
|
||||||
let strikethroughs: [TextNodeStrikethrough]
|
let strikethroughs: [TextNodeStrikethrough]
|
||||||
let spoilers: [TextNodeSpoiler]
|
let spoilers: [TextNodeSpoiler]
|
||||||
|
let spoilerWords: [TextNodeSpoiler]
|
||||||
|
|
||||||
init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler]) {
|
init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler], spoilerWords: [TextNodeSpoiler]) {
|
||||||
self.line = line
|
self.line = line
|
||||||
self.frame = frame
|
self.frame = frame
|
||||||
self.range = range
|
self.range = range
|
||||||
self.isRTL = isRTL
|
self.isRTL = isRTL
|
||||||
self.strikethroughs = strikethroughs
|
self.strikethroughs = strikethroughs
|
||||||
self.spoilers = spoilers
|
self.spoilers = spoilers
|
||||||
|
self.spoilerWords = spoilerWords
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +176,7 @@ public final class TextNodeLayout: NSObject {
|
|||||||
fileprivate let displaySpoilers: Bool
|
fileprivate let displaySpoilers: Bool
|
||||||
public let hasRTL: Bool
|
public let hasRTL: Bool
|
||||||
public let spoilers: [(NSRange, CGRect)]
|
public let spoilers: [(NSRange, CGRect)]
|
||||||
|
public let spoilerWords: [(NSRange, CGRect)]
|
||||||
|
|
||||||
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, explicitAlignment: NSTextAlignment, resolvedAlignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) {
|
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, explicitAlignment: NSTextAlignment, resolvedAlignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) {
|
||||||
self.attributedString = attributedString
|
self.attributedString = attributedString
|
||||||
@ -199,14 +202,17 @@ public final class TextNodeLayout: NSObject {
|
|||||||
self.displaySpoilers = displaySpoilers
|
self.displaySpoilers = displaySpoilers
|
||||||
var hasRTL = false
|
var hasRTL = false
|
||||||
var spoilers: [(NSRange, CGRect)] = []
|
var spoilers: [(NSRange, CGRect)] = []
|
||||||
|
var spoilerWords: [(NSRange, CGRect)] = []
|
||||||
for line in lines {
|
for line in lines {
|
||||||
if line.isRTL {
|
if line.isRTL {
|
||||||
hasRTL = true
|
hasRTL = true
|
||||||
}
|
}
|
||||||
spoilers.append(contentsOf: line.spoilers.map { ( $0.range, $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY)) })
|
spoilers.append(contentsOf: line.spoilers.map { ( $0.range, $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY)) })
|
||||||
|
spoilerWords.append(contentsOf: line.spoilerWords.map { ( $0.range, $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY)) })
|
||||||
}
|
}
|
||||||
self.hasRTL = hasRTL
|
self.hasRTL = hasRTL
|
||||||
self.spoilers = spoilers
|
self.spoilers = spoilers
|
||||||
|
self.spoilerWords = spoilerWords
|
||||||
}
|
}
|
||||||
|
|
||||||
public func areLinesEqual(to other: TextNodeLayout) -> Bool {
|
public func areLinesEqual(to other: TextNodeLayout) -> Bool {
|
||||||
@ -952,6 +958,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
while true {
|
while true {
|
||||||
var strikethroughs: [TextNodeStrikethrough] = []
|
var strikethroughs: [TextNodeStrikethrough] = []
|
||||||
var spoilers: [TextNodeSpoiler] = []
|
var spoilers: [TextNodeSpoiler] = []
|
||||||
|
var spoilerWords: [TextNodeSpoiler] = []
|
||||||
|
|
||||||
var lineConstrainedWidth = constrainedSize.width
|
var lineConstrainedWidth = constrainedSize.width
|
||||||
var lineConstrainedWidthDelta: CGFloat = 0.0
|
var lineConstrainedWidthDelta: CGFloat = 0.0
|
||||||
@ -973,7 +980,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
|
|
||||||
let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth))
|
let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth))
|
||||||
|
|
||||||
func addSpoiler(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) {
|
func addSpoiler(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int) {
|
||||||
var secondaryLeftOffset: CGFloat = 0.0
|
var secondaryLeftOffset: CGFloat = 0.0
|
||||||
let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset)
|
let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset)
|
||||||
var leftOffset = floor(rawLeftOffset)
|
var leftOffset = floor(rawLeftOffset)
|
||||||
@ -988,7 +995,25 @@ public class TextNode: ASDisplayNode {
|
|||||||
rightOffset = ceil(secondaryRightOffset)
|
rightOffset = ceil(secondaryRightOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
spoilers.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)))
|
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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSpoilerWord(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) {
|
||||||
|
var secondaryLeftOffset: CGFloat = 0.0
|
||||||
|
let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset)
|
||||||
|
var leftOffset = floor(rawLeftOffset)
|
||||||
|
if !rawLeftOffset.isEqual(to: secondaryLeftOffset) {
|
||||||
|
leftOffset = floor(secondaryLeftOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondaryRightOffset: CGFloat = 0.0
|
||||||
|
let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset)
|
||||||
|
var rightOffset = ceil(rawRightOffset)
|
||||||
|
if !rawRightOffset.isEqual(to: secondaryRightOffset) {
|
||||||
|
rightOffset = ceil(secondaryRightOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
var isLastLine = false
|
var isLastLine = false
|
||||||
@ -1056,7 +1081,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
if let currentStartIndex = startIndex {
|
if let currentStartIndex = startIndex {
|
||||||
startIndex = nil
|
startIndex = nil
|
||||||
let endIndex = range.location
|
let endIndex = range.location
|
||||||
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex)
|
addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex)
|
||||||
}
|
}
|
||||||
} else if startIndex == nil {
|
} else if startIndex == nil {
|
||||||
startIndex = range.location
|
startIndex = range.location
|
||||||
@ -1067,8 +1092,10 @@ public class TextNode: ASDisplayNode {
|
|||||||
if let currentStartIndex = startIndex, let currentIndex = currentIndex {
|
if let currentStartIndex = startIndex, let currentIndex = currentIndex {
|
||||||
startIndex = nil
|
startIndex = nil
|
||||||
let endIndex = currentIndex
|
let endIndex = currentIndex
|
||||||
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0)
|
addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length - 1)
|
||||||
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
|
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
|
||||||
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
|
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
|
||||||
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
|
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
|
||||||
@ -1098,7 +1125,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers))
|
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords))
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
if lineCharacterCount > 0 {
|
if lineCharacterCount > 0 {
|
||||||
@ -1135,7 +1162,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
if let currentStartIndex = startIndex {
|
if let currentStartIndex = startIndex {
|
||||||
startIndex = nil
|
startIndex = nil
|
||||||
let endIndex = range.location
|
let endIndex = range.location
|
||||||
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex)
|
addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex)
|
||||||
}
|
}
|
||||||
} else if startIndex == nil {
|
} else if startIndex == nil {
|
||||||
startIndex = range.location
|
startIndex = range.location
|
||||||
@ -1146,8 +1173,10 @@ public class TextNode: ASDisplayNode {
|
|||||||
if let currentStartIndex = startIndex, let currentIndex = currentIndex {
|
if let currentStartIndex = startIndex, let currentIndex = currentIndex {
|
||||||
startIndex = nil
|
startIndex = nil
|
||||||
let endIndex = currentIndex
|
let endIndex = currentIndex
|
||||||
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex)
|
addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length - 1)
|
||||||
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
|
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
|
||||||
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
|
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
|
||||||
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
|
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
|
||||||
@ -1176,7 +1205,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers))
|
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords))
|
||||||
} else {
|
} else {
|
||||||
if !lines.isEmpty {
|
if !lines.isEmpty {
|
||||||
layoutSize.height += fontLineSpacing
|
layoutSize.height += fontLineSpacing
|
||||||
@ -1293,7 +1322,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
if layout.displaySpoilers && !line.spoilers.isEmpty {
|
if layout.displaySpoilers && !line.spoilers.isEmpty {
|
||||||
context.saveGState()
|
context.saveGState()
|
||||||
var clipRects: [CGRect] = []
|
var clipRects: [CGRect] = []
|
||||||
for spoiler in line.spoilers {
|
for spoiler in line.spoilerWords {
|
||||||
clipRects.append(spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY))
|
clipRects.append(spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY))
|
||||||
}
|
}
|
||||||
context.clip(to: clipRects)
|
context.clip(to: clipRects)
|
||||||
@ -1328,7 +1357,7 @@ public class TextNode: ASDisplayNode {
|
|||||||
if layout.displaySpoilers {
|
if layout.displaySpoilers {
|
||||||
context.restoreGState()
|
context.restoreGState()
|
||||||
} else {
|
} else {
|
||||||
for spoiler in line.spoilers {
|
for spoiler in line.spoilerWords {
|
||||||
clearRects.append(spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY))
|
clearRects.append(spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -737,7 +737,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
|||||||
|
|
||||||
}
|
}
|
||||||
if let dustNode = self.dustNode {
|
if let dustNode = self.dustNode {
|
||||||
dustNode.update(size: textFrame.size, color: .white, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) })
|
dustNode.update(size: textFrame.size, color: .white, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) })
|
||||||
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -45,7 +45,7 @@ private let emitterMaskImage: UIImage = {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
public class InvisibleInkDustNode: ASDisplayNode {
|
public class InvisibleInkDustNode: ASDisplayNode {
|
||||||
private var currentParams: (size: CGSize, color: UIColor, rects: [CGRect])?
|
private var currentParams: (size: CGSize, color: UIColor, rects: [CGRect], wordRects: [CGRect])?
|
||||||
|
|
||||||
private weak var textNode: TextNode?
|
private weak var textNode: TextNode?
|
||||||
private let textMaskNode: ASDisplayNode
|
private let textMaskNode: ASDisplayNode
|
||||||
@ -162,7 +162,7 @@ public class InvisibleInkDustNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) {
|
@objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||||
guard let (size, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else {
|
guard let (size, _, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,8 +192,6 @@ public class InvisibleInkDustNode: ASDisplayNode {
|
|||||||
self?.emitterNode.view.mask = nil
|
self?.emitterNode.view.mask = nil
|
||||||
})
|
})
|
||||||
self.emitterMaskFillNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
self.emitterMaskFillNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||||
|
|
||||||
self.isRevealedUpdated(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) {
|
Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) {
|
||||||
@ -209,7 +207,6 @@ public class InvisibleInkDustNode: ASDisplayNode {
|
|||||||
let timeToRead = min(45.0, ceil(max(4.0, textLength * 0.04)))
|
let timeToRead = min(45.0, ceil(max(4.0, textLength * 0.04)))
|
||||||
Queue.mainQueue().after(timeToRead * UIView.animationDurationFactor()) {
|
Queue.mainQueue().after(timeToRead * UIView.animationDurationFactor()) {
|
||||||
self.isRevealed = false
|
self.isRevealed = false
|
||||||
self.isRevealedUpdated(false)
|
|
||||||
|
|
||||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear)
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear)
|
||||||
transition.updateAlpha(node: self, alpha: 1.0)
|
transition.updateAlpha(node: self, alpha: 1.0)
|
||||||
@ -218,12 +215,12 @@ public class InvisibleInkDustNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateEmitter() {
|
private func updateEmitter() {
|
||||||
guard let (size, color, rects) = self.currentParams else {
|
guard let (size, color, _, wordRects) = self.currentParams else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.emitter?.color = color.cgColor
|
self.emitter?.color = color.cgColor
|
||||||
self.emitterLayer?.setValue(rects, forKey: "emitterRects")
|
self.emitterLayer?.setValue(wordRects, forKey: "emitterRects")
|
||||||
self.emitterLayer?.frame = CGRect(origin: CGPoint(), size: size)
|
self.emitterLayer?.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
|
||||||
let radius = max(size.width, size.height)
|
let radius = max(size.width, size.height)
|
||||||
@ -231,15 +228,15 @@ public class InvisibleInkDustNode: ASDisplayNode {
|
|||||||
self.emitterLayer?.setValue(radius * -0.5, forKeyPath: "emitterBehaviors.fingerAttractor.falloff")
|
self.emitterLayer?.setValue(radius * -0.5, forKeyPath: "emitterBehaviors.fingerAttractor.falloff")
|
||||||
|
|
||||||
var square: Float = 0.0
|
var square: Float = 0.0
|
||||||
for rect in rects {
|
for rect in wordRects {
|
||||||
square += Float(rect.width * rect.height)
|
square += Float(rect.width * rect.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.emitter?.birthRate = square * 0.4
|
self.emitter?.birthRate = square * 0.4
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update(size: CGSize, color: UIColor, rects: [CGRect]) {
|
public func update(size: CGSize, color: UIColor, rects: [CGRect], wordRects: [CGRect]) {
|
||||||
self.currentParams = (size, color, rects)
|
self.currentParams = (size, color, rects, wordRects)
|
||||||
|
|
||||||
self.emitterNode.frame = CGRect(origin: CGPoint(), size: size)
|
self.emitterNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
self.emitterMaskNode.frame = self.emitterNode.bounds
|
self.emitterMaskNode.frame = self.emitterNode.bounds
|
||||||
@ -252,7 +249,7 @@ public class InvisibleInkDustNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||||
if let (_, _, rects) = self.currentParams {
|
if let (_, _, rects, _) = self.currentParams {
|
||||||
for rect in rects {
|
for rect in rects {
|
||||||
if rect.contains(point) {
|
if rect.contains(point) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -92,6 +92,7 @@ swift_library(
|
|||||||
"//submodules/DebugSettingsUI:DebugSettingsUI",
|
"//submodules/DebugSettingsUI:DebugSettingsUI",
|
||||||
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
|
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
|
||||||
"//submodules/WebPBinding:WebPBinding",
|
"//submodules/WebPBinding:WebPBinding",
|
||||||
|
"//submodules/Translate:Translate",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import SearchBarNode
|
|||||||
import SearchUI
|
import SearchUI
|
||||||
import UndoUI
|
import UndoUI
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
|
import Translate
|
||||||
|
|
||||||
private enum LanguageListSection: ItemListSectionId {
|
private enum LanguageListSection: ItemListSectionId {
|
||||||
case translate
|
case translate
|
||||||
@ -432,8 +433,20 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
|
|||||||
var existingIds = Set<String>()
|
var existingIds = Set<String>()
|
||||||
|
|
||||||
var showTranslate = true
|
var showTranslate = true
|
||||||
|
var ignoredLanguages: [String] = []
|
||||||
if let translationSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) {
|
if let translationSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) {
|
||||||
showTranslate = translationSettings.showTranslate
|
showTranslate = translationSettings.showTranslate
|
||||||
|
if let languages = translationSettings.ignoredLanguages {
|
||||||
|
ignoredLanguages = languages
|
||||||
|
} else {
|
||||||
|
if let activeLanguageCode = activeLanguageCode, supportedTranslationLanguages.contains(activeLanguageCode) {
|
||||||
|
ignoredLanguages = [activeLanguageCode]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let activeLanguageCode = activeLanguageCode, supportedTranslationLanguages.contains(activeLanguageCode) {
|
||||||
|
ignoredLanguages = [activeLanguageCode]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let localizationListState = (view.views[preferencesKey] as? PreferencesView)?.values[PreferencesKeys.localizationListState]?.get(LocalizationListState.self)
|
let localizationListState = (view.views[preferencesKey] as? PreferencesView)?.values[PreferencesKeys.localizationListState]?.get(LocalizationListState.self)
|
||||||
@ -444,8 +457,18 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
|
|||||||
entries.append(.translateTitle(text: presentationData.strings.Localization_TranslateMessages.uppercased()))
|
entries.append(.translateTitle(text: presentationData.strings.Localization_TranslateMessages.uppercased()))
|
||||||
entries.append(.translate(text: presentationData.strings.Localization_ShowTranslate, value: showTranslate))
|
entries.append(.translate(text: presentationData.strings.Localization_ShowTranslate, value: showTranslate))
|
||||||
if showTranslate {
|
if showTranslate {
|
||||||
entries.append(.doNotTranslate(text: presentationData.strings.Localization_DoNotTranslate, value: ""))
|
var value = ""
|
||||||
entries.append(.translateInfo(text: presentationData.strings.Localization_DoNotTranslateInfo))
|
if ignoredLanguages.count > 1 {
|
||||||
|
value = ignoredLanguages.joined(separator: ", ")
|
||||||
|
} else if let code = ignoredLanguages.first {
|
||||||
|
let enLocale = Locale(identifier: "en")
|
||||||
|
if let title = enLocale.localizedString(forLanguageCode: code) {
|
||||||
|
value = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.append(.doNotTranslate(text: presentationData.strings.Localization_DoNotTranslate, value: value))
|
||||||
|
entries.append(.translateInfo(text: ignoredLanguages.count > 1 ? presentationData.strings.Localization_DoNotTranslateManyInfo : presentationData.strings.Localization_DoNotTranslateInfo))
|
||||||
} else {
|
} else {
|
||||||
entries.append(.translateInfo(text: presentationData.strings.Localization_ShowTranslateInfo))
|
entries.append(.translateInfo(text: presentationData.strings.Localization_ShowTranslateInfo))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9798,7 +9798,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
|
|
||||||
private func getCaptionPanelView() -> TGCaptionPanelView {
|
private func getCaptionPanelView() -> TGCaptionPanelView {
|
||||||
let presentationData = self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
let presentationData = self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||||
var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil)
|
var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: self.presentationInterfaceState.chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil)
|
||||||
|
|
||||||
var updateChatPresentationInterfaceStateImpl: (((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void)?
|
var updateChatPresentationInterfaceStateImpl: (((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void)?
|
||||||
var ensureFocusedImpl: (() -> Void)?
|
var ensureFocusedImpl: (() -> Void)?
|
||||||
|
|||||||
@ -82,7 +82,11 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: LimitsCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let author = message.author, message.author?.id != message.id.peerId, author.id.namespace == Namespaces.Peer.CloudChannel && message.id.peerId.namespace == Namespaces.Peer.CloudChannel, !message.flags.contains(.Incoming) {
|
} else if let author = message.author, message.author?.id != message.id.peerId, author.id.namespace == Namespaces.Peer.CloudChannel && message.id.peerId.namespace == Namespaces.Peer.CloudChannel, !message.flags.contains(.Incoming) {
|
||||||
hasEditRights = true
|
if message.media.contains(where: { $0 is TelegramMediaInvoice }) {
|
||||||
|
hasEditRights = false
|
||||||
|
} else {
|
||||||
|
hasEditRights = true
|
||||||
|
}
|
||||||
} else if message.author?.id == message.id.peerId, let peer = message.peers[message.id.peerId] {
|
} else if message.author?.id == message.id.peerId, let peer = message.peers[message.id.peerId] {
|
||||||
if let peer = peer as? TelegramChannel {
|
if let peer = peer as? TelegramChannel {
|
||||||
switch peer.info {
|
switch peer.info {
|
||||||
@ -708,7 +712,19 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
resourceAvailable = false
|
resourceAvailable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!messages[0].text.isEmpty || resourceAvailable || diceEmoji != nil) && !chatPresentationInterfaceState.copyProtectionEnabled {
|
var messageText: String = ""
|
||||||
|
for message in messages {
|
||||||
|
if !message.text.isEmpty {
|
||||||
|
if messageText.isEmpty {
|
||||||
|
messageText = message.text
|
||||||
|
} else {
|
||||||
|
messageText = ""
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messageText.isEmpty || resourceAvailable || diceEmoji != nil) && !chatPresentationInterfaceState.copyProtectionEnabled {
|
||||||
let message = messages[0]
|
let message = messages[0]
|
||||||
var isExpired = false
|
var isExpired = false
|
||||||
for media in message.media {
|
for media in message.media {
|
||||||
@ -738,7 +754,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
if let restrictedText = restrictedText {
|
if let restrictedText = restrictedText {
|
||||||
storeMessageTextInPasteboard(restrictedText, entities: nil)
|
storeMessageTextInPasteboard(restrictedText, entities: nil)
|
||||||
} else {
|
} else {
|
||||||
storeMessageTextInPasteboard(message.text, entities: messageEntities)
|
storeMessageTextInPasteboard(messageText, entities: messageEntities)
|
||||||
}
|
}
|
||||||
|
|
||||||
Queue.mainQueue().after(0.2, {
|
Queue.mainQueue().after(0.2, {
|
||||||
@ -754,7 +770,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
|> deliverOnMainQueue).start(next: { data in
|
|> deliverOnMainQueue).start(next: { data in
|
||||||
if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
||||||
if let image = UIImage(data: imageData) {
|
if let image = UIImage(data: imageData) {
|
||||||
if !message.text.isEmpty {
|
if !messageText.isEmpty {
|
||||||
copyTextWithEntities()
|
copyTextWithEntities()
|
||||||
} else {
|
} else {
|
||||||
UIPasteboard.general.image = image
|
UIPasteboard.general.image = image
|
||||||
@ -780,20 +796,20 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
f(.default)
|
f(.default)
|
||||||
})))
|
})))
|
||||||
|
|
||||||
if canTranslateText(context: context, text: message.text, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages) {
|
if canTranslateText(context: context, text: messageText, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages) {
|
||||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in
|
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor)
|
||||||
}, action: { _, f in
|
}, action: { _, f in
|
||||||
controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: message.text), .translate)
|
controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: messageText), .translate)
|
||||||
f(.default)
|
f(.default)
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSpeakSelectionEnabled() && !message.text.isEmpty {
|
if isSpeakSelectionEnabled() && !messageText.isEmpty {
|
||||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuSpeak, icon: { theme in
|
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuSpeak, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Message"), color: theme.actionSheet.primaryTextColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Message"), color: theme.actionSheet.primaryTextColor)
|
||||||
}, action: { _, f in
|
}, action: { _, f in
|
||||||
controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: message.text), .speak)
|
controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: messageText), .speak)
|
||||||
f(.default)
|
f(.default)
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -410,7 +410,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
|||||||
self.insertSubnode(dustNode, aboveSubnode: self.textNode)
|
self.insertSubnode(dustNode, aboveSubnode: self.textNode)
|
||||||
}
|
}
|
||||||
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
||||||
dustNode.update(size: dustNode.frame.size, color: presentationData.theme.inAppNotification.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
dustNode.update(size: dustNode.frame.size, color: presentationData.theme.inAppNotification.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
||||||
} else if let dustNode = self.dustNode {
|
} else if let dustNode = self.dustNode {
|
||||||
dustNode.removeFromSupernode()
|
dustNode.removeFromSupernode()
|
||||||
self.dustNode = nil
|
self.dustNode = nil
|
||||||
|
|||||||
@ -254,7 +254,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
|||||||
node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode)
|
node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode)
|
||||||
}
|
}
|
||||||
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
||||||
dustNode.update(size: dustNode.frame.size, color: dustColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
dustNode.update(size: dustNode.frame.size, color: dustColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
||||||
} else if let dustNode = node.dustNode {
|
} else if let dustNode = node.dustNode {
|
||||||
dustNode.removeFromSupernode()
|
dustNode.removeFromSupernode()
|
||||||
node.dustNode = nil
|
node.dustNode = nil
|
||||||
|
|||||||
@ -405,7 +405,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode)
|
strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode)
|
||||||
}
|
}
|
||||||
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
||||||
dustNode.update(size: dustNode.frame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
dustNode.update(size: dustNode.frame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
||||||
} else if let spoilerTextNode = strongSelf.spoilerTextNode {
|
} else if let spoilerTextNode = strongSelf.spoilerTextNode {
|
||||||
strongSelf.spoilerTextNode = nil
|
strongSelf.spoilerTextNode = nil
|
||||||
spoilerTextNode.removeFromSupernode()
|
spoilerTextNode.removeFromSupernode()
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import AnimatedCountLabelNode
|
|||||||
import AnimatedNavigationStripeNode
|
import AnimatedNavigationStripeNode
|
||||||
import ContextUI
|
import ContextUI
|
||||||
import RadialStatusNode
|
import RadialStatusNode
|
||||||
|
import InvisibleInkDustNode
|
||||||
|
import TextFormat
|
||||||
|
|
||||||
private enum PinnedMessageAnimation {
|
private enum PinnedMessageAnimation {
|
||||||
case slideToTop
|
case slideToTop
|
||||||
@ -50,6 +52,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
private let lineNode: AnimatedNavigationStripeNode
|
private let lineNode: AnimatedNavigationStripeNode
|
||||||
private let titleNode: AnimatedCountLabelNode
|
private let titleNode: AnimatedCountLabelNode
|
||||||
private let textNode: TextNode
|
private let textNode: TextNode
|
||||||
|
private var dustNode: InvisibleInkDustNode?
|
||||||
|
|
||||||
private let imageNode: TransformImageNode
|
private let imageNode: TransformImageNode
|
||||||
private let imageNodeContainer: ASDisplayNode
|
private let imageNodeContainer: ASDisplayNode
|
||||||
@ -451,7 +454,25 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
}
|
}
|
||||||
let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), titleStrings)
|
let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), titleStrings)
|
||||||
|
|
||||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: foldLineBreaks(descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId).0), font: Font.regular(15.0), textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)))
|
let (textString, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId)
|
||||||
|
|
||||||
|
let messageText: NSAttributedString
|
||||||
|
let textFont = Font.regular(15.0)
|
||||||
|
if isText {
|
||||||
|
let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in
|
||||||
|
if case .Spoiler = entity.type {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let textColor = theme.chat.inputPanel.primaryTextColor
|
||||||
|
messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||||
|
} else {
|
||||||
|
messageText = NSAttributedString(string: foldLineBreaks(textString), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)))
|
||||||
|
|
||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
@ -463,7 +484,26 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
animationTransition.updateFrameAdditive(node: strongSelf.contentTextContainer, frame: CGRect(origin: CGPoint(x: contentLeftInset + textLineInset, y: 0.0), size: CGSize()))
|
animationTransition.updateFrameAdditive(node: strongSelf.contentTextContainer, frame: CGRect(origin: CGPoint(x: contentLeftInset + textLineInset, y: 0.0), size: CGSize()))
|
||||||
|
|
||||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 5.0), size: titleLayout.size)
|
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 5.0), size: titleLayout.size)
|
||||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 23.0), size: textLayout.size)
|
|
||||||
|
let textFrame = CGRect(origin: CGPoint(x: 0.0, y: 23.0), size: textLayout.size)
|
||||||
|
strongSelf.textNode.frame = textFrame
|
||||||
|
|
||||||
|
if !textLayout.spoilers.isEmpty {
|
||||||
|
let dustNode: InvisibleInkDustNode
|
||||||
|
if let current = strongSelf.dustNode {
|
||||||
|
dustNode = current
|
||||||
|
} else {
|
||||||
|
dustNode = InvisibleInkDustNode(textNode: nil)
|
||||||
|
dustNode.isUserInteractionEnabled = false
|
||||||
|
strongSelf.dustNode = dustNode
|
||||||
|
strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode)
|
||||||
|
}
|
||||||
|
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
||||||
|
dustNode.update(size: dustNode.frame.size, color: theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
||||||
|
} else if let dustNode = strongSelf.dustNode {
|
||||||
|
dustNode.removeFromSupernode()
|
||||||
|
strongSelf.dustNode = nil
|
||||||
|
}
|
||||||
|
|
||||||
let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight))
|
let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight))
|
||||||
animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame)
|
animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame)
|
||||||
|
|||||||
@ -17,6 +17,8 @@ final class ChatTextInputMenu {
|
|||||||
private var stringUnderline: String = "Underline"
|
private var stringUnderline: String = "Underline"
|
||||||
private var stringSpoiler: String = "Spoiler"
|
private var stringSpoiler: String = "Spoiler"
|
||||||
|
|
||||||
|
private let hasSpoilers: Bool
|
||||||
|
|
||||||
private(set) var state: ChatTextInputMenuState = .inactive {
|
private(set) var state: ChatTextInputMenuState = .inactive {
|
||||||
didSet {
|
didSet {
|
||||||
if self.state != oldValue {
|
if self.state != oldValue {
|
||||||
@ -26,15 +28,18 @@ final class ChatTextInputMenu {
|
|||||||
case .general:
|
case .general:
|
||||||
UIMenuController.shared.menuItems = []
|
UIMenuController.shared.menuItems = []
|
||||||
case .format:
|
case .format:
|
||||||
UIMenuController.shared.menuItems = [
|
var menuItems: [UIMenuItem] = [
|
||||||
UIMenuItem(title: self.stringBold, action: Selector(("formatAttributesBold:"))),
|
UIMenuItem(title: self.stringBold, action: Selector(("formatAttributesBold:"))),
|
||||||
UIMenuItem(title: self.stringItalic, action: Selector(("formatAttributesItalic:"))),
|
UIMenuItem(title: self.stringItalic, action: Selector(("formatAttributesItalic:"))),
|
||||||
UIMenuItem(title: self.stringMonospace, action: Selector(("formatAttributesMonospace:"))),
|
UIMenuItem(title: self.stringMonospace, action: Selector(("formatAttributesMonospace:"))),
|
||||||
UIMenuItem(title: self.stringLink, action: Selector(("formatAttributesLink:"))),
|
UIMenuItem(title: self.stringLink, action: Selector(("formatAttributesLink:"))),
|
||||||
UIMenuItem(title: self.stringSpoiler, action: Selector(("formatAttributesSpoiler:"))),
|
|
||||||
UIMenuItem(title: self.stringStrikethrough, action: Selector(("formatAttributesStrikethrough:"))),
|
UIMenuItem(title: self.stringStrikethrough, action: Selector(("formatAttributesStrikethrough:"))),
|
||||||
UIMenuItem(title: self.stringUnderline, action: Selector(("formatAttributesUnderline:")))
|
UIMenuItem(title: self.stringUnderline, action: Selector(("formatAttributesUnderline:")))
|
||||||
]
|
]
|
||||||
|
if self.hasSpoilers {
|
||||||
|
menuItems.insert(UIMenuItem(title: self.stringSpoiler, action: Selector(("formatAttributesSpoiler:"))), at: 0)
|
||||||
|
}
|
||||||
|
UIMenuController.shared.menuItems = menuItems
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -43,7 +48,8 @@ final class ChatTextInputMenu {
|
|||||||
|
|
||||||
private var observer: NSObjectProtocol?
|
private var observer: NSObjectProtocol?
|
||||||
|
|
||||||
init() {
|
init(hasSpoilers: Bool = false) {
|
||||||
|
self.hasSpoilers = hasSpoilers
|
||||||
self.observer = NotificationCenter.default.addObserver(forName: UIMenuController.didHideMenuNotification, object: nil, queue: nil, using: { [weak self] _ in
|
self.observer = NotificationCenter.default.addObserver(forName: UIMenuController.didHideMenuNotification, object: nil, queue: nil, using: { [weak self] _ in
|
||||||
self?.back()
|
self?.back()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -296,7 +296,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
|
|
||||||
var isMediaDeleted: Bool = false
|
var isMediaDeleted: Bool = false
|
||||||
|
|
||||||
private let inputMenu = ChatTextInputMenu()
|
private let inputMenu: ChatTextInputMenu
|
||||||
|
|
||||||
private var theme: PresentationTheme?
|
private var theme: PresentationTheme?
|
||||||
private var strings: PresentationStrings?
|
private var strings: PresentationStrings?
|
||||||
@ -448,6 +448,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
init(presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
|
init(presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
|
||||||
self.presentationInterfaceState = presentationInterfaceState
|
self.presentationInterfaceState = presentationInterfaceState
|
||||||
|
|
||||||
|
var hasSpoilers = true
|
||||||
|
if presentationInterfaceState.chatLocation.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||||
|
hasSpoilers = false
|
||||||
|
}
|
||||||
|
self.inputMenu = ChatTextInputMenu(hasSpoilers: hasSpoilers)
|
||||||
|
|
||||||
self.clippingNode = ASDisplayNode()
|
self.clippingNode = ASDisplayNode()
|
||||||
self.clippingNode.clipsToBounds = true
|
self.clippingNode.clipsToBounds = true
|
||||||
|
|
||||||
@ -713,6 +719,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom))
|
textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom))
|
||||||
|
textInputNode.view.layoutIfNeeded()
|
||||||
|
self.updateSpoiler()
|
||||||
}
|
}
|
||||||
|
|
||||||
self.textInputBackgroundNode.isUserInteractionEnabled = false
|
self.textInputBackgroundNode.isUserInteractionEnabled = false
|
||||||
@ -1793,10 +1801,145 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateSpoiler() {
|
private func updateSpoiler() {
|
||||||
|
guard let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
||||||
|
|
||||||
|
var rects: [CGRect] = []
|
||||||
|
|
||||||
|
if let attributedText = textInputNode.attributedText {
|
||||||
|
let beginning = textInputNode.textView.beginningOfDocument
|
||||||
|
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
|
||||||
|
if let _ = attributes[ChatTextInputAttributes.spoiler] {
|
||||||
|
func addSpoiler(startIndex: Int, endIndex: Int) {
|
||||||
|
if let start = textInputNode.textView.position(from: beginning, offset: startIndex), let end = textInputNode.textView.position(from: start, offset: endIndex - startIndex), let textRange = textInputNode.textView.textRange(from: start, to: end) {
|
||||||
|
let textRects = textInputNode.textView.selectionRects(for: textRange)
|
||||||
|
for textRect in textRects {
|
||||||
|
rects.append(textRect.rect.insetBy(dx: 1.0, dy: 1.0).offsetBy(dx: 0.0, dy: 1.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var startIndex: Int?
|
||||||
|
var currentIndex: Int?
|
||||||
|
|
||||||
|
let nsString = (attributedText.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
|
||||||
|
addSpoiler(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
|
||||||
|
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rects.isEmpty {
|
||||||
|
let dustNode: InvisibleInkDustNode
|
||||||
|
if let current = self.dustNode {
|
||||||
|
dustNode = current
|
||||||
|
} else {
|
||||||
|
dustNode = InvisibleInkDustNode(textNode: nil)
|
||||||
|
dustNode.alpha = self.spoilersRevealed ? 0.0 : 1.0
|
||||||
|
dustNode.isUserInteractionEnabled = false
|
||||||
|
textInputNode.textView.addSubview(dustNode.view)
|
||||||
|
self.dustNode = dustNode
|
||||||
|
}
|
||||||
|
dustNode.frame = CGRect(origin: CGPoint(), size: textInputNode.textView.contentSize)
|
||||||
|
dustNode.update(size: textInputNode.textView.contentSize, color: textColor, rects: rects, wordRects: rects)
|
||||||
|
} else if let dustNode = self.dustNode {
|
||||||
|
dustNode.removeFromSupernode()
|
||||||
|
self.dustNode = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateSpoilersRevealed(animated: Bool = true) {
|
||||||
guard let textInputNode = self.textInputNode else {
|
guard let textInputNode = self.textInputNode else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
print(textInputNode.attributedText?.description ?? "")
|
|
||||||
|
let selectionRange = textInputNode.textView.selectedRange
|
||||||
|
|
||||||
|
var revealed = false
|
||||||
|
if let attributedText = textInputNode.attributedText {
|
||||||
|
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
|
||||||
|
if let _ = attributes[ChatTextInputAttributes.spoiler] {
|
||||||
|
if let _ = selectionRange.intersection(range) {
|
||||||
|
revealed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
guard self.spoilersRevealed != revealed else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.spoilersRevealed = revealed
|
||||||
|
|
||||||
|
if revealed {
|
||||||
|
self.updateInternalSpoilersRevealed(true, animated: animated)
|
||||||
|
} else {
|
||||||
|
Queue.mainQueue().after(1.5, {
|
||||||
|
self.updateInternalSpoilersRevealed(false, animated: true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateInternalSpoilersRevealed(_ revealed: Bool, animated: Bool) {
|
||||||
|
guard self.spoilersRevealed == revealed, let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
||||||
|
let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
|
||||||
|
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
||||||
|
|
||||||
|
refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed)
|
||||||
|
|
||||||
|
textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed)
|
||||||
|
|
||||||
|
if textInputNode.textView.subviews.count > 1, animated {
|
||||||
|
let containerView = textInputNode.textView.subviews[1]
|
||||||
|
if let canvasView = containerView.subviews.first {
|
||||||
|
if let snapshotView = canvasView.snapshotView(afterScreenUpdates: false) {
|
||||||
|
textInputNode.view.insertSubview(snapshotView, at: 0)
|
||||||
|
canvasView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||||
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||||
|
snapshotView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if animated {
|
||||||
|
if revealed {
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
|
||||||
|
if let dustNode = self.dustNode {
|
||||||
|
transition.updateAlpha(node: dustNode, alpha: 0.0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
|
||||||
|
if let dustNode = self.dustNode {
|
||||||
|
transition.updateAlpha(node: dustNode, alpha: 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let dustNode = self.dustNode {
|
||||||
|
dustNode.alpha = revealed ? 0.0 : 1.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateCounterTextNode(transition: ContainedViewLayoutTransition) {
|
private func updateCounterTextNode(transition: ContainedViewLayoutTransition) {
|
||||||
@ -2069,6 +2212,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
|
|
||||||
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
||||||
refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
|
refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
|
||||||
|
|
||||||
|
self.updateSpoilersRevealed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2190,9 +2335,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
|
|
||||||
@objc func formatAttributesSpoiler(_ sender: Any) {
|
@objc func formatAttributesSpoiler(_ sender: Any) {
|
||||||
self.inputMenu.back()
|
self.inputMenu.back()
|
||||||
|
|
||||||
|
var animated = false
|
||||||
|
if let attributedText = self.textInputNode?.attributedText {
|
||||||
|
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in
|
||||||
|
if let _ = attributes[ChatTextInputAttributes.spoiler] {
|
||||||
|
animated = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||||||
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.spoiler), inputMode)
|
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.spoiler), inputMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.updateSpoilersRevealed(animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
@objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||||
|
|||||||
@ -279,7 +279,7 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
|
|||||||
textColorValue = presentationData.theme.list.itemAccentColor
|
textColorValue = presentationData.theme.list.itemAccentColor
|
||||||
}
|
}
|
||||||
|
|
||||||
self.expandNode.attributedText = NSAttributedString(string: presentationData.strings.PeerInfo_BioExpand.uppercased(), font: Font.medium(15.0), textColor: presentationData.theme.list.itemAccentColor)
|
self.expandNode.attributedText = NSAttributedString(string: presentationData.strings.PeerInfo_BioExpand, font: Font.medium(15.0), textColor: presentationData.theme.list.itemAccentColor)
|
||||||
let expandSize = self.expandNode.updateLayout(CGSize(width: width, height: 100.0))
|
let expandSize = self.expandNode.updateLayout(CGSize(width: width, height: 100.0))
|
||||||
|
|
||||||
self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
|
||||||
|
|||||||
@ -141,7 +141,7 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A
|
|||||||
private var presentationInterfaceState: ChatPresentationInterfaceState?
|
private var presentationInterfaceState: ChatPresentationInterfaceState?
|
||||||
private var initializedPlaceholder = false
|
private var initializedPlaceholder = false
|
||||||
|
|
||||||
private let inputMenu = ChatTextInputMenu()
|
private let inputMenu: ChatTextInputMenu
|
||||||
|
|
||||||
private var theme: PresentationTheme?
|
private var theme: PresentationTheme?
|
||||||
private var strings: PresentationStrings?
|
private var strings: PresentationStrings?
|
||||||
@ -241,6 +241,12 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A
|
|||||||
self.presentationInterfaceState = presentationInterfaceState
|
self.presentationInterfaceState = presentationInterfaceState
|
||||||
self.isCaption = isCaption
|
self.isCaption = isCaption
|
||||||
|
|
||||||
|
var hasSpoilers = true
|
||||||
|
if presentationInterfaceState.chatLocation.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||||
|
hasSpoilers = false
|
||||||
|
}
|
||||||
|
self.inputMenu = ChatTextInputMenu(hasSpoilers: hasSpoilers)
|
||||||
|
|
||||||
self.textInputContainerBackgroundNode = ASImageNode()
|
self.textInputContainerBackgroundNode = ASImageNode()
|
||||||
self.textInputContainerBackgroundNode.isUserInteractionEnabled = false
|
self.textInputContainerBackgroundNode.isUserInteractionEnabled = false
|
||||||
self.textInputContainerBackgroundNode.displaysAsynchronously = false
|
self.textInputContainerBackgroundNode.displaysAsynchronously = false
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import AccountContext
|
|||||||
import LocalizedPeerData
|
import LocalizedPeerData
|
||||||
import PhotoResources
|
import PhotoResources
|
||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
|
import InvisibleInkDustNode
|
||||||
|
import TextFormat
|
||||||
|
|
||||||
final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
||||||
private let messageDisposable = MetaDisposable()
|
private let messageDisposable = MetaDisposable()
|
||||||
@ -23,6 +25,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
|||||||
let iconNode: ASImageNode
|
let iconNode: ASImageNode
|
||||||
let titleNode: ImmediateTextNode
|
let titleNode: ImmediateTextNode
|
||||||
let textNode: ImmediateTextNode
|
let textNode: ImmediateTextNode
|
||||||
|
var dustNode: InvisibleInkDustNode?
|
||||||
let imageNode: TransformImageNode
|
let imageNode: TransformImageNode
|
||||||
|
|
||||||
private let actionArea: AccessibilityAreaNode
|
private let actionArea: AccessibilityAreaNode
|
||||||
@ -96,6 +99,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
|||||||
|
|
||||||
var authorName = ""
|
var authorName = ""
|
||||||
var text = ""
|
var text = ""
|
||||||
|
var isText = true
|
||||||
if let forwardInfo = message?.forwardInfo, forwardInfo.flags.contains(.isImported) {
|
if let forwardInfo = message?.forwardInfo, forwardInfo.flags.contains(.isImported) {
|
||||||
if let author = forwardInfo.author {
|
if let author = forwardInfo.author {
|
||||||
authorName = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
|
authorName = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
|
||||||
@ -105,8 +109,34 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
|||||||
} else if let author = message?.effectiveAuthor {
|
} else if let author = message?.effectiveAuthor {
|
||||||
authorName = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
|
authorName = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isMedia: Bool
|
||||||
if let message = message {
|
if let message = message {
|
||||||
(text, _, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId)
|
switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) {
|
||||||
|
case .text:
|
||||||
|
isMedia = false
|
||||||
|
default:
|
||||||
|
isMedia = true
|
||||||
|
}
|
||||||
|
(text, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId)
|
||||||
|
} else {
|
||||||
|
isMedia = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let textFont = Font.regular(14.0)
|
||||||
|
let messageText: NSAttributedString
|
||||||
|
if isText, let message = message {
|
||||||
|
let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in
|
||||||
|
if case .Spoiler = entity.type {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor
|
||||||
|
messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false)
|
||||||
|
} else {
|
||||||
|
messageText = NSAttributedString(string: text, font: textFont, textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedMediaReference: AnyMediaReference?
|
var updatedMediaReference: AnyMediaReference?
|
||||||
@ -169,22 +199,10 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
|||||||
updateImageSignal = .single({ _ in return nil })
|
updateImageSignal = .single({ _ in return nil })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let isMedia: Bool
|
|
||||||
if let message = message {
|
|
||||||
switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) {
|
|
||||||
case .text:
|
|
||||||
isMedia = false
|
|
||||||
default:
|
|
||||||
isMedia = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isMedia = false
|
|
||||||
}
|
|
||||||
|
|
||||||
strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string, font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor)
|
strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string, font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor)
|
||||||
strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor)
|
strongSelf.textNode.attributedText = messageText
|
||||||
|
|
||||||
let headerString: String
|
let headerString: String
|
||||||
if let message = message, message.flags.contains(.Incoming), let author = message.author {
|
if let message = message, message.flags.contains(.Incoming), let author = message.author {
|
||||||
headerString = "Reply to message. From: \(EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder))"
|
headerString = "Reply to message. From: \(EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder))"
|
||||||
@ -295,8 +313,25 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height))
|
let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height))
|
||||||
|
let textFrame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset - self.textNode.insets.left, y: 25.0 - self.textNode.insets.top), size: textSize)
|
||||||
if self.textNode.supernode == self {
|
if self.textNode.supernode == self {
|
||||||
self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset - self.textNode.insets.left, y: 25.0 - self.textNode.insets.top), size: textSize)
|
self.textNode.frame = textFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
if let textLayout = self.textNode.cachedLayout, !textLayout.spoilers.isEmpty {
|
||||||
|
if self.dustNode == nil {
|
||||||
|
let dustNode = InvisibleInkDustNode(textNode: nil)
|
||||||
|
self.dustNode = dustNode
|
||||||
|
self.textNode.supernode?.insertSubnode(dustNode, aboveSubnode: self.textNode)
|
||||||
|
|
||||||
|
}
|
||||||
|
if let dustNode = self.dustNode {
|
||||||
|
dustNode.update(size: textFrame.size, color: self.theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
||||||
|
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
||||||
|
}
|
||||||
|
} else if let dustNode = self.dustNode {
|
||||||
|
self.dustNode = nil
|
||||||
|
dustNode.removeFromSupernode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -86,7 +86,11 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo
|
|||||||
result.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
|
result.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
|
||||||
} else if key == ChatTextInputAttributes.spoiler {
|
} else if key == ChatTextInputAttributes.spoiler {
|
||||||
result.addAttribute(key, value: value, range: range)
|
result.addAttribute(key, value: value, range: range)
|
||||||
result.addAttribute(NSAttributedString.Key.backgroundColor, value: textColor.withAlphaComponent(0.15), range: range)
|
if spoilersRevealed {
|
||||||
|
result.addAttribute(NSAttributedString.Key.backgroundColor, value: textColor.withAlphaComponent(0.15), range: range)
|
||||||
|
} else {
|
||||||
|
result.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -472,7 +476,11 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme
|
|||||||
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
|
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
|
||||||
} else if key == ChatTextInputAttributes.spoiler {
|
} else if key == ChatTextInputAttributes.spoiler {
|
||||||
textNode.textView.textStorage.addAttribute(key, value: value, range: range)
|
textNode.textView.textStorage.addAttribute(key, value: value, range: range)
|
||||||
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range)
|
if spoilersRevealed {
|
||||||
|
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range)
|
||||||
|
} else {
|
||||||
|
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -564,7 +572,11 @@ public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, th
|
|||||||
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
|
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range)
|
||||||
} else if key == ChatTextInputAttributes.spoiler {
|
} else if key == ChatTextInputAttributes.spoiler {
|
||||||
textNode.textView.textStorage.addAttribute(key, value: value, range: range)
|
textNode.textView.textStorage.addAttribute(key, value: value, range: range)
|
||||||
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: UIColor.clear, range: range)
|
if spoilersRevealed {
|
||||||
|
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range)
|
||||||
|
} else {
|
||||||
|
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user