mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
e0f4655cc5
871 changed files with 275897 additions and 64193 deletions
|
|
@ -630,7 +630,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
collect(blocks: blocks)
|
||||
case let .list(items, _):
|
||||
for item in items {
|
||||
if case let .blocks(blocks, _) = item {
|
||||
if case let .blocks(blocks, _, _) = item {
|
||||
collect(blocks: blocks)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ private let markdownInlineHTMLInlineIntent = InlinePresentationIntent(rawValue:
|
|||
private let markdownDefaultBlockImageDimensions = PixelDimensions(width: 1200, height: 900)
|
||||
private let markdownDefaultInlineImageDimensions = PixelDimensions(width: 18, height: 18)
|
||||
private let markdownImageParsingEnabled = false
|
||||
private let markdownTaskListUncheckedNumber = "\u{001f}tg-md-task:unchecked"
|
||||
private let markdownTaskListCheckedNumber = "\u{001f}tg-md-task:checked"
|
||||
private let markdownRawHTMLTagRegex = try! NSRegularExpression(pattern: #"</?([A-Za-z][A-Za-z0-9:-]*)\b[^>]*?>"#)
|
||||
private let markdownFormulaPlaceholderRegex = try! NSRegularExpression(pattern: #"TGMDMATH\d+TGMD"#)
|
||||
private let markdownVoidHTMLTags: Set<String> = [
|
||||
|
|
@ -200,7 +198,7 @@ private final class MarkdownConversionBudget {
|
|||
guard self.registerBlockDepth(depth) else {
|
||||
return false
|
||||
}
|
||||
if case let .blocks(blocks, _) = listItem {
|
||||
if case let .blocks(blocks, _, _) = listItem {
|
||||
return self.validate(blocks: blocks, depth: depth + 1, count: &count)
|
||||
} else {
|
||||
return true
|
||||
|
|
@ -333,7 +331,7 @@ private enum MarkdownTaskListState {
|
|||
|
||||
private final class MarkdownConversionContext {
|
||||
private let context: AccountContext
|
||||
fileprivate let documentURL: URL
|
||||
fileprivate let documentURL: URL?
|
||||
fileprivate let formulasByPlaceholder: [String: MarkdownFormulaDescriptor]
|
||||
fileprivate let budget: MarkdownConversionBudget
|
||||
private var nextRemoteMediaId: Int64 = 0
|
||||
|
|
@ -341,7 +339,7 @@ private final class MarkdownConversionContext {
|
|||
|
||||
private(set) var media: [EngineMedia.Id: EngineRawMedia] = [:]
|
||||
|
||||
init(context: AccountContext, documentURL: URL, formulasByPlaceholder: [String: MarkdownFormulaDescriptor], budget: MarkdownConversionBudget) {
|
||||
init(context: AccountContext, documentURL: URL?, formulasByPlaceholder: [String: MarkdownFormulaDescriptor], budget: MarkdownConversionBudget) {
|
||||
self.context = context
|
||||
self.documentURL = documentURL
|
||||
self.formulasByPlaceholder = formulasByPlaceholder
|
||||
|
|
@ -748,6 +746,11 @@ private func markdownBlockFormulaReplacement(
|
|||
}
|
||||
|
||||
let openerContent = String(trimmedStart.dropFirst(opener.count))
|
||||
// Block opener must be exactly `$$` (not `$$$`); a `$$$…` line is not a block and
|
||||
// falls through to inline handling.
|
||||
if opener == "$$", openerContent.first == "$" {
|
||||
return nil
|
||||
}
|
||||
var latex = ""
|
||||
|
||||
if let closeRange = markdownFirstUnescapedRange(of: closer, in: openerContent, from: openerContent.startIndex) {
|
||||
|
|
@ -770,6 +773,11 @@ private func markdownBlockFormulaReplacement(
|
|||
return (indentation + descriptor.placeholder + ending, startLineIndex + 1, descriptor)
|
||||
}
|
||||
|
||||
// Reached only when there is no closer on the opener line (multi-line form).
|
||||
// A multi-line block opener must be a bare `$$` line; `$$ content` is not a block.
|
||||
if opener == "$$", !openerContent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return nil
|
||||
}
|
||||
let (_, openerEnding) = markdownLineContentAndEnding(lines[startLineIndex])
|
||||
latex.append(contentsOf: openerContent)
|
||||
latex.append(contentsOf: openerEnding)
|
||||
|
|
@ -862,29 +870,47 @@ private func markdownReplacingInlineFormulas(
|
|||
}
|
||||
|
||||
if content[index] == "$", !markdownIsEscaped(content, at: index) {
|
||||
guard let bodyStart = markdownIndex(content, offsetBy: 1, from: index), bodyStart < content.endIndex else {
|
||||
// Outer boundary before the opener: line start, or a non-alphanumeric char.
|
||||
// (Rejects `cost$5$total`, the `$` after `5` in `$5-$10`, etc.)
|
||||
if index > content.startIndex {
|
||||
let beforeOpener = content[content.index(before: index)]
|
||||
if beforeOpener.isLetter || beforeOpener.isNumber {
|
||||
result.append(content[index])
|
||||
index = content.index(after: index)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Opener: 1 or 2 leading `$` (a 3rd `$` becomes inner content).
|
||||
let openerRunEnd = markdownIndex(afterRepeating: "$", in: content, from: index)
|
||||
let openerCount = min(2, content.distance(from: index, to: openerRunEnd))
|
||||
let bodyStart = content.index(index, offsetBy: openerCount)
|
||||
// Inner boundary after the opener: must exist and be non-whitespace.
|
||||
guard bodyStart < content.endIndex, !content[bodyStart].isWhitespace else {
|
||||
result.append(content[index])
|
||||
index = content.index(after: index)
|
||||
continue
|
||||
}
|
||||
if content[bodyStart] == "$" || content[bodyStart].isWhitespace {
|
||||
result.append(content[index])
|
||||
index = content.index(after: index)
|
||||
continue
|
||||
}
|
||||
|
||||
let closerPattern = String(repeating: "$", count: openerCount)
|
||||
var searchIndex = bodyStart
|
||||
var matchedRange: Range<String.Index>?
|
||||
while let closeRange = markdownFirstUnescapedRange(of: "$", in: content, from: searchIndex) {
|
||||
let closerIndex = closeRange.lowerBound
|
||||
let previousIndex = content.index(before: closerIndex)
|
||||
if !content[previousIndex].isWhitespace {
|
||||
matchedRange = closeRange
|
||||
break
|
||||
while let closeRange = markdownFirstUnescapedRange(of: closerPattern, in: content, from: searchIndex) {
|
||||
// Inner boundary before the closer: non-whitespace.
|
||||
let beforeCloser = content[content.index(before: closeRange.lowerBound)]
|
||||
if beforeCloser.isWhitespace {
|
||||
searchIndex = closeRange.upperBound
|
||||
continue
|
||||
}
|
||||
searchIndex = closeRange.upperBound
|
||||
// Outer boundary after the closer: line end, or a non-alphanumeric char.
|
||||
if closeRange.upperBound < content.endIndex {
|
||||
let afterCloser = content[closeRange.upperBound]
|
||||
if afterCloser.isLetter || afterCloser.isNumber {
|
||||
searchIndex = closeRange.upperBound
|
||||
continue
|
||||
}
|
||||
}
|
||||
matchedRange = closeRange
|
||||
break
|
||||
}
|
||||
|
||||
if let matchedRange {
|
||||
let latex = String(content[bodyStart ..< matchedRange.lowerBound])
|
||||
if let descriptor = markdownAcceptedFormulaDescriptor(
|
||||
|
|
@ -977,14 +1003,153 @@ func markdownWebpage(context: AccountContext, file: FileMediaReference) -> (webP
|
|||
guard let data = try? Data(contentsOf: fileURL) else {
|
||||
return nil
|
||||
}
|
||||
guard let webPage = markdownWebpage(context: context, file: file, fileURL: fileURL, data: data) else {
|
||||
guard let webPage = markdownWebpage(context: context, file: (file, fileURL), data: data) else {
|
||||
return nil
|
||||
}
|
||||
return (webPage, fileURL)
|
||||
}
|
||||
|
||||
public func inputRichTextAttributeFromText(context: AccountContext, text: String) -> RichTextMessageAttribute? {
|
||||
guard #available(iOS 15.0, *) else {
|
||||
return nil
|
||||
}
|
||||
guard let data = text.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
guard let webpage = markdownWebpage(context: context, file: nil, data: data), case let .Loaded(content) = webpage.content, let instantPage = content.instantPage else {
|
||||
return nil
|
||||
}
|
||||
return RichTextMessageAttribute(instantPage: instantPage._parse())
|
||||
}
|
||||
|
||||
// MARK: - Markdown classification (entity-expressible vs. rich layout)
|
||||
|
||||
private let markdownBlockHeuristicRegex = try? NSRegularExpression(
|
||||
pattern: "(^|\\n)[ \\t]*(#{1,6}[ \\t]|[-*+][ \\t]|\\d{1,9}[.)][ \\t]|-{1,}[ \\t]*(\\n|$))",
|
||||
options: []
|
||||
)
|
||||
|
||||
// Cheap necessary-condition pre-filter. A safe over-approximation: if it
|
||||
// returns false, the text cannot contain a rich-only construct, so the
|
||||
// expensive markdown parse is skipped and the entity path is used.
|
||||
private func markdownMightNeedRichLayout(_ text: String) -> Bool {
|
||||
// Tables ('|') and images ('![') can appear anywhere on a line. This over-approximates
|
||||
// (prose containing '|' also triggers a parse); the parse + block inspection resolves it.
|
||||
if text.contains("|") || text.contains("![") {
|
||||
return true
|
||||
}
|
||||
// Math delimiters. Over-approximates (any `$` triggers a parse); the strict
|
||||
// detection + gate decide whether a formula block is actually produced.
|
||||
if text.contains("$") || text.contains("\\(") || text.contains("\\[") {
|
||||
return true
|
||||
}
|
||||
// Setext H1 heading: a line of '=' underlining the previous line.
|
||||
// (Setext H2 dash-underlines are caught by the dash-line branch in the regex.)
|
||||
if text.contains("\n=") {
|
||||
return true
|
||||
}
|
||||
// Line-anchored block markers: headings, list items, setext-H2 dash underlines.
|
||||
if let regex = markdownBlockHeuristicRegex {
|
||||
let range = NSRange(text.startIndex..., in: text)
|
||||
if regex.firstMatch(in: text, options: [], range: range) != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// True when this inline RichText maps onto Telegram message entities.
|
||||
// Returns false for inline content that forces the rich path (inline image,
|
||||
// sub/superscript, highlight, formula). Formulas trigger the rich path; casual
|
||||
// '$' usage is excluded by the strict boundary rule in the detection step, not here.
|
||||
private func richTextIsEntityExpressible(_ text: RichText) -> Bool {
|
||||
switch text {
|
||||
case .empty, .plain:
|
||||
return true
|
||||
case .bold(let inner), .italic(let inner), .underline(let inner), .strikethrough(let inner), .fixed(let inner):
|
||||
return richTextIsEntityExpressible(inner)
|
||||
case .superscript, .marked, .`subscript`:
|
||||
return false
|
||||
case .url(let inner, _, _):
|
||||
return richTextIsEntityExpressible(inner)
|
||||
case .email(let inner, _):
|
||||
return richTextIsEntityExpressible(inner)
|
||||
case .concat(let items):
|
||||
return items.allSatisfy(richTextIsEntityExpressible)
|
||||
case .phone(let inner, _):
|
||||
return richTextIsEntityExpressible(inner)
|
||||
case .image:
|
||||
return false
|
||||
case .anchor(let inner, _):
|
||||
return richTextIsEntityExpressible(inner)
|
||||
case .formula:
|
||||
return false
|
||||
case .textCustomEmoji:
|
||||
return true
|
||||
case .textAutoEmail(let inner), .textAutoPhone(let inner), .textAutoUrl(let inner), .textBankCard(let inner), .textBotCommand(let inner), .textCashtag(let inner), .textHashtag(let inner), .textMention(let inner), .textSpoiler(let inner):
|
||||
return richTextIsEntityExpressible(inner)
|
||||
case .textMentionName(let inner, _):
|
||||
return richTextIsEntityExpressible(inner)
|
||||
}
|
||||
}
|
||||
|
||||
private func isEmptyRichText(_ text: RichText) -> Bool {
|
||||
switch text {
|
||||
case .empty:
|
||||
return true
|
||||
case .plain(let value):
|
||||
return value.isEmpty
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Block types that do NOT trigger a rich-layout message. Besides the genuinely
|
||||
// entity-expressible blocks (paragraph/preformatted/blockQuote/anchor), dividers
|
||||
// are intentionally excluded as triggers too ('---' is too common in casual text).
|
||||
// Formulas DO trigger the rich path; casual '$' usage is excluded by the strict
|
||||
// boundary rule in the detection step. Effective rich triggers are therefore
|
||||
// headings, lists, tables, and formulas.
|
||||
private func blockIsEntityExpressible(_ block: InstantPageBlock) -> Bool {
|
||||
switch block {
|
||||
case .paragraph(let text):
|
||||
return richTextIsEntityExpressible(text)
|
||||
case .preformatted(let text, _):
|
||||
return richTextIsEntityExpressible(text)
|
||||
case .blockQuote(let text, let caption):
|
||||
return isEmptyRichText(caption) && richTextIsEntityExpressible(text)
|
||||
case .anchor, .unsupported:
|
||||
return true
|
||||
case .divider:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func instantPageNeedsRichLayout(_ blocks: [InstantPageBlock]) -> Bool {
|
||||
return blocks.contains { !blockIsEntityExpressible($0) }
|
||||
}
|
||||
|
||||
// Returns a RichTextMessageAttribute IFF the markdown in `text` produces an
|
||||
// InstantPage block with no entity equivalent. Returns nil (-> send via the
|
||||
// regular entity path) for plain text, pre-iOS-15, oversize markdown, or
|
||||
// markdown that maps cleanly onto entities.
|
||||
public func richMarkdownAttributeIfNeeded(context: AccountContext, text: String) -> RichTextMessageAttribute? {
|
||||
guard markdownMightNeedRichLayout(text) else {
|
||||
return nil
|
||||
}
|
||||
guard let attribute = inputRichTextAttributeFromText(context: context, text: text) else {
|
||||
return nil
|
||||
}
|
||||
guard instantPageNeedsRichLayout(attribute.instantPage.blocks) else {
|
||||
return nil
|
||||
}
|
||||
return attribute
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
private func markdownWebpage(context: AccountContext, file: FileMediaReference, fileURL: URL, data: Data) -> TelegramMediaWebpage? {
|
||||
private func markdownWebpage(context: AccountContext, file: (file: FileMediaReference, url: URL)?, data: Data) -> TelegramMediaWebpage? {
|
||||
let limits = markdownSafetyLimits
|
||||
guard markdownPassesPreflight(data: data, limits: limits) else {
|
||||
return nil
|
||||
|
|
@ -996,33 +1161,50 @@ private func markdownWebpage(context: AccountContext, file: FileMediaReference,
|
|||
|
||||
let attributedString: NSAttributedString
|
||||
do {
|
||||
let baseURL: URL?
|
||||
if let file {
|
||||
baseURL = file.url.deletingLastPathComponent()
|
||||
} else {
|
||||
baseURL = nil
|
||||
}
|
||||
attributedString = try NSAttributedString(
|
||||
markdown: Data(preparedSource.text.utf8),
|
||||
options: .init(),
|
||||
baseURL: fileURL.deletingLastPathComponent()
|
||||
baseURL: baseURL
|
||||
)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
|
||||
let budget = MarkdownConversionBudget(limits: limits)
|
||||
let conversionContext = MarkdownConversionContext(context: context, documentURL: fileURL, formulasByPlaceholder: preparedSource.formulasByPlaceholder, budget: budget)
|
||||
let conversionContext = MarkdownConversionContext(context: context, documentURL: file?.url, formulasByPlaceholder: preparedSource.formulasByPlaceholder, budget: budget)
|
||||
guard let pageResult = markdownPageResult(from: attributedString, context: conversionContext) else {
|
||||
return nil
|
||||
}
|
||||
let blocks = markdownBlocksWithGeneratedAnchors(pageResult.blocks)
|
||||
// Heading anchors exist for intra-document navigation ([link](#slug)); they are
|
||||
// noise in a chat message, where they prepend an invisible block per heading.
|
||||
// Only generate them for the document path (file != nil).
|
||||
let blocks: [InstantPageBlock]
|
||||
if file != nil {
|
||||
blocks = markdownBlocksWithGeneratedAnchors(pageResult.blocks)
|
||||
} else {
|
||||
blocks = pageResult.blocks
|
||||
}
|
||||
guard !blocks.isEmpty, budget.validateFinalBlocks(blocks) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = markdownTitle(from: blocks, file: file, fileURL: fileURL)
|
||||
var title: String?
|
||||
if let file {
|
||||
title = markdownTitle(from: blocks, file: file.file, fileURL: file.url)
|
||||
}
|
||||
let text = markdownFirstParagraphText(from: blocks)
|
||||
let instantPage = InstantPage(
|
||||
blocks: blocks,
|
||||
media: pageResult.media,
|
||||
isComplete: true,
|
||||
rtl: false,
|
||||
url: fileURL.absoluteString,
|
||||
url: file?.url.absoluteString ?? "",
|
||||
views: nil
|
||||
)
|
||||
|
||||
|
|
@ -1030,8 +1212,8 @@ private func markdownWebpage(context: AccountContext, file: FileMediaReference,
|
|||
webpageId: EngineMedia.Id(namespace: 0, id: 0),
|
||||
content: .Loaded(
|
||||
TelegramMediaWebpageLoadedContent(
|
||||
url: fileURL.absoluteString,
|
||||
displayUrl: fileURL.absoluteString,
|
||||
url: file?.url.absoluteString ?? "",
|
||||
displayUrl: file?.url.absoluteString ?? "",
|
||||
hash: 0,
|
||||
type: "article",
|
||||
websiteName: nil,
|
||||
|
|
@ -1168,7 +1350,12 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv
|
|||
}
|
||||
switch level {
|
||||
case Int.min ... 1:
|
||||
return [.title(text)]
|
||||
if context.documentURL == nil {
|
||||
// Chat message: a single '#' is a normal heading, not a document title.
|
||||
return [.heading(text: text, level: 1)]
|
||||
} else {
|
||||
return [.title(text)]
|
||||
}
|
||||
default:
|
||||
return [.heading(text: text, level: Int32(max(2, min(level, 6))))]
|
||||
}
|
||||
|
|
@ -1258,42 +1445,35 @@ private func markdownListItems(from nodes: [MarkdownIntentNode], ordered: Bool,
|
|||
return nil
|
||||
}
|
||||
let taskListState = markdownApplyTaskListMarker(to: &blocks)
|
||||
let number: String?
|
||||
if let taskListState {
|
||||
number = markdownTaskListNumber(for: taskListState)
|
||||
} else if ordered {
|
||||
number = "\(ordinal)"
|
||||
} else {
|
||||
number = nil
|
||||
let checked: Bool?
|
||||
switch taskListState {
|
||||
case .unchecked:
|
||||
checked = false
|
||||
case .checked:
|
||||
checked = true
|
||||
case nil:
|
||||
checked = nil
|
||||
}
|
||||
let number: String? = ordered ? "\(ordinal)" : nil
|
||||
if blocks.isEmpty {
|
||||
if let number {
|
||||
result.append(.text(.plain(" "), number))
|
||||
if checked != nil || number != nil {
|
||||
result.append(.text(.plain(" "), number, checked))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if blocks.count == 1, case let .paragraph(text) = blocks[0] {
|
||||
if number != nil && markdownIsWhitespaceOnly(text) {
|
||||
result.append(.text(.plain(" "), number))
|
||||
if markdownIsWhitespaceOnly(text) && (checked != nil || number != nil) {
|
||||
result.append(.text(.plain(" "), number, checked))
|
||||
} else {
|
||||
result.append(.text(text, number))
|
||||
result.append(.text(text, number, checked))
|
||||
}
|
||||
} else {
|
||||
result.append(.blocks(blocks, number))
|
||||
result.append(.blocks(blocks, number, checked))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func markdownTaskListNumber(for state: MarkdownTaskListState) -> String {
|
||||
switch state {
|
||||
case .unchecked:
|
||||
return markdownTaskListUncheckedNumber
|
||||
case .checked:
|
||||
return markdownTaskListCheckedNumber
|
||||
}
|
||||
}
|
||||
|
||||
private func markdownApplyTaskListMarker(to blocks: inout [InstantPageBlock]) -> MarkdownTaskListState? {
|
||||
guard !blocks.isEmpty, case let .paragraph(text) = blocks[0] else {
|
||||
return nil
|
||||
|
|
@ -1764,7 +1944,7 @@ private func markdownInlineImageDimensions(attributes: [NSAttributedString.Key:
|
|||
return PixelDimensions(width: side, height: side)
|
||||
}
|
||||
|
||||
private func markdownLink(attributes: [NSAttributedString.Key: Any], documentURL: URL) -> String? {
|
||||
private func markdownLink(attributes: [NSAttributedString.Key: Any], documentURL: URL?) -> String? {
|
||||
if let value = attributes[markdownLinkAttribute] as? URL {
|
||||
return markdownNormalizedLink(value, documentURL: documentURL)
|
||||
}
|
||||
|
|
@ -1783,14 +1963,14 @@ private func markdownLink(attributes: [NSAttributedString.Key: Any], documentURL
|
|||
return nil
|
||||
}
|
||||
|
||||
private func markdownNormalizedLink(_ url: URL, documentURL: URL) -> String {
|
||||
private func markdownNormalizedLink(_ url: URL, documentURL: URL?) -> String {
|
||||
if url.baseURL != nil {
|
||||
let relative = url.relativeString
|
||||
if relative.hasPrefix("#") {
|
||||
return relative
|
||||
}
|
||||
}
|
||||
if let fragment = url.fragment, markdownMatchesDocument(url, documentURL: documentURL) {
|
||||
if let documentURL, let fragment = url.fragment, markdownMatchesDocument(url, documentURL: documentURL) {
|
||||
return "#\(fragment)"
|
||||
}
|
||||
return url.absoluteString
|
||||
|
|
@ -1934,6 +2114,10 @@ private func markdownDroppingPrefixLength(_ length: Int, from text: RichText) ->
|
|||
case let .anchor(inner, name):
|
||||
let dropped = markdownDroppingPrefixLength(length, from: inner)
|
||||
return dropped == .empty ? .empty : .anchor(text: dropped, name: name)
|
||||
case .textCustomEmoji:
|
||||
return text
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1963,6 +2147,10 @@ private func markdownHasDisplayableContent(_ richText: RichText) -> Bool {
|
|||
return true
|
||||
case let .formula(latex):
|
||||
return !latex.isEmpty
|
||||
case .textCustomEmoji:
|
||||
return true
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1992,6 +2180,10 @@ private func markdownIsWhitespaceOnly(_ richText: RichText) -> Bool {
|
|||
return false
|
||||
case let .formula(latex):
|
||||
return latex.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
case .textCustomEmoji:
|
||||
return false
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2084,11 +2276,11 @@ private func markdownFirstParagraphText(from blocks: [InstantPageBlock], depth:
|
|||
case let .list(items, _):
|
||||
for item in items {
|
||||
switch item {
|
||||
case let .text(text, _):
|
||||
case let .text(text, _, _):
|
||||
if !text.plainText.isEmpty {
|
||||
return text.plainText
|
||||
}
|
||||
case let .blocks(blocks, _):
|
||||
case let .blocks(blocks, _, _):
|
||||
if let text = markdownFirstParagraphText(from: blocks, depth: depth + 1) {
|
||||
return text
|
||||
}
|
||||
|
|
|
|||
|
|
@ -404,6 +404,10 @@ private func trimStart(_ input: RichText) -> RichText {
|
|||
break
|
||||
case .formula:
|
||||
break
|
||||
case .textCustomEmoji:
|
||||
break
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
break
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
|
@ -448,6 +452,10 @@ private func trimEnd(_ input: RichText) -> RichText {
|
|||
break
|
||||
case .formula:
|
||||
break
|
||||
case .textCustomEmoji:
|
||||
break
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
break
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
|
@ -493,6 +501,10 @@ private func trim(_ input: RichText) -> RichText {
|
|||
break
|
||||
case .formula:
|
||||
break
|
||||
case .textCustomEmoji:
|
||||
break
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
break
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
|
@ -537,6 +549,10 @@ private func addNewLine(_ input: RichText) -> RichText {
|
|||
break
|
||||
case let .formula(latex):
|
||||
text = .concat([.formula(latex: latex), .plain("\n")])
|
||||
case .textCustomEmoji:
|
||||
break
|
||||
case .textAutoEmail, .textAutoPhone, .textAutoUrl, .textBankCard, .textBotCommand, .textCashtag, .textHashtag, .textMention, .textMentionName, .textSpoiler:
|
||||
break
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
|
@ -654,17 +670,17 @@ private func parseList(_ input: [String: Any], _ url: String, _ media: inout [En
|
|||
if parseAsBlocks {
|
||||
let blocks = parsePageBlocks(subcontent, url, &media)
|
||||
if !blocks.isEmpty {
|
||||
items.append(.blocks(blocks, nil))
|
||||
items.append(.blocks(blocks, nil, nil))
|
||||
}
|
||||
} else {
|
||||
items.append(.text(trim(parseRichText(item, &media)), nil))
|
||||
items.append(.text(trim(parseRichText(item, &media)), nil, nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
let ordered = tag == "ol"
|
||||
var allEmpty = true
|
||||
for item in items {
|
||||
if case let .text(text, _) = item {
|
||||
if case let .text(text, _, _) = item {
|
||||
if case .empty = text {
|
||||
} else {
|
||||
let plainText = text.plainText
|
||||
|
|
|
|||
259
submodules/BrowserUI/Sources/InstantPageToMarkdown.swift
Normal file
259
submodules/BrowserUI/Sources/InstantPageToMarkdown.swift
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
import Foundation
|
||||
import TelegramCore
|
||||
|
||||
/// Reconstructs a markdown source string from an `InstantPage`.
|
||||
///
|
||||
/// This is the inverse of the markdown→`InstantPage` conversion used when
|
||||
/// sending rich messages. It is best-effort and never fails: media blocks
|
||||
/// (images/videos/audio/embeds/maps) are skipped, and any block or inline node
|
||||
/// without a CommonMark representation falls back to its plain text.
|
||||
///
|
||||
/// Inline formatting is emitted as CommonMark (`**bold**`, `*italic*`,
|
||||
/// `` `code` ``, `~~strike~~`, `[text](url)`) because the send-time classifier
|
||||
/// re-parses the text through the rich (Apple CommonMark) path, not the entity
|
||||
/// regex.
|
||||
public func markdownStringFromInstantPage(_ instantPage: InstantPage) -> String {
|
||||
return markdownString(from: instantPage.blocks)
|
||||
}
|
||||
|
||||
private func markdownString(from blocks: [InstantPageBlock]) -> String {
|
||||
var pieces: [String] = []
|
||||
for block in blocks {
|
||||
if let piece = markdownString(from: block) {
|
||||
pieces.append(piece)
|
||||
}
|
||||
}
|
||||
return pieces.joined(separator: "\n\n")
|
||||
}
|
||||
|
||||
private func markdownString(from block: InstantPageBlock) -> String? {
|
||||
switch block {
|
||||
case let .heading(text, level):
|
||||
let hashes = String(repeating: "#", count: max(1, min(6, Int(level))))
|
||||
return "\(hashes) \(markdownInline(from: text))"
|
||||
case let .title(text):
|
||||
return "# \(markdownInline(from: text))"
|
||||
case let .paragraph(text):
|
||||
let rendered = markdownInline(from: text)
|
||||
return rendered.isEmpty ? nil : escapeLeadingBlockMarker(rendered)
|
||||
case let .preformatted(text, language):
|
||||
let language = language ?? ""
|
||||
return "```\(language)\n\(markdownInline(from: text))\n```"
|
||||
case let .blockQuote(text, _):
|
||||
return markdownBlockQuote(text)
|
||||
case let .pullQuote(text, _):
|
||||
return markdownBlockQuote(text)
|
||||
case let .list(items, ordered):
|
||||
return markdownList(items: items, ordered: ordered, indent: 0)
|
||||
case let .table(_, rows, _, _):
|
||||
return markdownTable(rows: rows)
|
||||
case .divider:
|
||||
return "---"
|
||||
case let .formula(latex):
|
||||
return "$\(latex)$"
|
||||
case .anchor:
|
||||
// Chat send already strips generated heading anchors; drop them here too.
|
||||
return nil
|
||||
case let .subtitle(text), let .header(text), let .subheader(text), let .footer(text), let .kicker(text):
|
||||
let rendered = markdownInline(from: text)
|
||||
return rendered.isEmpty ? nil : escapeLeadingBlockMarker(rendered)
|
||||
default:
|
||||
// Media and other structural blocks are skipped (out of scope).
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func markdownBlockQuote(_ text: RichText) -> String {
|
||||
let body = markdownInline(from: text)
|
||||
let lines = body.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
return lines.map { "> \($0)" }.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private func markdownInline(from richText: RichText) -> String {
|
||||
switch richText {
|
||||
case .empty:
|
||||
return ""
|
||||
case let .plain(string):
|
||||
return escapeMarkdown(string)
|
||||
case let .bold(text):
|
||||
return "**\(markdownInline(from: text))**"
|
||||
case let .italic(text):
|
||||
return "*\(markdownInline(from: text))*"
|
||||
case let .fixed(text):
|
||||
return "`\(markdownInline(from: text))`"
|
||||
case let .strikethrough(text):
|
||||
return "~~\(markdownInline(from: text))~~"
|
||||
case let .url(text, url, _):
|
||||
return "[\(markdownInline(from: text))](\(url))"
|
||||
case let .email(text, email):
|
||||
return "[\(markdownInline(from: text))](mailto:\(email))"
|
||||
case let .phone(text, phone):
|
||||
return "[\(markdownInline(from: text))](tel:\(phone))"
|
||||
case let .concat(parts):
|
||||
return parts.map { markdownInline(from: $0) }.joined()
|
||||
case let .anchor(text, _):
|
||||
return markdownInline(from: text)
|
||||
case let .formula(latex):
|
||||
return "$\(latex)$"
|
||||
// No CommonMark equivalent; emit inner text. These cannot arise from a
|
||||
// markdown-composed message (Apple's markdown parser never produces them),
|
||||
// so this is purely defensive.
|
||||
case let .underline(text):
|
||||
return markdownInline(from: text)
|
||||
case let .marked(text):
|
||||
return markdownInline(from: text)
|
||||
case let .superscript(text):
|
||||
return markdownInline(from: text)
|
||||
case let .`subscript`(text):
|
||||
return markdownInline(from: text)
|
||||
default:
|
||||
// .image, .textCustomEmoji, and the entity cases (.textMention,
|
||||
// .textHashtag, …): fall back to plain text.
|
||||
return escapeMarkdown(richText.plainText)
|
||||
}
|
||||
}
|
||||
|
||||
/// Backslash-escapes inline markdown-significant characters so literal text
|
||||
/// does not re-parse as formatting. Pragmatic set, not full CommonMark.
|
||||
private func escapeMarkdown(_ string: String) -> String {
|
||||
var result = ""
|
||||
result.reserveCapacity(string.count)
|
||||
for character in string {
|
||||
switch character {
|
||||
case "\\", "*", "_", "`", "[", "]", "~", "|":
|
||||
result.append("\\")
|
||||
result.append(character)
|
||||
default:
|
||||
result.append(character)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Escapes a line-leading character that would otherwise be read as a block
|
||||
/// marker (ATX heading, blockquote, or list item) when the text starts a line.
|
||||
private func escapeLeadingBlockMarker(_ string: String) -> String {
|
||||
guard let first = string.first else {
|
||||
return string
|
||||
}
|
||||
if first == "#" || first == ">" || first == "-" || first == "+" {
|
||||
return "\\" + string
|
||||
}
|
||||
// Ordered-list marker: one or more digits followed by '.' or ')'.
|
||||
if first.isNumber {
|
||||
var index = string.startIndex
|
||||
while index < string.endIndex, string[index].isNumber {
|
||||
index = string.index(after: index)
|
||||
}
|
||||
if index < string.endIndex, string[index] == "." || string[index] == ")" {
|
||||
let prefix = string[string.startIndex ..< index]
|
||||
let delimiter = string[index]
|
||||
let rest = string[string.index(after: index)...]
|
||||
return "\(prefix)\\\(delimiter)\(rest)"
|
||||
}
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
/// Collapses newlines for a single-line GFM table cell. The `|` character is
|
||||
/// already escaped by `escapeMarkdown` for `.plain` runs.
|
||||
private func escapeTableCell(_ string: String) -> String {
|
||||
return string.replacingOccurrences(of: "\n", with: " ")
|
||||
}
|
||||
|
||||
private func markdownList(items: [InstantPageListItem], ordered: Bool, indent: Int) -> String {
|
||||
let indentString = String(repeating: " ", count: indent * 2)
|
||||
var lines: [String] = []
|
||||
var index = 1
|
||||
for item in items {
|
||||
// Ordered markers are regenerated from the running index (CommonMark renumbers
|
||||
// anyway); the unordered marker is fixed. A task-list `checked` state is emitted
|
||||
// as a GitHub task marker so re-classification on save re-parses it as a checkbox.
|
||||
let listMarker = ordered ? "\(index). " : "- "
|
||||
let taskMarker: String
|
||||
switch item.checked {
|
||||
case .some(false):
|
||||
taskMarker = "[ ] "
|
||||
case .some(true):
|
||||
taskMarker = "[x] "
|
||||
case .none:
|
||||
taskMarker = ""
|
||||
}
|
||||
let marker = "\(listMarker)\(taskMarker)"
|
||||
switch item {
|
||||
case let .text(text, _, _):
|
||||
lines.append("\(indentString)\(marker)\(markdownInline(from: text))")
|
||||
case let .blocks(blocks, _, _):
|
||||
var remainder = blocks
|
||||
var markerLineText = ""
|
||||
if case let .paragraph(text)? = remainder.first {
|
||||
markerLineText = markdownInline(from: text)
|
||||
remainder = Array(remainder.dropFirst())
|
||||
}
|
||||
lines.append("\(indentString)\(marker)\(markerLineText)")
|
||||
let childIndentString = String(repeating: " ", count: (indent + 1) * 2)
|
||||
for block in remainder {
|
||||
if case let .list(nestedItems, nestedOrdered) = block {
|
||||
lines.append(markdownList(items: nestedItems, ordered: nestedOrdered, indent: indent + 1))
|
||||
} else if let rendered = markdownString(from: block) {
|
||||
for line in rendered.split(separator: "\n", omittingEmptySubsequences: false) {
|
||||
lines.append("\(childIndentString)\(line)")
|
||||
}
|
||||
}
|
||||
}
|
||||
case .unknown:
|
||||
break
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private func markdownTable(rows: [InstantPageTableRow]) -> String? {
|
||||
guard !rows.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
let columnCount = rows.map { $0.cells.count }.max() ?? 0
|
||||
guard columnCount > 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
func renderRow(_ row: InstantPageTableRow) -> String {
|
||||
var cellStrings: [String] = []
|
||||
for columnIndex in 0 ..< columnCount {
|
||||
if columnIndex < row.cells.count, let text = row.cells[columnIndex].text {
|
||||
cellStrings.append(escapeTableCell(markdownInline(from: text)))
|
||||
} else {
|
||||
cellStrings.append("")
|
||||
}
|
||||
}
|
||||
return "| " + cellStrings.joined(separator: " | ") + " |"
|
||||
}
|
||||
|
||||
var lines: [String] = []
|
||||
lines.append(renderRow(rows[0]))
|
||||
|
||||
var separators: [String] = []
|
||||
for columnIndex in 0 ..< columnCount {
|
||||
let alignment: TableHorizontalAlignment
|
||||
if columnIndex < rows[0].cells.count {
|
||||
alignment = rows[0].cells[columnIndex].alignment
|
||||
} else {
|
||||
alignment = .left
|
||||
}
|
||||
switch alignment {
|
||||
case .left:
|
||||
separators.append("---")
|
||||
case .center:
|
||||
separators.append(":---:")
|
||||
case .right:
|
||||
separators.append("---:")
|
||||
}
|
||||
}
|
||||
lines.append("| " + separators.joined(separator: " | ") + " |")
|
||||
|
||||
for row in rows.dropFirst() {
|
||||
lines.append(renderRow(row))
|
||||
}
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
|
@ -1404,12 +1404,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
}
|
||||
|
||||
let _ = (combineLatest(queue: .mainQueue(),
|
||||
self.context.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peer.id)])
|
||||
|> take(1),
|
||||
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.CachedData(id: peer.id)),
|
||||
forumSourcePeer
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] combinedView, forumSourcePeer in
|
||||
guard let self, let cachedDataView = combinedView.views[.cachedPeerData(peerId: peer.id)] as? CachedPeerDataView else {
|
||||
|> deliverOnMainQueue).start(next: { [weak self] cachedPeerData, forumSourcePeer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let navigationController = self.navigationController as? NavigationController else {
|
||||
|
|
@ -1435,7 +1434,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
} else if case let .channel(channel) = peer, channel.flags.contains(.displayForumAsTabs) {
|
||||
openAsInlineForum = false
|
||||
} else {
|
||||
if let cachedData = cachedDataView.cachedPeerData as? CachedChannelData, case let .known(viewForumAsMessages) = cachedData.viewForumAsMessages, viewForumAsMessages {
|
||||
if let cachedData = cachedPeerData as? CachedChannelData, case let .known(viewForumAsMessages) = cachedData.viewForumAsMessages, viewForumAsMessages {
|
||||
openAsInlineForum = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ enum ChatListFilterCategoryIcon {
|
|||
case archived
|
||||
}
|
||||
|
||||
final class ChatListFilterPresetCategoryItem: ListViewItem, ItemListItem {
|
||||
final class ChatListFilterPresetCategoryItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let systemStyle: ItemListSystemStyle
|
||||
let title: String
|
||||
|
|
@ -31,6 +31,10 @@ final class ChatListFilterPresetCategoryItem: ListViewItem, ItemListItem {
|
|||
let sectionId: ItemListSectionId
|
||||
let updatedRevealedOptions: (Bool) -> Void
|
||||
let remove: () -> Void
|
||||
|
||||
var hasActiveRevealOptions: Bool {
|
||||
return self.isRevealed
|
||||
}
|
||||
|
||||
init(
|
||||
presentationData: ItemListPresentationData,
|
||||
|
|
@ -110,6 +114,7 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL
|
|||
|
||||
private var item: ChatListFilterPresetCategoryItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
private var isHighlighted = false
|
||||
|
||||
private var editableControlNode: ItemListEditableControlNode?
|
||||
|
||||
|
|
@ -326,26 +331,29 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL
|
|||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
let topStripeIsHidden: Bool
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
topStripeIsHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
topStripeIsHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
let bottomStripeIsHidden: Bool
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset + editingOffset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
bottomStripeIsHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
bottomStripeIsHidden = hasCorners
|
||||
}
|
||||
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
|
||||
|
||||
|
|
@ -362,10 +370,9 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL
|
|||
strongSelf.avatarNode.image = updatedAvatarImage
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)), transition: transition)
|
||||
|
||||
strongSelf.backgroundNode.isHidden = false
|
||||
strongSelf.highlightedBackgroundNode.isHidden = true
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
|
|
@ -379,39 +386,8 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL
|
|||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.isHighlighted = highlighted
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
|
|
@ -446,6 +422,12 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL
|
|||
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 15.0, y: self.avatarNode.frame.minY), size: self.avatarNode.bounds.size))
|
||||
}
|
||||
|
||||
override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyOpened() {
|
||||
if let item = self.item {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ struct ChatListFilterPresetListItemEditing: Equatable {
|
|||
let revealed: Bool
|
||||
}
|
||||
|
||||
final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
|
||||
final class ChatListFilterPresetListItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem {
|
||||
let context: AccountContext
|
||||
let presentationData: ItemListPresentationData
|
||||
let systemStyle: ItemListSystemStyle
|
||||
|
|
@ -33,6 +33,10 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
|
|||
let action: () -> Void
|
||||
let setItemWithRevealedOptions: (Int32?, Int32?) -> Void
|
||||
let remove: () -> Void
|
||||
|
||||
var hasActiveRevealOptions: Bool {
|
||||
return self.editing.revealed
|
||||
}
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
|
|
@ -145,6 +149,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
private var item: ChatListFilterPresetListItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
private var isHighlighted = false
|
||||
|
||||
override var canBeSelected: Bool {
|
||||
if self.editableControlNode != nil {
|
||||
|
|
@ -396,26 +401,29 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
|
|||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
let topStripeIsHidden: Bool
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
topStripeIsHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
topStripeIsHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
let bottomStripeIsHidden: Bool
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset + editingOffset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
bottomStripeIsHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
bottomStripeIsHidden = hasCorners
|
||||
}
|
||||
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
|
||||
|
||||
|
|
@ -496,7 +504,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height))
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)), transition: transition)
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
|
|
@ -517,39 +525,8 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
|
|||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.isHighlighted = highlighted
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
|
|
@ -605,6 +582,12 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
|
|||
transition.updateFrame(view: tagIconView, frame: tagIconFrame)
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyOpened() {
|
||||
if let item = self.item {
|
||||
|
|
|
|||
|
|
@ -1392,8 +1392,6 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, nextHasActiveRevealControls: Bool, ListViewItemLayoutParams, countersSize: CGFloat)?
|
||||
|
||||
private var isHighlighted: Bool = false
|
||||
private var isRevealHighlighted: Bool = false
|
||||
private var keepRevealHighlightUntilClosed: Bool = false
|
||||
private var nextHasActiveRevealControls: Bool = false
|
||||
private var skipFadeout: Bool = false
|
||||
private var customAnimationInProgress: Bool = false
|
||||
|
|
@ -2061,7 +2059,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
}
|
||||
|
||||
var reallyHighlighted: Bool {
|
||||
var reallyHighlighted = self.isHighlighted || self.isRevealHighlighted
|
||||
var reallyHighlighted = self.isHighlighted || self.isRevealOptionsActive
|
||||
if let item = self.item {
|
||||
if let itemChatLocation = item.content.chatLocation {
|
||||
if itemChatLocation == item.interaction.highlightedChatLocation?.location {
|
||||
|
|
@ -2079,7 +2077,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
|
||||
let highlightProgress: CGFloat = self.item?.interaction.highlightedChatLocation?.progress ?? 1.0
|
||||
transition.updateCornerRadius(node: self.highlightedBackgroundNode, cornerRadius: self.isRevealHighlighted ? 26.0 : 0.0)
|
||||
transition.updateCornerRadius(node: self.highlightedBackgroundNode, cornerRadius: self.isRevealOptionsActive ? 26.0 : 0.0)
|
||||
self.updateSeparatorAlpha(transition: transition)
|
||||
|
||||
if self.reallyHighlighted {
|
||||
|
|
@ -2145,7 +2143,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
}
|
||||
|
||||
private func updateSeparatorAlpha(transition: ContainedViewLayoutTransition, inlineNavigationProgress: CGFloat? = nil) {
|
||||
let revealSeparatorAlpha: CGFloat = (self.isRevealHighlighted || self.nextHasActiveRevealControls) ? 0.0 : 1.0
|
||||
let revealSeparatorAlpha: CGFloat = (self.isRevealOptionsActive || self.isNextRevealOptionsActive || self.nextHasActiveRevealControls) ? 0.0 : 1.0
|
||||
if let inlineNavigationProgress = inlineNavigationProgress ?? self.item?.interaction.inlineNavigationLocation?.progress {
|
||||
transition.updateAlpha(node: self.separatorNode, alpha: (1.0 - inlineNavigationProgress) * revealSeparatorAlpha)
|
||||
} else {
|
||||
|
|
@ -5175,7 +5173,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
strongSelf.highlightedBackgroundNode.backgroundColor = highlightedBackgroundColor
|
||||
let topNegativeInset: CGFloat = 0.0
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: strongSelf.revealOffset, y: layoutOffset - separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset))
|
||||
transition.updateCornerRadius(node: strongSelf.highlightedBackgroundNode, cornerRadius: strongSelf.isRevealHighlighted ? 26.0 : 0.0)
|
||||
transition.updateCornerRadius(node: strongSelf.highlightedBackgroundNode, cornerRadius: strongSelf.isRevealOptionsActive ? 26.0 : 0.0)
|
||||
|
||||
if let peerPresence = peerPresence {
|
||||
strongSelf.peerPresenceManager?.reset(presence: EnginePeer.Presence(status: peerPresence.status, lastActivity: 0), isOnline: online)
|
||||
|
|
@ -5324,23 +5322,18 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
let highlightedBackgroundFrame = self.highlightedBackgroundNode.frame
|
||||
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: offset, y: highlightedBackgroundFrame.minY), size: highlightedBackgroundFrame.size))
|
||||
|
||||
if !offset.isZero {
|
||||
self.keepRevealHighlightUntilClosed = true
|
||||
}
|
||||
let isRevealHighlighted = !offset.isZero || self.keepRevealHighlightUntilClosed
|
||||
if self.isRevealHighlighted != isRevealHighlighted {
|
||||
self.isRevealHighlighted = isRevealHighlighted
|
||||
self.updateIsHighlighted(transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func clearRevealHighlightIfNeeded(transition: ContainedViewLayoutTransition) {
|
||||
self.keepRevealHighlightUntilClosed = false
|
||||
if self.revealOffset.isZero && self.isRevealHighlighted {
|
||||
self.isRevealHighlighted = false
|
||||
self.updateIsHighlighted(transition: transition)
|
||||
}
|
||||
override public func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
|
||||
self.updateIsHighlighted(transition: transition)
|
||||
}
|
||||
|
||||
override public func nextRevealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
super.nextRevealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
|
||||
self.updateSeparatorAlpha(transition: transition)
|
||||
}
|
||||
|
||||
override public func touchesToOtherItemsPrevented() {
|
||||
|
|
@ -5348,11 +5341,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
if let item = self.item {
|
||||
item.interaction.setPeerIdWithRevealedOptions(nil, nil)
|
||||
}
|
||||
self.clearRevealHighlightIfNeeded(transition: .immediate)
|
||||
}
|
||||
|
||||
override public func revealOptionsInteractivelyOpened() {
|
||||
self.keepRevealHighlightUntilClosed = true
|
||||
if let item = self.item {
|
||||
switch item.index {
|
||||
case let .chatList(index):
|
||||
|
|
@ -5364,7 +5355,6 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
}
|
||||
|
||||
override public func revealOptionsInteractivelyClosed() {
|
||||
self.clearRevealHighlightIfNeeded(transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
if let item = self.item {
|
||||
switch item.index {
|
||||
case let .chatList(index):
|
||||
|
|
|
|||
|
|
@ -103,7 +103,10 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||
|
||||
messageText = ""
|
||||
for message in messages {
|
||||
if !message.text.isEmpty {
|
||||
if let richText = message.richText {
|
||||
messageText = richText.instantPage.previewText()
|
||||
messageEntities = []
|
||||
} else if !message.text.isEmpty {
|
||||
messageText = message.text
|
||||
messageEntities = message._asMessage().textEntitiesAttribute?.entities ?? []
|
||||
for entity in messageEntities {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,17 @@ private func drawRectsImageContent(size: CGSize, context: CGContext, color: UICo
|
|||
}
|
||||
currentRects.removeAll()
|
||||
} else {
|
||||
// The path-stitching loop below assumes consecutive rects in a group
|
||||
// are adjacent (overlapping or sharing an edge). When they're disjoint
|
||||
// — vertical gap, vertical inversion, or horizontal gap on the same
|
||||
// line — it bridges them with a polygon perimeter that renders as a
|
||||
// diagonal bridge across empty space. Split groups whenever the next
|
||||
// rect doesn't intersect the last one (1pt slop in both axes matches
|
||||
// the snap loop's adjacency test).
|
||||
if let last = currentRects.last, !last.insetBy(dx: -1.0, dy: -1.0).intersects(rect) {
|
||||
combinedRects.append(currentRects)
|
||||
currentRects.removeAll()
|
||||
}
|
||||
currentRects.append(rect)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -389,23 +389,16 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
|
|||
self.disposable = (combineLatest(queue: .mainQueue(),
|
||||
mappedFeatured,
|
||||
self.additionalPacks.get(),
|
||||
context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])]),
|
||||
context.engine.data.subscribe(TelegramEngine.EngineData.Item.ItemCollections.InstalledPackIds(namespace: Namespaces.ItemCollection.CloudStickerPacks)),
|
||||
context.sharedContext.presentationData
|
||||
)
|
||||
|> map { featuredEntries, additionalPacks, view, presentationData -> FeaturedTransition in
|
||||
|> map { featuredEntries, additionalPacks, installedPackIds, presentationData -> FeaturedTransition in
|
||||
var presentationData = presentationData
|
||||
if let forceTheme {
|
||||
presentationData = presentationData.withUpdated(theme: forceTheme)
|
||||
}
|
||||
|
||||
var installedPacks = Set<ItemCollectionId>()
|
||||
if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView {
|
||||
if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] {
|
||||
for entry in packsEntries {
|
||||
installedPacks.insert(entry.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let installedPacks = Set(installedPackIds)
|
||||
let entries = featuredScreenEntries(featuredEntries: featuredEntries.0, installedPacks: installedPacks, theme: presentationData.theme, strings: presentationData.strings, fixedUnread: featuredEntries.1, additionalPacks: additionalPacks)
|
||||
let previous = previousEntries.swap(entries)
|
||||
|
||||
|
|
@ -1370,17 +1363,9 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode {
|
|||
)
|
||||
}
|
||||
|
||||
let installedPackIds = context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])
|
||||
|> map { view -> Set<ItemCollectionId> in
|
||||
var installedPacks = Set<ItemCollectionId>()
|
||||
if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView {
|
||||
if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] {
|
||||
for entry in packsEntries {
|
||||
installedPacks.insert(entry.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return installedPacks
|
||||
let installedPackIds = context.engine.data.subscribe(TelegramEngine.EngineData.Item.ItemCollections.InstalledPackIds(namespace: Namespaces.ItemCollection.CloudStickerPacks))
|
||||
|> map { installedPackIds -> Set<ItemCollectionId> in
|
||||
return Set(installedPackIds)
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
let packs = combineLatest(rawPacks, installedPackIds)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ swift_library(
|
|||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/GalleryUI:GalleryUI",
|
||||
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
|
||||
"//submodules/LiveLocationPositionNode:LiveLocationPositionNode",
|
||||
|
|
@ -30,6 +34,7 @@ swift_library(
|
|||
"//submodules/TranslateUI:TranslateUI",
|
||||
"//submodules/Tuples:Tuples",
|
||||
"//third-party/SwiftMath:SwiftMath",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
private var previousContentOffset: CGPoint?
|
||||
private var isDeceleratingBecauseOfDragging = false
|
||||
|
||||
private let hiddenMediaDisposable = MetaDisposable()
|
||||
private let resolveUrlDisposable = MetaDisposable()
|
||||
private let loadWebpageDisposable = MetaDisposable()
|
||||
|
||||
|
|
@ -224,7 +223,6 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
|
||||
deinit {
|
||||
self.hiddenMediaDisposable.dispose()
|
||||
self.resolveUrlDisposable.dispose()
|
||||
self.loadWebpageDisposable.dispose()
|
||||
self.loadProgressDisposable.dispose()
|
||||
|
|
@ -1818,21 +1816,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
guard let items = self.currentLayout?.items, let (webPage, _) = self.webPage else {
|
||||
return
|
||||
}
|
||||
|
||||
if case let .geo(map) = media.media {
|
||||
let controllerParams = LocationViewParams(sendLiveLocation: { _ in
|
||||
}, stopLiveLocation: { _ in
|
||||
}, openUrl: { _ in }, openPeer: { _ in
|
||||
}, showAll: false)
|
||||
|
||||
let peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
|
||||
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peer, text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
|
||||
let controller = LocationViewController(context: self.context, subject: EngineMessage(message), params: controllerParams)
|
||||
self.pushController(controller)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if case let .file(file) = media.media, (file.isVoice || file.isMusic) {
|
||||
var medias: [InstantPageMedia] = []
|
||||
var initialIndex = 0
|
||||
|
|
@ -1849,68 +1833,45 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
self.context.sharedContext.mediaManager.setPlaylist((self.context, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play))
|
||||
return
|
||||
}
|
||||
|
||||
var fromPlayingVideo = false
|
||||
|
||||
var entries: [InstantPageGalleryEntry] = []
|
||||
if case let .webpage(webPage) = media.media {
|
||||
entries.append(InstantPageGalleryEntry(index: 0, pageId: webPage.webpageId, media: media, caption: nil, credit: nil, location: nil))
|
||||
} else if case let .file(file) = media.media, file.isAnimated {
|
||||
fromPlayingVideo = true
|
||||
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil))
|
||||
} else {
|
||||
fromPlayingVideo = true
|
||||
var medias: [InstantPageMedia] = mediasFromItems(items)
|
||||
medias = medias.filter { item in
|
||||
switch item.media {
|
||||
case .image, .file:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for media in medias {
|
||||
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: InstantPageGalleryEntryLocation(position: Int32(entries.count), totalCount: Int32(medias.count))))
|
||||
}
|
||||
}
|
||||
|
||||
var centralIndex: Int?
|
||||
for i in 0 ..< entries.count {
|
||||
if entries[i].media == media {
|
||||
centralIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let centralIndex = centralIndex {
|
||||
let controller = InstantPageGalleryController(context: self.context, userLocation: self.sourceLocation.userLocation, webPage: webPage, entries: entries, centralIndex: centralIndex, fromPlayingVideo: fromPlayingVideo, replaceRootController: { _, _ in
|
||||
}, baseNavigationController: self.getNavigationController())
|
||||
self.hiddenMediaDisposable.set((controller.hiddenMedia |> deliverOnMainQueue).start(next: { [weak self] entry in
|
||||
if let strongSelf = self {
|
||||
for (_, itemNode) in strongSelf.visibleItemsWithNodes {
|
||||
itemNode.updateHiddenMedia(media: entry?.media)
|
||||
}
|
||||
}
|
||||
}))
|
||||
controller.openUrl = { [weak self] url in
|
||||
|
||||
openInstantPageMedia(
|
||||
media: media,
|
||||
allMedias: self.mediasFromItems(items),
|
||||
webPage: webPage,
|
||||
context: self.context,
|
||||
userLocation: self.sourceLocation.userLocation,
|
||||
present: { [weak self] controller, args in
|
||||
self?.present(controller, args)
|
||||
},
|
||||
push: { [weak self] controller in
|
||||
self?.pushController(controller)
|
||||
},
|
||||
openUrl: { [weak self] url in
|
||||
self?.openUrl(url)
|
||||
}
|
||||
self.present(controller, InstantPageGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in
|
||||
if let strongSelf = self {
|
||||
for (_, itemNode) in strongSelf.visibleItemsWithNodes {
|
||||
if let transitionNode = itemNode.transitionNode(media: entry.media) {
|
||||
return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in
|
||||
if let strongSelf = self {
|
||||
strongSelf.scrollNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.scrollNode.view)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
baseNavigationController: { [weak self] in
|
||||
self?.getNavigationController()
|
||||
},
|
||||
transitionArgsForMedia: { [weak self] tappedMedia -> GalleryTransitionArguments? in
|
||||
guard let strongSelf = self else { return nil }
|
||||
for (_, itemNode) in strongSelf.visibleItemsWithNodes {
|
||||
if let transitionNode = itemNode.transitionNode(media: tappedMedia) {
|
||||
return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in
|
||||
if let strongSelf = self {
|
||||
strongSelf.scrollNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.scrollNode.view)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
},
|
||||
hiddenMediaCallback: { [weak self] hidden in
|
||||
guard let strongSelf = self else { return }
|
||||
for (_, itemNode) in strongSelf.visibleItemsWithNodes {
|
||||
itemNode.updateHiddenMedia(media: hidden)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) {
|
||||
|
|
|
|||
|
|
@ -7,12 +7,17 @@ import TelegramUIPreferences
|
|||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
protocol InstantPageImageAttribute {
|
||||
public protocol InstantPageImageAttribute {
|
||||
}
|
||||
|
||||
struct InstantPageMapAttribute: InstantPageImageAttribute {
|
||||
let zoom: Int32
|
||||
let dimensions: CGSize
|
||||
public struct InstantPageMapAttribute: InstantPageImageAttribute {
|
||||
public let zoom: Int32
|
||||
public let dimensions: CGSize
|
||||
|
||||
public init(zoom: Int32, dimensions: CGSize) {
|
||||
self.zoom = zoom
|
||||
self.dimensions = dimensions
|
||||
}
|
||||
}
|
||||
|
||||
public final class InstantPageImageItem: InstantPageItem {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode, InstantPageExt
|
|||
private var themeUpdated: Bool = false
|
||||
private var externalMediaDimensionsUpdated: Bool = false
|
||||
|
||||
init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, getPreloadedResource: @escaping (String) -> Data?) {
|
||||
init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, imageReferenceForMedia: ((TelegramMediaImage) -> ImageMediaReference)? = nil, fileReferenceForMedia: ((TelegramMediaFile) -> FileMediaReference)? = nil, getPreloadedResource: @escaping (String) -> Data?) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.webPage = webPage
|
||||
|
|
@ -117,7 +117,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode, InstantPageExt
|
|||
if let externalResource = largest.resource as? InstantPageExternalMediaResource {
|
||||
self.loadExternalImage(resourceUrl: externalResource.url)
|
||||
} else {
|
||||
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
||||
let imageReference = imageReferenceForMedia?(image) ?? ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
||||
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
|
||||
|
||||
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: image) {
|
||||
|
|
@ -149,7 +149,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode, InstantPageExt
|
|||
if let externalResource = file.resource as? InstantPageExternalMediaResource {
|
||||
self.loadExternalImage(resourceUrl: externalResource.url)
|
||||
} else {
|
||||
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
|
||||
let fileReference = fileReferenceForMedia?(file) ?? FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
|
||||
if file.mimeType.hasPrefix("image/") {
|
||||
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: file) {
|
||||
_ = freeMediaFileInteractiveFetched(account: context.account, userLocation: sourceLocation.userLocation, fileReference: fileReference).start()
|
||||
|
|
@ -176,7 +176,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode, InstantPageExt
|
|||
let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: Int32(dimensions.width), height: Int32(dimensions.height))
|
||||
self.imageNode.setSignal(chatMapSnapshotImage(engine: context.engine, resource: resource))
|
||||
} else if case let .webpage(webPage) = media.media, case let .Loaded(content) = webPage.content, let image = content.image {
|
||||
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
||||
let imageReference = imageReferenceForMedia?(image) ?? ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
||||
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
|
||||
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start())
|
||||
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
|
||||
|
|
|
|||
|
|
@ -131,21 +131,8 @@ private func attributedStringForPreformattedText(_ text: RichText, language: Str
|
|||
return attributedString
|
||||
}
|
||||
|
||||
private let instantPageTaskListUncheckedNumber = "\u{001f}tg-md-task:unchecked"
|
||||
private let instantPageTaskListCheckedNumber = "\u{001f}tg-md-task:checked"
|
||||
private let instantPageChecklistMarkerSize = CGSize(width: 18.0, height: 18.0)
|
||||
|
||||
private func instantPageTaskListMarkerState(_ number: String?) -> Bool? {
|
||||
switch number {
|
||||
case instantPageTaskListUncheckedNumber:
|
||||
return false
|
||||
case instantPageTaskListCheckedNumber:
|
||||
return true
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func instantPageFirstTextLineMidY(in items: [InstantPageItem]) -> CGFloat? {
|
||||
for item in items {
|
||||
if let textItem = item as? InstantPageTextItem {
|
||||
|
|
@ -165,7 +152,7 @@ private func instantPageFirstTextLineMidY(in items: [InstantPageItem]) -> CGFloa
|
|||
return nil
|
||||
}
|
||||
|
||||
public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, excludeCaptions: Bool, isLast: Bool) -> InstantPageLayout {
|
||||
public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, excludeCaptions: Bool, isLast: Bool, fitToWidth: Bool) -> InstantPageLayout {
|
||||
let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in
|
||||
var items: [InstantPageItem] = []
|
||||
var offset = contentSize.height
|
||||
|
|
@ -177,7 +164,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
offset += 14.0
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
|
||||
let (textItem, captionItems, captionContentSize) = layoutTextItemWithString(attributedStringForRichText(caption.text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: offset), media: media, webpage: webpage)
|
||||
let (textItem, captionItems, captionContentSize) = layoutTextItemWithString(attributedStringForRichText(caption.text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: offset), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
contentSize.height += captionContentSize.height
|
||||
offset += captionContentSize.height
|
||||
items.append(contentsOf: captionItems)
|
||||
|
|
@ -196,7 +183,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
}
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .credit, link: false)
|
||||
let (_, captionItems, captionContentSize) = layoutTextItemWithString(attributedStringForRichText(caption.credit, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, alignment: rtl ? .right : .natural, offset: CGPoint(x: horizontalInset, y: offset), media: media, webpage: webpage)
|
||||
let (_, captionItems, captionContentSize) = layoutTextItemWithString(attributedStringForRichText(caption.credit, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, alignment: rtl ? .right : .natural, offset: CGPoint(x: horizontalInset, y: offset), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
contentSize.height += captionContentSize.height
|
||||
offset += captionContentSize.height
|
||||
items.append(contentsOf: captionItems)
|
||||
|
|
@ -217,16 +204,16 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
|
||||
switch block {
|
||||
case let .cover(block):
|
||||
return layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: true)
|
||||
return layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: true, fitToWidth: fitToWidth)
|
||||
case let .title(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .header, link: false)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .subtitle(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .subheader, link: false)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .authorDate(author: author, date: date):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
|
|
@ -265,7 +252,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
if let previousItem = previousItems.last as? InstantPageTextItem, previousItem.containsRTL {
|
||||
previousItemHasRTL = true
|
||||
}
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, alignment: rtl || previousItemHasRTL ? .right : .natural, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, alignment: rtl || previousItemHasRTL ? .right : .natural, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
} else {
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: [])
|
||||
|
|
@ -273,22 +260,22 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
case let .kicker(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .kicker, link: false)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .header(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .header, link: false)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .subheader(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .subheader, link: false)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .heading(text, level):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, attributes: theme.headingTextAttributes(level: level, link: false))
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .formula(latex):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
|
|
@ -298,7 +285,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
let fontSize = (attributes[NSAttributedString.Key.font] as? UIFont)?.pointSize ?? theme.textCategories.paragraph.font.size
|
||||
|
||||
guard let attachment = instantPageMathAttachment(latex: latex, fontSize: fontSize, textColor: textColor, mode: .block) else {
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(.plain(latex), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(.plain(latex), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
}
|
||||
|
||||
|
|
@ -324,7 +311,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
case let .paragraph(text):
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, horizontalInset: horizontalInset, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
|
||||
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, horizontalInset: horizontalInset, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
case let .preformatted(text, language):
|
||||
let backgroundInset: CGFloat = 14.0
|
||||
|
|
@ -342,6 +329,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
offset: CGPoint(x: 17.0, y: backgroundInset),
|
||||
media: media,
|
||||
webpage: webpage,
|
||||
fitToWidth: fitToWidth,
|
||||
opaqueBackground: true
|
||||
)
|
||||
let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height + backgroundInset * 2.0)), shape: .rect, color: theme.codeBlockBackgroundColor)
|
||||
|
|
@ -371,7 +359,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
var hasNums = false
|
||||
if ordered {
|
||||
for item in contentItems {
|
||||
if instantPageTaskListMarkerState(item.num) != nil {
|
||||
if item.checked != nil {
|
||||
hasTaskMarkers = true
|
||||
} else if let num = item.num, !num.isEmpty {
|
||||
hasNums = true
|
||||
|
|
@ -379,7 +367,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
}
|
||||
} else {
|
||||
for item in contentItems {
|
||||
if instantPageTaskListMarkerState(item.num) != nil {
|
||||
if item.checked != nil {
|
||||
hasTaskMarkers = true
|
||||
break
|
||||
}
|
||||
|
|
@ -388,7 +376,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
|
||||
for i in 0 ..< contentItems.count {
|
||||
let item = contentItems[i]
|
||||
if let checked = instantPageTaskListMarkerState(item.num) {
|
||||
if let checked = item.checked {
|
||||
let checklistItem = InstantPageChecklistMarkerItem(frame: CGRect(origin: .zero, size: instantPageChecklistMarkerSize), checked: checked)
|
||||
if ordered {
|
||||
maxIndexWidth = max(maxIndexWidth, instantPageChecklistMarkerSize.width)
|
||||
|
|
@ -407,7 +395,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
} else {
|
||||
value = "\(i + 1)."
|
||||
}
|
||||
let (textItem, _, _) = layoutTextItemWithString(attributedStringForRichText(.plain(value), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint())
|
||||
let (textItem, _, _) = layoutTextItemWithString(attributedStringForRichText(.plain(value), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(), fitToWidth: fitToWidth)
|
||||
if let textItem = textItem, let line = textItem.lines.first {
|
||||
textItem.selectable = false
|
||||
maxIndexWidth = max(maxIndexWidth, line.frame.width)
|
||||
|
|
@ -420,19 +408,23 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
}
|
||||
let indexSpacing: CGFloat = ordered ? (hasTaskMarkers ? 16.0 : 12.0) : (hasTaskMarkers ? 24.0 : 20.0)
|
||||
for (i, item) in contentItems.enumerated() {
|
||||
if (i != 0) {
|
||||
contentSize.height += 18.0
|
||||
if i != 0 {
|
||||
if fitToWidth {
|
||||
contentSize.height += 12.0
|
||||
} else {
|
||||
contentSize.height += 18.0
|
||||
}
|
||||
}
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
|
||||
var effectiveItem = item
|
||||
if case let .blocks(blocks, num) = effectiveItem, blocks.isEmpty {
|
||||
effectiveItem = .text(.plain(" "), num)
|
||||
if case let .blocks(blocks, num, checked) = effectiveItem, blocks.isEmpty {
|
||||
effectiveItem = .text(.plain(" "), num, checked)
|
||||
}
|
||||
switch effectiveItem {
|
||||
case let .text(text, _):
|
||||
let (textItem, textItems, textItemSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, offset: CGPoint(x: horizontalInset + indexSpacing + maxIndexWidth, y: contentSize.height), media: media, webpage: webpage)
|
||||
case let .text(text, _, _):
|
||||
let (textItem, textItems, textItemSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, offset: CGPoint(x: horizontalInset + indexSpacing + maxIndexWidth, y: contentSize.height), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
|
||||
contentSize.height += textItemSize.height
|
||||
let indexItem = indexItems[i]
|
||||
|
|
@ -461,15 +453,15 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
indexItems[i].frame = itemFrame
|
||||
listItems.append(indexItems[i])
|
||||
listItems.append(contentsOf: textItems)
|
||||
case let .blocks(blocks, _):
|
||||
case let .blocks(blocks, _, _):
|
||||
var previousBlock: InstantPageBlock?
|
||||
var originY: CGFloat = contentSize.height
|
||||
var firstBlockLineMidY: CGFloat?
|
||||
for i in 0 ..< blocks.count {
|
||||
let subBlock = blocks[i]
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: i == blocks.count - 1)
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: i == blocks.count - 1, fitToWidth: fitToWidth)
|
||||
|
||||
let spacing: CGFloat = previousBlock != nil && subLayout.contentSize.height > 0.0 ? spacingBetweenBlocks(upper: previousBlock, lower: subBlock) : 0.0
|
||||
let spacing: CGFloat = previousBlock != nil && subLayout.contentSize.height > 0.0 ? spacingBetweenBlocks(upper: previousBlock, lower: subBlock, fitToWidth: fitToWidth) : 0.0
|
||||
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + indexSpacing + maxIndexWidth, y: contentSize.height + spacing))
|
||||
if previousBlock == nil {
|
||||
originY += spacing
|
||||
|
|
@ -520,7 +512,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
styleStack.push(.italic)
|
||||
|
||||
let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, offset: CGPoint(x: horizontalInset + lineInset, y: contentSize.height), media: media, webpage: webpage)
|
||||
let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, offset: CGPoint(x: horizontalInset + lineInset, y: contentSize.height), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
|
||||
contentSize.height += textContentSize.height
|
||||
items.append(contentsOf: textItems)
|
||||
|
|
@ -554,7 +546,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
styleStack.push(.italic)
|
||||
|
||||
let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, alignment: .center, offset: CGPoint(x: 0.0, y: contentSize.height), media: media, webpage: webpage)
|
||||
let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, alignment: .center, offset: CGPoint(x: 0.0, y: contentSize.height), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
for var item in textItems {
|
||||
item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0)
|
||||
}
|
||||
|
|
@ -684,7 +676,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
var i = 0
|
||||
for subItem in innerItems {
|
||||
let frame = mosaicLayout[i].0
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subItem, boundingWidth: frame.width, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: frame.size, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: true, isLast: false)
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subItem, boundingWidth: frame.width, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: frame.size, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: true, isLast: false, fitToWidth: false)
|
||||
items.append(contentsOf: subLayout.flattenedItemsWithOrigin(frame.origin))
|
||||
i += 1
|
||||
}
|
||||
|
|
@ -731,7 +723,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
styleStack.push(.bold)
|
||||
|
||||
let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(.plain(author), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset - avatarInset, offset: CGPoint(x: horizontalInset + lineInset + avatarInset, y: contentSize.height + avatarVerticalInset), media: media, webpage: webpage)
|
||||
let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(.plain(author), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset - avatarInset, offset: CGPoint(x: horizontalInset + lineInset + avatarInset, y: contentSize.height + avatarVerticalInset), media: media, webpage: webpage, fitToWidth: fitToWidth)
|
||||
items.append(contentsOf: textItems)
|
||||
|
||||
contentSize.height += textContentSize.height + avatarVerticalInset
|
||||
|
|
@ -759,9 +751,9 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
var previousBlock: InstantPageBlock?
|
||||
for i in 0 ..< blocks.count {
|
||||
let subBlock = blocks[i]
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: i == blocks.count - 1)
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: i == blocks.count - 1, fitToWidth: fitToWidth)
|
||||
|
||||
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock)
|
||||
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock, fitToWidth: false)
|
||||
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + lineInset, y: contentSize.height + spacing))
|
||||
items.append(contentsOf: blockItems)
|
||||
contentSize.height += subLayout.contentSize.height + spacing
|
||||
|
|
@ -943,9 +935,9 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
var previousBlock: InstantPageBlock?
|
||||
for i in 0 ..< blocks.count {
|
||||
let subBlock = blocks[i]
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: i == blocks.count - 1)
|
||||
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: i == blocks.count - 1, fitToWidth: fitToWidth)
|
||||
|
||||
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock)
|
||||
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock, fitToWidth: false)
|
||||
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing))
|
||||
subitems.append(contentsOf: blockItems)
|
||||
contentSize.height += subLayout.contentSize.height + spacing
|
||||
|
|
@ -953,7 +945,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
}
|
||||
|
||||
if !blocks.isEmpty {
|
||||
let closingSpacing = spacingBetweenBlocks(upper: previousBlock, lower: nil)
|
||||
let closingSpacing = spacingBetweenBlocks(upper: previousBlock, lower: nil, fitToWidth: false)
|
||||
contentSize.height += closingSpacing
|
||||
}
|
||||
|
||||
|
|
@ -971,7 +963,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
|
||||
styleStack.push(.bold)
|
||||
let backgroundInset: CGFloat = 14.0
|
||||
let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(title, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0, offset: CGPoint(x: horizontalInset, y: backgroundInset), media: media, webpage: webpage, opaqueBackground: true)
|
||||
let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(title, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0, offset: CGPoint(x: horizontalInset, y: backgroundInset), media: media, webpage: webpage, fitToWidth: fitToWidth, opaqueBackground: true)
|
||||
let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: textContentSize.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: textContentSize.height + backgroundInset * 2.0)), shape: .rect, color: theme.panelBackgroundColor)
|
||||
items.append(backgroundItem)
|
||||
items.append(contentsOf: textItems)
|
||||
|
|
@ -1054,7 +1046,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
}
|
||||
}
|
||||
|
||||
public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, sideInset: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, addFeedback: Bool = true) -> InstantPageLayout {
|
||||
public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, sideInset: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, addFeedback: Bool = true, fitToWidth: Bool = false) -> InstantPageLayout {
|
||||
var maybeLoadedContent: TelegramMediaWebpageLoadedContent?
|
||||
if case let .Loaded(content) = webPage.content {
|
||||
maybeLoadedContent = content
|
||||
|
|
@ -1084,8 +1076,8 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instant
|
|||
var previousBlock: InstantPageBlock?
|
||||
for i in 0 ..< pageBlocks.count {
|
||||
let block = pageBlocks[i]
|
||||
let blockLayout = layoutInstantPageBlock(webpage: webPage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: sideInset + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: i == pageBlocks.count - 1)
|
||||
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block)
|
||||
let blockLayout = layoutInstantPageBlock(webpage: webPage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: sideInset + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false, isLast: i == pageBlocks.count - 1, fitToWidth: fitToWidth)
|
||||
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block, fitToWidth: fitToWidth)
|
||||
let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing))
|
||||
items.append(contentsOf: blockItems)
|
||||
if CGFloat(0.0).isLess(than: blockLayout.contentSize.height) {
|
||||
|
|
@ -1094,7 +1086,7 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instant
|
|||
}
|
||||
}
|
||||
|
||||
let closingSpacing = spacingBetweenBlocks(upper: previousBlock, lower: nil)
|
||||
let closingSpacing = spacingBetweenBlocks(upper: previousBlock, lower: nil, fitToWidth: fitToWidth)
|
||||
contentSize.height += closingSpacing
|
||||
|
||||
if webPage.webpageId.id != 0 && addFeedback {
|
||||
|
|
@ -1103,5 +1095,13 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instant
|
|||
items.append(feedbackItem)
|
||||
}
|
||||
|
||||
if fitToWidth {
|
||||
contentSize.width = 0.0
|
||||
for item in items {
|
||||
contentSize.width = max(contentSize.width, ceil(item.frame.maxX) + sideInset)
|
||||
}
|
||||
contentSize.width = min(contentSize.width, boundingWidth)
|
||||
}
|
||||
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,82 +2,114 @@ import Foundation
|
|||
import UIKit
|
||||
import TelegramCore
|
||||
|
||||
func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> CGFloat {
|
||||
if let upper = upper, let lower = lower {
|
||||
func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?, fitToWidth: Bool) -> CGFloat {
|
||||
if let upper, let lower {
|
||||
switch (upper, lower) {
|
||||
case (_, .cover), (_, .channelBanner), (.details, .details), (.relatedArticles, _), (_, .anchor):
|
||||
return 0.0
|
||||
case (.divider, _), (_, .divider):
|
||||
case (_, .cover), (_, .channelBanner), (.details, .details), (.relatedArticles, _), (_, .anchor):
|
||||
return 0.0
|
||||
case (.divider, _), (_, .divider):
|
||||
if fitToWidth {
|
||||
return 10.0
|
||||
} else {
|
||||
return 25.0
|
||||
case (_, .blockQuote), (.blockQuote, _), (_, .pullQuote), (.pullQuote, _):
|
||||
return 27.0
|
||||
case (.kicker, .title), (.cover, .title):
|
||||
return 16.0
|
||||
case (_, .title):
|
||||
return 20.0
|
||||
case (.title, .authorDate), (.subtitle, .authorDate):
|
||||
return 18.0
|
||||
case (_, .authorDate):
|
||||
return 20.0
|
||||
case (.title, .paragraph), (.authorDate, .paragraph):
|
||||
return 34.0
|
||||
case (.header, .paragraph), (.subheader, .paragraph), (.heading, .paragraph):
|
||||
}
|
||||
case (_, .blockQuote), (.blockQuote, _), (_, .pullQuote), (.pullQuote, _):
|
||||
return 27.0
|
||||
case (.kicker, .title), (.cover, .title):
|
||||
return 16.0
|
||||
case (_, .title):
|
||||
return 20.0
|
||||
case (.title, .authorDate), (.subtitle, .authorDate):
|
||||
return 18.0
|
||||
case (_, .authorDate):
|
||||
return 20.0
|
||||
case (.title, .paragraph), (.authorDate, .paragraph):
|
||||
return 34.0
|
||||
case (.header, .paragraph), (.subheader, .paragraph), (.heading, .paragraph):
|
||||
if fitToWidth {
|
||||
return 10.0
|
||||
} else {
|
||||
return 25.0
|
||||
case (.list, .paragraph):
|
||||
return 31.0
|
||||
case (.preformatted, .paragraph):
|
||||
return 19.0
|
||||
case (.formula, .paragraph):
|
||||
return 19.0
|
||||
case (.paragraph, .paragraph):
|
||||
}
|
||||
case (.list, .paragraph):
|
||||
return 31.0
|
||||
case (.preformatted, .paragraph):
|
||||
return 19.0
|
||||
case (.formula, .paragraph):
|
||||
return 19.0
|
||||
case (.paragraph, .paragraph):
|
||||
if fitToWidth {
|
||||
return 10.0
|
||||
} else {
|
||||
return 25.0
|
||||
case (_, .paragraph):
|
||||
return 20.0
|
||||
case (.title, .formula), (.authorDate, .formula):
|
||||
return 34.0
|
||||
case (.header, .formula), (.subheader, .formula), (.heading, .formula):
|
||||
}
|
||||
case (_, .paragraph):
|
||||
return 20.0
|
||||
case (.title, .formula), (.authorDate, .formula):
|
||||
return 34.0
|
||||
case (.header, .formula), (.subheader, .formula), (.heading, .formula):
|
||||
if fitToWidth {
|
||||
return 10.0
|
||||
} else {
|
||||
return 25.0
|
||||
case (.list, .formula):
|
||||
return 31.0
|
||||
case (.preformatted, .formula):
|
||||
return 19.0
|
||||
case (.paragraph, .formula):
|
||||
return 19.0
|
||||
case (_, .formula):
|
||||
return 20.0
|
||||
case (.title, .list), (.authorDate, .list):
|
||||
return 34.0
|
||||
case (.header, .list), (.subheader, .list), (.heading, .list):
|
||||
return 31.0
|
||||
case (.preformatted, .list):
|
||||
return 19.0
|
||||
case (.formula, .list):
|
||||
}
|
||||
case (.list, .formula):
|
||||
return 31.0
|
||||
case (.preformatted, .formula):
|
||||
return 19.0
|
||||
case (.paragraph, .formula):
|
||||
return 19.0
|
||||
case (_, .formula):
|
||||
return 20.0
|
||||
case (.title, .list), (.authorDate, .list):
|
||||
return 34.0
|
||||
case (.header, .list), (.subheader, .list), (.heading, .list):
|
||||
return 31.0
|
||||
case (.preformatted, .list):
|
||||
return 19.0
|
||||
case (.formula, .list):
|
||||
if fitToWidth {
|
||||
return 10.0
|
||||
} else {
|
||||
return 25.0
|
||||
case (_, .list):
|
||||
}
|
||||
case (_, .list):
|
||||
if fitToWidth {
|
||||
return 10.0
|
||||
} else {
|
||||
return 25.0
|
||||
case (.paragraph, .preformatted):
|
||||
return 19.0
|
||||
case (.formula, .preformatted):
|
||||
return 19.0
|
||||
case (_, .preformatted):
|
||||
return 20.0
|
||||
case (_, .header), (_, .subheader), (_, .heading):
|
||||
return 32.0
|
||||
default:
|
||||
return 20.0
|
||||
}
|
||||
case (.paragraph, .preformatted):
|
||||
return 19.0
|
||||
case (.formula, .preformatted):
|
||||
return 19.0
|
||||
case (_, .preformatted):
|
||||
return 20.0
|
||||
case (_, .header), (_, .subheader), (_, .heading):
|
||||
return 32.0
|
||||
default:
|
||||
return 20.0
|
||||
}
|
||||
} else if let lower = lower {
|
||||
} else if let lower {
|
||||
switch lower {
|
||||
case .cover, .channelBanner, .details, .anchor:
|
||||
return 0.0
|
||||
default:
|
||||
case .cover, .channelBanner, .details, .anchor, .table:
|
||||
return 0.0
|
||||
default:
|
||||
if fitToWidth {
|
||||
return 10.0
|
||||
} else {
|
||||
return 25.0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let upper = upper, case .relatedArticles = upper {
|
||||
if let upper, case .relatedArticles = upper {
|
||||
return 0.0
|
||||
} else {
|
||||
return 25.0
|
||||
if fitToWidth {
|
||||
return 5.0
|
||||
} else {
|
||||
return 25.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,27 +2,27 @@ import Foundation
|
|||
import UIKit
|
||||
import SwiftMath
|
||||
|
||||
enum InstantPageMathMode {
|
||||
public enum InstantPageMathMode {
|
||||
case inline
|
||||
case block
|
||||
}
|
||||
|
||||
struct InstantPageMathRenderResult {
|
||||
let image: UIImage
|
||||
let size: CGSize
|
||||
let width: CGFloat
|
||||
let ascent: CGFloat
|
||||
let descent: CGFloat
|
||||
public struct InstantPageMathRenderResult {
|
||||
public let image: UIImage
|
||||
public let size: CGSize
|
||||
public let width: CGFloat
|
||||
public let ascent: CGFloat
|
||||
public let descent: CGFloat
|
||||
}
|
||||
|
||||
final class InstantPageMathAttachment: NSObject {
|
||||
let latex: String
|
||||
let fontSize: CGFloat
|
||||
let textColor: UIColor
|
||||
let mode: InstantPageMathMode
|
||||
let rendered: InstantPageMathRenderResult
|
||||
public final class InstantPageMathAttachment: NSObject {
|
||||
public let latex: String
|
||||
public let fontSize: CGFloat
|
||||
public let textColor: UIColor
|
||||
public let mode: InstantPageMathMode
|
||||
public let rendered: InstantPageMathRenderResult
|
||||
|
||||
init(latex: String, fontSize: CGFloat, textColor: UIColor, mode: InstantPageMathMode, rendered: InstantPageMathRenderResult) {
|
||||
public init(latex: String, fontSize: CGFloat, textColor: UIColor, mode: InstantPageMathMode, rendered: InstantPageMathRenderResult) {
|
||||
self.latex = latex
|
||||
self.fontSize = fontSize
|
||||
self.textColor = textColor
|
||||
|
|
|
|||
124
submodules/InstantPageUI/Sources/InstantPageMediaOpen.swift
Normal file
124
submodules/InstantPageUI/Sources/InstantPageMediaOpen.swift
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import Foundation
|
||||
import Display
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import TelegramUIPreferences
|
||||
import GalleryUI
|
||||
import LocationUI
|
||||
|
||||
/// Routes a media tap (originating from V1's full-page Instant View, or from V2's in-bubble preview)
|
||||
/// to the appropriate viewer:
|
||||
/// - `.geo(map)` → pushes `LocationViewController` via `push`.
|
||||
/// - `.webpage(webPage)` cover → single-entry `InstantPageGalleryController`.
|
||||
/// - `.file(file)` with `isAnimated` → single-entry gallery in "playing video" mode.
|
||||
/// - Default → multi-entry gallery built from `allMedias` (filtered to `.image / .file`),
|
||||
/// centered on the tapped media; "playing video" mode is on.
|
||||
///
|
||||
/// Behavior matches V1's `InstantPageControllerNode.openMedia(_:)` bit-for-bit.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - allMedias: every laid-out media on the page, in laid-out order. Used to build sibling
|
||||
/// entries when the gallery needs them. Callers may pass `[]` for paths that don't need
|
||||
/// siblings (e.g. webpage-cover single-entry gallery), but it's safer to always pass the
|
||||
/// full list — the helper filters/uses it only on the default branch.
|
||||
/// - transitionArgsForMedia: invoked by the gallery presentation to find the source rect for
|
||||
/// the swipe-back animation; return `nil` if the source view is not on screen.
|
||||
/// - hiddenMediaCallback: invoked while the gallery is foregrounded so callers can hide the
|
||||
/// source so the gallery's transitioning image isn't double-visible.
|
||||
public func openInstantPageMedia(
|
||||
media: InstantPageMedia,
|
||||
allMedias: [InstantPageMedia],
|
||||
webPage: TelegramMediaWebpage,
|
||||
context: AccountContext,
|
||||
userLocation: MediaResourceUserLocation,
|
||||
present: (ViewController, Any?) -> Void,
|
||||
push: (ViewController) -> Void,
|
||||
openUrl: @escaping (InstantPageUrlItem) -> Void,
|
||||
baseNavigationController: () -> NavigationController?,
|
||||
transitionArgsForMedia: @escaping (InstantPageMedia) -> GalleryTransitionArguments?,
|
||||
hiddenMediaCallback: @escaping (InstantPageMedia?) -> Void
|
||||
) {
|
||||
if case let .geo(map) = media.media {
|
||||
let controllerParams = LocationViewParams(sendLiveLocation: { _ in
|
||||
}, stopLiveLocation: { _ in
|
||||
}, openUrl: { _ in }, openPeer: { _ in
|
||||
}, showAll: false)
|
||||
|
||||
let peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
|
||||
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peer, text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
|
||||
let controller = LocationViewController(context: context, subject: EngineMessage(message), params: controllerParams)
|
||||
push(controller)
|
||||
return
|
||||
}
|
||||
|
||||
var fromPlayingVideo = false
|
||||
|
||||
var entries: [InstantPageGalleryEntry] = []
|
||||
if case let .webpage(webPage) = media.media {
|
||||
entries.append(InstantPageGalleryEntry(index: 0, pageId: webPage.webpageId, media: media, caption: nil, credit: nil, location: nil))
|
||||
} else if case let .file(file) = media.media, file.isAnimated {
|
||||
fromPlayingVideo = true
|
||||
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil))
|
||||
} else {
|
||||
fromPlayingVideo = true
|
||||
let filteredMedias = allMedias.filter { item in
|
||||
switch item.media {
|
||||
case .image, .file:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for media in filteredMedias {
|
||||
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: InstantPageGalleryEntryLocation(position: Int32(entries.count), totalCount: Int32(filteredMedias.count))))
|
||||
}
|
||||
}
|
||||
|
||||
var centralIndex: Int?
|
||||
for i in 0 ..< entries.count {
|
||||
if entries[i].media == media {
|
||||
centralIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let centralIndex = centralIndex {
|
||||
let controller = InstantPageGalleryController(context: context, userLocation: userLocation, webPage: webPage, entries: entries, centralIndex: centralIndex, fromPlayingVideo: fromPlayingVideo, replaceRootController: { _, _ in
|
||||
}, baseNavigationController: baseNavigationController())
|
||||
let hiddenMediaDisposable = MetaDisposable()
|
||||
hiddenMediaDisposable.set((controller.hiddenMedia |> deliverOnMainQueue).start(next: { entry in
|
||||
hiddenMediaCallback(entry?.media)
|
||||
}))
|
||||
controller.openUrl = openUrl
|
||||
|
||||
// The disposable lives as long as the gallery controller. Bind its lifetime to the
|
||||
// controller by attaching it as an associated object so it survives until dismissal.
|
||||
InstantPageMediaOpenDisposableBox.attach(disposable: hiddenMediaDisposable, to: controller)
|
||||
|
||||
present(controller, InstantPageGalleryControllerPresentationArguments(transitionArguments: { entry -> GalleryTransitionArguments? in
|
||||
return transitionArgsForMedia(entry.media)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Disposable lifetime helper
|
||||
|
||||
/// Holds a `MetaDisposable` that subscribes to the gallery controller's `hiddenMedia` signal.
|
||||
/// Without this, the disposable would deallocate after `openInstantPageMedia` returns and the
|
||||
/// subscription would stop firing. We attach it to the gallery controller via objc associated
|
||||
/// objects so it lives as long as the controller does.
|
||||
private final class InstantPageMediaOpenDisposableBox {
|
||||
private static var key: UInt8 = 0
|
||||
let disposable: MetaDisposable
|
||||
init(_ disposable: MetaDisposable) { self.disposable = disposable }
|
||||
deinit { self.disposable.dispose() }
|
||||
|
||||
static func attach(disposable: MetaDisposable, to controller: ViewController) {
|
||||
let box = InstantPageMediaOpenDisposableBox(disposable)
|
||||
objc_setAssociatedObject(controller, &key, box, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,28 +5,38 @@ import Display
|
|||
import TelegramCore
|
||||
|
||||
public final class InstantPageMultiTextAdapter: ASDisplayNode, TextNodeProtocol {
|
||||
private struct Entry {
|
||||
public struct Entry {
|
||||
public let item: InstantPageTextItem
|
||||
public let frameOrigin: CGPoint
|
||||
|
||||
public init(item: InstantPageTextItem, frameOrigin: CGPoint) {
|
||||
self.item = item
|
||||
self.frameOrigin = frameOrigin
|
||||
}
|
||||
}
|
||||
|
||||
private struct InternalEntry {
|
||||
let item: InstantPageTextItem
|
||||
let charOffset: Int
|
||||
let frameOrigin: CGPoint
|
||||
}
|
||||
|
||||
private let entries: [Entry]
|
||||
private let entries: [InternalEntry]
|
||||
private let combinedString: NSAttributedString
|
||||
|
||||
public init(items: [InstantPageTextItem]) {
|
||||
public init(entries: [Entry]) {
|
||||
let separator = NSAttributedString(string: "\n\n")
|
||||
let combined = NSMutableAttributedString()
|
||||
var entries: [Entry] = []
|
||||
for (index, item) in items.enumerated() {
|
||||
var internalEntries: [InternalEntry] = []
|
||||
for (index, entry) in entries.enumerated() {
|
||||
let charOffset = combined.length
|
||||
entries.append(Entry(item: item, charOffset: charOffset, frameOrigin: item.frame.origin))
|
||||
combined.append(item.attributedString)
|
||||
if index != items.count - 1 {
|
||||
internalEntries.append(InternalEntry(item: entry.item, charOffset: charOffset, frameOrigin: entry.frameOrigin))
|
||||
combined.append(entry.item.attributedString)
|
||||
if index != entries.count - 1 {
|
||||
combined.append(separator)
|
||||
}
|
||||
}
|
||||
self.entries = entries
|
||||
self.entries = internalEntries
|
||||
self.combinedString = combined
|
||||
super.init()
|
||||
self.isUserInteractionEnabled = false
|
||||
|
|
|
|||
2235
submodules/InstantPageUI/Sources/InstantPageRenderer.swift
Normal file
2235
submodules/InstantPageUI/Sources/InstantPageRenderer.swift
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -29,6 +29,11 @@ struct InstantPageTextMarkedItem {
|
|||
let range: NSRange
|
||||
}
|
||||
|
||||
struct InstantPageTextSpoilerItem {
|
||||
let frame: CGRect
|
||||
let range: NSRange
|
||||
}
|
||||
|
||||
struct InstantPageTextStrikethroughItem {
|
||||
let frame: CGRect
|
||||
}
|
||||
|
|
@ -51,6 +56,12 @@ struct InstantPageTextFormulaRun {
|
|||
let attachment: InstantPageMathAttachment
|
||||
}
|
||||
|
||||
struct InstantPageTextEmojiItem {
|
||||
let frame: CGRect
|
||||
let range: NSRange
|
||||
let emoji: ChatTextInputTextCustomEmojiAttribute
|
||||
}
|
||||
|
||||
public struct InstantPageTextAnchorItem {
|
||||
public let name: String
|
||||
public let anchorText: NSAttributedString?
|
||||
|
|
@ -76,22 +87,28 @@ public final class InstantPageTextLine {
|
|||
let strikethroughItems: [InstantPageTextStrikethroughItem]
|
||||
let underlineItems: [InstantPageTextUnderlineItem]
|
||||
let markedItems: [InstantPageTextMarkedItem]
|
||||
let spoilerItems: [InstantPageTextSpoilerItem]
|
||||
let imageItems: [InstantPageTextImageItem]
|
||||
let formulaItems: [InstantPageTextFormulaRun]
|
||||
let emojiItems: [InstantPageTextEmojiItem]
|
||||
public let anchorItems: [InstantPageTextAnchorItem]
|
||||
let isRTL: Bool
|
||||
public let characterRects: [CGRect]? // line-local, one rect per character in `range`; nil = not computed
|
||||
|
||||
init(line: CTLine, range: NSRange, frame: CGRect, strikethroughItems: [InstantPageTextStrikethroughItem], underlineItems: [InstantPageTextUnderlineItem], markedItems: [InstantPageTextMarkedItem], imageItems: [InstantPageTextImageItem], formulaItems: [InstantPageTextFormulaRun], anchorItems: [InstantPageTextAnchorItem], isRTL: Bool) {
|
||||
init(line: CTLine, range: NSRange, frame: CGRect, strikethroughItems: [InstantPageTextStrikethroughItem], underlineItems: [InstantPageTextUnderlineItem], markedItems: [InstantPageTextMarkedItem], spoilerItems: [InstantPageTextSpoilerItem] = [], imageItems: [InstantPageTextImageItem], formulaItems: [InstantPageTextFormulaRun], emojiItems: [InstantPageTextEmojiItem] = [], anchorItems: [InstantPageTextAnchorItem], isRTL: Bool, characterRects: [CGRect]? = nil) {
|
||||
self.line = line
|
||||
self.range = range
|
||||
self.frame = frame
|
||||
self.strikethroughItems = strikethroughItems
|
||||
self.underlineItems = underlineItems
|
||||
self.markedItems = markedItems
|
||||
self.spoilerItems = spoilerItems
|
||||
self.imageItems = imageItems
|
||||
self.formulaItems = formulaItems
|
||||
self.emojiItems = emojiItems
|
||||
self.anchorItems = anchorItems
|
||||
self.isRTL = isRTL
|
||||
self.characterRects = characterRects
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -323,13 +340,16 @@ public final class InstantPageTextItem: InstantPageItem {
|
|||
}
|
||||
|
||||
public func attributesAtPoint(_ point: CGPoint, orNearest: Bool) -> (Int, [NSAttributedString.Key: Any])? {
|
||||
if let direct = self.attributesAtPoint(point) {
|
||||
return direct
|
||||
// Hit-testing (taps on links/entities) wants the character under the finger — keep the
|
||||
// strict, clamping behavior.
|
||||
if !orNearest {
|
||||
return self.attributesAtPoint(point)
|
||||
}
|
||||
guard orNearest, !self.lines.isEmpty else {
|
||||
guard !self.lines.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Selection drags can travel outside the text bounds, so pick the vertically-closest line.
|
||||
let boundsWidth = self.frame.width
|
||||
var nearestLineIndex = 0
|
||||
var nearestDistance = CGFloat.greatestFiniteMagnitude
|
||||
|
|
@ -351,21 +371,31 @@ public final class InstantPageTextItem: InstantPageItem {
|
|||
|
||||
let line = self.lines[nearestLineIndex]
|
||||
let lineFrame = expandedFrameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment)
|
||||
let clampedX = max(lineFrame.minX, min(lineFrame.maxX, point.x))
|
||||
var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: clampedX - lineFrame.minX, y: 0.0))
|
||||
if index == self.attributedString.length {
|
||||
index -= 1
|
||||
} else if index != 0 {
|
||||
var glyphStart: CGFloat = 0.0
|
||||
CTLineGetOffsetForStringIndex(line.line, index, &glyphStart)
|
||||
if clampedX - lineFrame.minX < glyphStart {
|
||||
index -= 1
|
||||
let lineRange = CTLineGetStringRange(line.line)
|
||||
var index: Int
|
||||
if point.x <= lineFrame.minX {
|
||||
index = lineRange.location
|
||||
} else if point.x >= lineFrame.maxX {
|
||||
// Trailing edge: return the line's upper bound (one past its last character) so a
|
||||
// right-handle drag can include the last character/item of the line. The selection
|
||||
// upper bound is exclusive, so clamping to the last character's index — as the strict
|
||||
// path does — would always leave it unselected. Mirrors Display.TextNode.
|
||||
index = lineRange.location + lineRange.length
|
||||
} else {
|
||||
index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: point.x - lineFrame.minX, y: 0.0))
|
||||
if index != 0 {
|
||||
var glyphStart: CGFloat = 0.0
|
||||
CTLineGetOffsetForStringIndex(line.line, index, &glyphStart)
|
||||
if point.x - lineFrame.minX < glyphStart {
|
||||
index -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
guard index >= 0, index < self.attributedString.length else {
|
||||
guard index >= 0, index <= self.attributedString.length else {
|
||||
return nil
|
||||
}
|
||||
return (index, self.attributedString.attributes(at: index, effectiveRange: nil))
|
||||
let attributes = index < self.attributedString.length ? self.attributedString.attributes(at: index, effectiveRange: nil) : [:]
|
||||
return (index, attributes)
|
||||
}
|
||||
|
||||
private func attributeRects(name: NSAttributedString.Key, at index: Int) -> [CGRect]? {
|
||||
|
|
@ -414,8 +444,17 @@ public final class InstantPageTextItem: InstantPageItem {
|
|||
|
||||
public func linkSelectionRects(at point: CGPoint) -> [CGRect] {
|
||||
if let (index, dict) = self.attributesAtPoint(point) {
|
||||
if let _ = dict[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
if let rects = self.attributeRects(name: NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), at: index) {
|
||||
let interactiveKeys = [
|
||||
TelegramTextAttributes.URL,
|
||||
TelegramTextAttributes.PeerMention,
|
||||
TelegramTextAttributes.PeerTextMention,
|
||||
TelegramTextAttributes.BotCommand,
|
||||
TelegramTextAttributes.Hashtag,
|
||||
TelegramTextAttributes.BankCard
|
||||
]
|
||||
for key in interactiveKeys {
|
||||
let attrKey = NSAttributedString.Key(rawValue: key)
|
||||
if dict[attrKey] != nil, let rects = self.attributeRects(name: attrKey, at: index) {
|
||||
return rects.compactMap { rect in
|
||||
if rect.width > 5.0 {
|
||||
return rect.insetBy(dx: 0.0, dy: -3.0)
|
||||
|
|
@ -752,7 +791,8 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
}
|
||||
let extentBuffer = UnsafeMutablePointer<RunStruct>.allocate(capacity: 1)
|
||||
extentBuffer.initialize(to: RunStruct(ascent: 0.0, descent: 0.0, width: dimensions.cgSize.width))
|
||||
var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (pointer) in
|
||||
var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { pointer in
|
||||
pointer.assumingMemoryBound(to: RunStruct.self).deallocate()
|
||||
}, getAscent: { (pointer) -> CGFloat in
|
||||
let d = pointer.assumingMemoryBound(to: RunStruct.self)
|
||||
return d.pointee.ascent
|
||||
|
|
@ -787,7 +827,8 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
}
|
||||
let extentBuffer = UnsafeMutablePointer<RunStruct>.allocate(capacity: 1)
|
||||
extentBuffer.initialize(to: RunStruct(ascent: attachment.rendered.ascent, descent: attachment.rendered.descent, width: attachment.rendered.size.width))
|
||||
var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { _ in
|
||||
var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { pointer in
|
||||
pointer.assumingMemoryBound(to: RunStruct.self).deallocate()
|
||||
}, getAscent: { pointer -> CGFloat in
|
||||
let data = pointer.assumingMemoryBound(to: RunStruct.self)
|
||||
return data.pointee.ascent
|
||||
|
|
@ -817,10 +858,118 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .textAutoUrl(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: text.plainText, webpageId: nil))
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .textAutoEmail(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "mailto:\(text.plainText)", webpageId: nil))
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .textAutoPhone(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "tel:\(text.plainText)", webpageId: nil))
|
||||
styleStack.pop()
|
||||
return result
|
||||
case let .textMention(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
styleStack.pop()
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
mutable.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention), value: mutable.string, range: NSRange(location: 0, length: mutable.length))
|
||||
}
|
||||
return mutable
|
||||
case let .textMentionName(text, peerId):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
styleStack.pop()
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
let mention = TelegramPeerMention(peerId: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(peerId)), mention: mutable.string)
|
||||
mutable.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention), value: mention, range: NSRange(location: 0, length: mutable.length))
|
||||
}
|
||||
return mutable
|
||||
case let .textHashtag(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
styleStack.pop()
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
mutable.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: nil, hashtag: mutable.string), range: NSRange(location: 0, length: mutable.length))
|
||||
}
|
||||
return mutable
|
||||
case let .textCashtag(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
styleStack.pop()
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
mutable.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: nil, hashtag: mutable.string), range: NSRange(location: 0, length: mutable.length))
|
||||
}
|
||||
return mutable
|
||||
case let .textBotCommand(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
styleStack.pop()
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
mutable.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand), value: mutable.string, range: NSRange(location: 0, length: mutable.length))
|
||||
}
|
||||
return mutable
|
||||
case let .textBankCard(text):
|
||||
styleStack.push(.link(false))
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
styleStack.pop()
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
mutable.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard), value: mutable.string, range: NSRange(location: 0, length: mutable.length))
|
||||
}
|
||||
return mutable
|
||||
case let .textCustomEmoji(fileId, _):
|
||||
struct RunStruct {
|
||||
let ascent: CGFloat
|
||||
let descent: CGFloat
|
||||
let width: CGFloat
|
||||
}
|
||||
let attributes = styleStack.textAttributes()
|
||||
let font = (attributes[NSAttributedString.Key.font] as? UIFont) ?? UIFont.systemFont(ofSize: 17.0)
|
||||
let itemSize = font.pointSize * 24.0 / 17.0
|
||||
let extentBuffer = UnsafeMutablePointer<RunStruct>.allocate(capacity: 1)
|
||||
extentBuffer.initialize(to: RunStruct(ascent: font.ascender, descent: font.descender, width: itemSize))
|
||||
var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { pointer in
|
||||
pointer.assumingMemoryBound(to: RunStruct.self).deallocate()
|
||||
}, getAscent: { pointer -> CGFloat in
|
||||
let d = pointer.assumingMemoryBound(to: RunStruct.self)
|
||||
return d.pointee.ascent
|
||||
}, getDescent: { pointer -> CGFloat in
|
||||
let d = pointer.assumingMemoryBound(to: RunStruct.self)
|
||||
return d.pointee.descent
|
||||
}, getWidth: { pointer -> CGFloat in
|
||||
let d = pointer.assumingMemoryBound(to: RunStruct.self)
|
||||
return d.pointee.width
|
||||
})
|
||||
let delegate = CTRunDelegateCreate(&callbacks, extentBuffer)
|
||||
let emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil)
|
||||
let mutableAttributedString = attributedStringForRichText(.plain(" "), styleStack: styleStack, url: url).mutableCopy() as! NSMutableAttributedString
|
||||
mutableAttributedString.addAttributes([
|
||||
kCTRunDelegateAttributeName as NSAttributedString.Key: delegate as Any,
|
||||
ChatTextInputAttributes.customEmoji: emojiAttribute
|
||||
], range: NSMakeRange(0, mutableAttributedString.length))
|
||||
return mutableAttributedString
|
||||
case let .textSpoiler(text):
|
||||
let result = attributedStringForRichText(text, styleStack: styleStack, url: url)
|
||||
let mutable = result.mutableCopy() as! NSMutableAttributedString
|
||||
if mutable.length != 0 {
|
||||
mutable.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler), value: true, range: NSRange(location: 0, length: mutable.length))
|
||||
}
|
||||
return mutable
|
||||
}
|
||||
}
|
||||
|
||||
func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat, horizontalInset: CGFloat = 0.0, alignment: NSTextAlignment = .natural, offset: CGPoint, media: [EngineMedia.Id: EngineMedia] = [:], webpage: TelegramMediaWebpage? = nil, minimizeWidth: Bool = false, maxNumberOfLines: Int = 0, opaqueBackground: Bool = false) -> (InstantPageTextItem?, [InstantPageItem], CGSize) {
|
||||
func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat, horizontalInset: CGFloat = 0.0, alignment: NSTextAlignment = .natural, offset: CGPoint, media: [EngineMedia.Id: EngineMedia] = [:], webpage: TelegramMediaWebpage? = nil, minimizeWidth: Bool = false, fitToWidth: Bool = false, maxNumberOfLines: Int = 0, opaqueBackground: Bool = false) -> (InstantPageTextItem?, [InstantPageItem], CGSize) {
|
||||
if string.length == 0 {
|
||||
return (nil, [], CGSize())
|
||||
}
|
||||
|
|
@ -1091,6 +1240,9 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
|
|||
}
|
||||
|
||||
var textWidth = boundingWidth
|
||||
if fitToWidth {
|
||||
textWidth = maxLineWidth
|
||||
}
|
||||
var requiresScroll = false
|
||||
if (!imageItems.isEmpty || !formulaItems.isEmpty) && maxLineWidth > boundingWidth + 10.0 {
|
||||
textWidth = maxLineWidth
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import PhotoResources
|
||||
import MediaResources
|
||||
|
||||
/// Lightweight inline image view for InstantPage V2 — wraps a `TransformImageNode`
|
||||
/// to render a single `RichText.image` cell inside a text view.
|
||||
///
|
||||
/// Owned by `InstantPageV2View` (not an `InstantPageItemView` conformer; not in
|
||||
/// the view-factory switch). Hosted inside the parent text view's
|
||||
/// `imageContainerView` (sibling of `renderContainer`, above the reveal mask,
|
||||
/// below `emojiContainerView`), so the streaming reveal can wipe text glyphs
|
||||
/// while the image pops in independently. Non-interactive — taps pass through
|
||||
/// to the text view, so a URL-wrapping `RichText.url(text: .image(...))`
|
||||
/// continues to route taps to the URL handler.
|
||||
final class InstantPageV2InlineImageView: UIView {
|
||||
let fileId: Int64
|
||||
private let imageNode: TransformImageNode
|
||||
private let media: EngineMedia
|
||||
private let theme: InstantPageTheme
|
||||
private let fetchedDisposable = MetaDisposable()
|
||||
|
||||
init(media: EngineMedia,
|
||||
webpage: TelegramMediaWebpage?,
|
||||
frame: CGRect,
|
||||
context: AccountContext,
|
||||
userLocation: MediaResourceUserLocation,
|
||||
theme: InstantPageTheme) {
|
||||
self.media = media
|
||||
self.theme = theme
|
||||
self.fileId = media.id?.id ?? 0
|
||||
self.imageNode = TransformImageNode()
|
||||
|
||||
super.init(frame: frame)
|
||||
self.isUserInteractionEnabled = false
|
||||
self.addSubview(self.imageNode.view)
|
||||
|
||||
self.bindSignal(webpage: webpage, context: context, userLocation: userLocation)
|
||||
self.applyLayout(size: frame.size)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
deinit {
|
||||
self.fetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
private func bindSignal(webpage: TelegramMediaWebpage?,
|
||||
context: AccountContext,
|
||||
userLocation: MediaResourceUserLocation) {
|
||||
// Without a webpage we can't form a `WebpageReference` for the standard
|
||||
// chat-message signals, so the image node stays at its empty colour.
|
||||
guard let webpage = webpage else { return }
|
||||
let webPageRef = WebpageReference(webpage)
|
||||
|
||||
switch self.media {
|
||||
case let .image(image):
|
||||
let imageReference = ImageMediaReference.webPage(webPage: webPageRef, media: image)
|
||||
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox,
|
||||
userLocation: userLocation,
|
||||
photoReference: imageReference))
|
||||
// Non-interactive: always auto-fetch so the image arrives without a tap.
|
||||
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context,
|
||||
userLocation: userLocation,
|
||||
photoReference: imageReference,
|
||||
displayAtSize: nil,
|
||||
storeToDownloadsPeerId: nil).start())
|
||||
case let .file(file):
|
||||
let fileReference = FileMediaReference.webPage(webPage: webPageRef, media: file)
|
||||
if file.mimeType.hasPrefix("image/") {
|
||||
self.fetchedDisposable.set(freeMediaFileInteractiveFetched(account: context.account,
|
||||
userLocation: userLocation,
|
||||
fileReference: fileReference).start())
|
||||
self.imageNode.setSignal(instantPageImageFile(account: context.account,
|
||||
userLocation: userLocation,
|
||||
fileReference: fileReference,
|
||||
fetched: true))
|
||||
} else {
|
||||
// Video / animated file: render a single still frame. No play overlay.
|
||||
self.imageNode.setSignal(chatMessageVideo(postbox: context.account.postbox,
|
||||
userLocation: userLocation,
|
||||
videoReference: fileReference))
|
||||
}
|
||||
default:
|
||||
// RichText.image's MediaId resolves to .image or .file in practice; other
|
||||
// EngineMedia kinds (geo, webpage, story, ...) leave the image node blank.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func applyLayout(size: CGSize) {
|
||||
guard size.width > 0, size.height > 0 else { return }
|
||||
self.imageNode.frame = CGRect(origin: .zero, size: size)
|
||||
|
||||
let intrinsic: CGSize
|
||||
switch self.media {
|
||||
case let .image(image):
|
||||
if let largest = largestImageRepresentation(image.representations) {
|
||||
intrinsic = largest.dimensions.cgSize
|
||||
} else {
|
||||
intrinsic = size
|
||||
}
|
||||
case let .file(file):
|
||||
intrinsic = file.dimensions?.cgSize ?? size
|
||||
default:
|
||||
intrinsic = size
|
||||
}
|
||||
|
||||
let imageSize = intrinsic.aspectFilled(size)
|
||||
let arguments = TransformImageArguments(
|
||||
corners: ImageCorners(),
|
||||
imageSize: imageSize,
|
||||
boundingSize: size,
|
||||
intrinsicInsets: UIEdgeInsets(),
|
||||
emptyColor: nil
|
||||
)
|
||||
let apply = self.imageNode.asyncLayout()(arguments)
|
||||
apply()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
if self.imageNode.frame.size != self.bounds.size {
|
||||
self.applyLayout(size: self.bounds.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
2896
submodules/InstantPageUI/Sources/InstantPageV2Layout.swift
Normal file
2896
submodules/InstantPageUI/Sources/InstantPageV2Layout.swift
Normal file
File diff suppressed because it is too large
Load diff
336
submodules/InstantPageUI/Sources/InstantPageV2MediaViews.swift
Normal file
336
submodules/InstantPageUI/Sources/InstantPageV2MediaViews.swift
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import GalleryUI
|
||||
|
||||
// Mutable weak box: lets a wrapper hand its `openMedia` closure a back-reference to itself,
|
||||
// filled in after `super.init` (when `self` becomes usable). SwiftSignalKit's `Weak<T>` requires
|
||||
// a non-optional value at init time, so it can't be used here.
|
||||
private final class WrapperRef {
|
||||
weak var view: UIView?
|
||||
}
|
||||
|
||||
// MARK: - Shared media node factory
|
||||
|
||||
// Hosts a V1 `InstantPageImageNode` inside a V2 UIView wrapper. The caller sizes its own
|
||||
// frame from `item.frame` and adds the returned node's view as a subview.
|
||||
private func makeMediaWrapper(
|
||||
frame: CGRect,
|
||||
media: InstantPageMedia,
|
||||
webPage: TelegramMediaWebpage,
|
||||
attributes: [InstantPageImageAttribute],
|
||||
renderContext: InstantPageV2RenderContext,
|
||||
theme: InstantPageTheme,
|
||||
openMedia: @escaping (InstantPageMedia) -> Void,
|
||||
longPressMedia: @escaping (InstantPageMedia) -> Void
|
||||
) -> InstantPageImageNode {
|
||||
let imageNode = InstantPageImageNode(
|
||||
context: renderContext.context,
|
||||
sourceLocation: renderContext.sourceLocation,
|
||||
theme: theme,
|
||||
webPage: webPage,
|
||||
media: media,
|
||||
attributes: attributes,
|
||||
interactive: true,
|
||||
roundCorners: false,
|
||||
fit: false,
|
||||
openMedia: openMedia,
|
||||
longPressMedia: longPressMedia,
|
||||
activatePinchPreview: nil,
|
||||
pinchPreviewFinished: nil,
|
||||
imageReferenceForMedia: renderContext.imageReference,
|
||||
fileReferenceForMedia: renderContext.fileReference,
|
||||
getPreloadedResource: { _ in nil }
|
||||
)
|
||||
imageNode.frame = CGRect(origin: .zero, size: frame.size)
|
||||
return imageNode
|
||||
}
|
||||
|
||||
// Walks up the superview chain from `start` to find the nearest enclosing `InstantPageV2View`.
|
||||
private func findEnclosingV2View(from start: UIView?) -> InstantPageV2View? {
|
||||
var view: UIView? = start
|
||||
while view != nil {
|
||||
if let v2 = view as? InstantPageV2View {
|
||||
return v2
|
||||
}
|
||||
view = view?.superview
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Registers `wrapper` in the root V2View's `mediaRegistry` under `mediaIndex`. The root is
|
||||
// reached by walking up the superview chain to the nearest `InstantPageV2View`, then walking
|
||||
// its `rootMediaRegistryHost` chain transitively (nested details blocks can leave an inner
|
||||
// body's host pointing at an intermediate body — see `trueRegistryRoot`). No-op if the wrapper
|
||||
// isn't yet attached to a V2View ancestor.
|
||||
private func registerInRootRegistry(wrapper: UIView, mediaIndex: Int) {
|
||||
guard let v2 = findEnclosingV2View(from: wrapper.superview) else { return }
|
||||
v2.trueRegistryRoot.mediaRegistry[mediaIndex] = Weak(wrapper)
|
||||
}
|
||||
|
||||
// Routes a tap on `tapped` through `openInstantPageMedia`, sourcing sibling medias from the
|
||||
// root V2View's `currentLayout`. No-op if the wrapper isn't currently in a V2View tree.
|
||||
private func handleOpenMediaTap(
|
||||
tapped: InstantPageMedia,
|
||||
wrapper: UIView,
|
||||
renderContext: InstantPageV2RenderContext
|
||||
) {
|
||||
guard let v2 = findEnclosingV2View(from: wrapper.superview) else { return }
|
||||
let root = v2.trueRegistryRoot
|
||||
guard let layout = root.currentLayout else { return }
|
||||
openInstantPageMedia(
|
||||
media: tapped,
|
||||
allMedias: layout.allMedias(),
|
||||
webPage: renderContext.webpage,
|
||||
context: renderContext.context,
|
||||
userLocation: renderContext.sourceLocation.userLocation,
|
||||
present: renderContext.present,
|
||||
push: renderContext.push,
|
||||
openUrl: renderContext.openUrl,
|
||||
baseNavigationController: renderContext.baseNavigationController,
|
||||
transitionArgsForMedia: { [weak root] tappedSibling -> GalleryTransitionArguments? in
|
||||
guard let root else { return nil }
|
||||
return root.transitionArgsFor(tappedSibling, addToTransitionSurface: { [weak root] view in
|
||||
root?.superview?.addSubview(view)
|
||||
})
|
||||
},
|
||||
hiddenMediaCallback: { [weak root] hidden in
|
||||
root?.applyHiddenMedia(hidden)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Concrete wrapper classes
|
||||
|
||||
final class InstantPageV2MediaImageView: UIView, InstantPageItemView {
|
||||
private(set) var item: InstantPageV2MediaImageItem
|
||||
var itemFrame: CGRect { return self.item.frame }
|
||||
let wrappedNode: InstantPageImageNode
|
||||
|
||||
init(item: InstantPageV2MediaImageItem, renderContext: InstantPageV2RenderContext, theme: InstantPageTheme) {
|
||||
self.item = item
|
||||
|
||||
// The tap closure can't capture `[weak self]` before `super.init`, so we route through
|
||||
// a `WrapperRef` box that gets filled in after `super.init`. The box's weak storage
|
||||
// breaks the wrapper → wrappedNode → closure → wrapper retain cycle that would otherwise
|
||||
// form (the wrapper owns wrappedNode, which owns the closure, which holds the wrapper).
|
||||
let wrapperRef = WrapperRef()
|
||||
let renderContextRef = renderContext
|
||||
let openMedia: (InstantPageMedia) -> Void = { tapped in
|
||||
guard let wrapper = wrapperRef.view else { return }
|
||||
handleOpenMediaTap(tapped: tapped, wrapper: wrapper, renderContext: renderContextRef)
|
||||
}
|
||||
self.wrappedNode = makeMediaWrapper(
|
||||
frame: item.frame,
|
||||
media: item.media,
|
||||
webPage: item.webPage,
|
||||
attributes: item.attributes,
|
||||
renderContext: renderContext,
|
||||
theme: theme,
|
||||
openMedia: openMedia,
|
||||
longPressMedia: { _ in }
|
||||
)
|
||||
|
||||
super.init(frame: item.frame)
|
||||
self.backgroundColor = .clear
|
||||
self.layer.cornerRadius = item.cornerRadius
|
||||
self.clipsToBounds = item.cornerRadius > 0.0
|
||||
self.addSubview(self.wrappedNode.view)
|
||||
wrapperRef.view = self
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard self.window != nil else { return }
|
||||
registerInRootRegistry(wrapper: self, mediaIndex: self.item.media.index)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
self.wrappedNode.frame = self.bounds
|
||||
}
|
||||
|
||||
func update(item: InstantPageV2MediaImageItem, theme: InstantPageTheme, renderContext: InstantPageV2RenderContext) {
|
||||
self.item = item
|
||||
self.layer.cornerRadius = item.cornerRadius
|
||||
self.clipsToBounds = item.cornerRadius > 0.0
|
||||
let strings = renderContext.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
self.wrappedNode.update(strings: strings, theme: theme)
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantPageV2MediaVideoView: UIView, InstantPageItemView {
|
||||
private(set) var item: InstantPageV2MediaVideoItem
|
||||
var itemFrame: CGRect { return self.item.frame }
|
||||
let wrappedNode: InstantPageImageNode
|
||||
|
||||
init(item: InstantPageV2MediaVideoItem, renderContext: InstantPageV2RenderContext, theme: InstantPageTheme) {
|
||||
self.item = item
|
||||
|
||||
let wrapperRef = WrapperRef()
|
||||
let renderContextRef = renderContext
|
||||
let openMedia: (InstantPageMedia) -> Void = { tapped in
|
||||
guard let wrapper = wrapperRef.view else { return }
|
||||
handleOpenMediaTap(tapped: tapped, wrapper: wrapper, renderContext: renderContextRef)
|
||||
}
|
||||
self.wrappedNode = makeMediaWrapper(
|
||||
frame: item.frame,
|
||||
media: item.media,
|
||||
webPage: item.webPage,
|
||||
attributes: item.attributes,
|
||||
renderContext: renderContext,
|
||||
theme: theme,
|
||||
openMedia: openMedia,
|
||||
longPressMedia: { _ in }
|
||||
)
|
||||
|
||||
super.init(frame: item.frame)
|
||||
self.backgroundColor = .clear
|
||||
self.layer.cornerRadius = item.cornerRadius
|
||||
self.clipsToBounds = item.cornerRadius > 0.0
|
||||
self.addSubview(self.wrappedNode.view)
|
||||
wrapperRef.view = self
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard self.window != nil else { return }
|
||||
registerInRootRegistry(wrapper: self, mediaIndex: self.item.media.index)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
self.wrappedNode.frame = self.bounds
|
||||
}
|
||||
|
||||
func update(item: InstantPageV2MediaVideoItem, theme: InstantPageTheme, renderContext: InstantPageV2RenderContext) {
|
||||
self.item = item
|
||||
self.layer.cornerRadius = item.cornerRadius
|
||||
self.clipsToBounds = item.cornerRadius > 0.0
|
||||
let strings = renderContext.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
self.wrappedNode.update(strings: strings, theme: theme)
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantPageV2MediaMapView: UIView, InstantPageItemView {
|
||||
private(set) var item: InstantPageV2MediaMapItem
|
||||
var itemFrame: CGRect { return self.item.frame }
|
||||
let wrappedNode: InstantPageImageNode
|
||||
|
||||
init(item: InstantPageV2MediaMapItem, renderContext: InstantPageV2RenderContext, theme: InstantPageTheme) {
|
||||
self.item = item
|
||||
|
||||
let wrapperRef = WrapperRef()
|
||||
let renderContextRef = renderContext
|
||||
let openMedia: (InstantPageMedia) -> Void = { tapped in
|
||||
guard let wrapper = wrapperRef.view else { return }
|
||||
handleOpenMediaTap(tapped: tapped, wrapper: wrapper, renderContext: renderContextRef)
|
||||
}
|
||||
self.wrappedNode = makeMediaWrapper(
|
||||
frame: item.frame,
|
||||
media: item.media,
|
||||
webPage: item.webPage,
|
||||
attributes: item.attributes,
|
||||
renderContext: renderContext,
|
||||
theme: theme,
|
||||
openMedia: openMedia,
|
||||
longPressMedia: { _ in }
|
||||
)
|
||||
|
||||
super.init(frame: item.frame)
|
||||
self.backgroundColor = .clear
|
||||
self.layer.cornerRadius = item.cornerRadius
|
||||
self.clipsToBounds = item.cornerRadius > 0.0
|
||||
self.addSubview(self.wrappedNode.view)
|
||||
wrapperRef.view = self
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard self.window != nil else { return }
|
||||
registerInRootRegistry(wrapper: self, mediaIndex: self.item.media.index)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
self.wrappedNode.frame = self.bounds
|
||||
}
|
||||
|
||||
func update(item: InstantPageV2MediaMapItem, theme: InstantPageTheme, renderContext: InstantPageV2RenderContext) {
|
||||
self.item = item
|
||||
self.layer.cornerRadius = item.cornerRadius
|
||||
self.clipsToBounds = item.cornerRadius > 0.0
|
||||
let strings = renderContext.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
self.wrappedNode.update(strings: strings, theme: theme)
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantPageV2MediaCoverImageView: UIView, InstantPageItemView {
|
||||
private(set) var item: InstantPageV2MediaCoverImageItem
|
||||
var itemFrame: CGRect { return self.item.frame }
|
||||
let wrappedNode: InstantPageImageNode
|
||||
|
||||
init(item: InstantPageV2MediaCoverImageItem, renderContext: InstantPageV2RenderContext, theme: InstantPageTheme) {
|
||||
self.item = item
|
||||
|
||||
let wrapperRef = WrapperRef()
|
||||
let renderContextRef = renderContext
|
||||
let openMedia: (InstantPageMedia) -> Void = { tapped in
|
||||
guard let wrapper = wrapperRef.view else { return }
|
||||
handleOpenMediaTap(tapped: tapped, wrapper: wrapper, renderContext: renderContextRef)
|
||||
}
|
||||
self.wrappedNode = makeMediaWrapper(
|
||||
frame: item.frame,
|
||||
media: item.media,
|
||||
webPage: item.webPage,
|
||||
attributes: item.attributes,
|
||||
renderContext: renderContext,
|
||||
theme: theme,
|
||||
openMedia: openMedia,
|
||||
longPressMedia: { _ in }
|
||||
)
|
||||
|
||||
super.init(frame: item.frame)
|
||||
self.backgroundColor = .clear
|
||||
self.layer.cornerRadius = item.cornerRadius
|
||||
self.clipsToBounds = item.cornerRadius > 0.0
|
||||
self.addSubview(self.wrappedNode.view)
|
||||
wrapperRef.view = self
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard self.window != nil else { return }
|
||||
registerInRootRegistry(wrapper: self, mediaIndex: self.item.media.index)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
self.wrappedNode.frame = self.bounds
|
||||
}
|
||||
|
||||
func update(item: InstantPageV2MediaCoverImageItem, theme: InstantPageTheme, renderContext: InstantPageV2RenderContext) {
|
||||
self.item = item
|
||||
self.layer.cornerRadius = item.cornerRadius
|
||||
self.clipsToBounds = item.cornerRadius > 0.0
|
||||
let strings = renderContext.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
self.wrappedNode.update(strings: strings, theme: theme)
|
||||
}
|
||||
}
|
||||
554
submodules/InstantPageUI/Sources/InstantPageV2RevealCost.swift
Normal file
554
submodules/InstantPageUI/Sources/InstantPageV2RevealCost.swift
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import QuartzCore
|
||||
|
||||
/// Opaque per-layout reveal-cost description. Public surface is just `total` (the running
|
||||
/// length used to drive `TextRevealController`); internals are coupled to `applyReveal`'s
|
||||
/// view-tree walk (same module, same file).
|
||||
public struct InstantPageV2RevealCostMap {
|
||||
public let total: Int
|
||||
fileprivate let topLevelEntries: [Entry]
|
||||
}
|
||||
|
||||
extension InstantPageV2RevealCostMap {
|
||||
fileprivate enum Entry {
|
||||
case text(start: Int, end: Int)
|
||||
case nonText(start: Int, end: Int)
|
||||
case details(start: Int, end: Int, body: InstantPageV2RevealCostMap?)
|
||||
case codeBlock(start: Int, end: Int)
|
||||
case table(start: Int, end: Int, rows: [TableRow], title: InstantPageV2RevealCostMap?)
|
||||
}
|
||||
|
||||
fileprivate struct TableRow {
|
||||
let startCount: Int
|
||||
let cells: [InstantPageV2RevealCostMap?]
|
||||
}
|
||||
}
|
||||
|
||||
// Reveal cost is in width units (points along the reading direction). The unit is uniform
|
||||
// across all item kinds, so the streaming reveal pace ("points per second") is visually
|
||||
// consistent — wide tables and media take proportionally longer than narrow inline text.
|
||||
//
|
||||
// For text items, the cost is the sum of glyph ink widths across all lines (the total ink
|
||||
// extent in reading direction). For non-text items, the cost is `item.frame.width`. Zero-width
|
||||
// items (e.g. anchors) contribute 0 and are revealed instantly when the cursor reaches them.
|
||||
|
||||
private func textInkWidth(_ textItem: InstantPageTextItem) -> Int {
|
||||
var total: CGFloat = 0.0
|
||||
for line in textItem.lines {
|
||||
if let chars = line.characterRects {
|
||||
for r in chars where !r.isEmpty {
|
||||
total += r.width
|
||||
}
|
||||
} else {
|
||||
total += line.frame.width
|
||||
}
|
||||
}
|
||||
return max(0, Int(total.rounded()))
|
||||
}
|
||||
|
||||
private func itemWidthCost(_ item: InstantPageV2LaidOutItem) -> Int {
|
||||
return max(0, Int(item.frame.width.rounded()))
|
||||
}
|
||||
|
||||
/// Convert a width-budget (cost in points) into a character count for a text item.
|
||||
/// Walks the item's lines and per-glyph rects accumulating widths until the budget runs out;
|
||||
/// returns the character index of the first glyph not yet fully covered. Used to bridge the
|
||||
/// width-based cost map to the per-character mask API on `InstantPageV2TextView`.
|
||||
private func charCountForWidthBudget(textItem: InstantPageTextItem, widthBudget: Int) -> Int {
|
||||
var remaining = CGFloat(widthBudget)
|
||||
var count = 0
|
||||
for line in textItem.lines {
|
||||
if remaining <= 0 { break }
|
||||
guard let chars = line.characterRects else {
|
||||
let lineWidth = line.frame.width
|
||||
if remaining >= lineWidth {
|
||||
count += line.range.length
|
||||
remaining -= lineWidth
|
||||
} else {
|
||||
let frac = remaining / max(lineWidth, 0.001)
|
||||
count += Int((CGFloat(line.range.length) * frac).rounded(.down))
|
||||
remaining = 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
var lineDone = false
|
||||
for r in chars {
|
||||
let w = max(0, r.width)
|
||||
if remaining >= w {
|
||||
remaining -= w
|
||||
count += 1
|
||||
} else {
|
||||
lineDone = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if lineDone {
|
||||
break
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
public extension InstantPageV2Layout {
|
||||
func computeRevealCostMap() -> InstantPageV2RevealCostMap {
|
||||
var cursor = 0
|
||||
let entries = computeEntries(items: self.items, cursor: &cursor)
|
||||
return InstantPageV2RevealCostMap(total: cursor, topLevelEntries: entries)
|
||||
}
|
||||
}
|
||||
|
||||
public extension InstantPageV2RevealCostMap {
|
||||
/// Size that the layout would occupy if only the items revealed up to `revealedCount`
|
||||
/// were visible. Width is the layout's full content width (we don't shrink horizontally,
|
||||
/// since text wraps to the layout-decided width and shrinking would cause visual jitter).
|
||||
/// Height is the bottom y of the revealed prefix:
|
||||
///
|
||||
/// * Fully-revealed items contribute their full frame.maxY.
|
||||
/// * Partially-revealed text items contribute the bottom y of their last revealed line.
|
||||
/// * Partially-revealed tables contribute up to the last revealed row's bottom y.
|
||||
/// * Non-text items (formula, media, etc.) contribute only once revealedCount >= entry.endCount.
|
||||
/// * `details` / `codeBlock`: the whole container appears as soon as revealedCount > entry.start
|
||||
/// (background and chrome pop in atomically, then inner text reveals char-by-char).
|
||||
///
|
||||
/// Used by the rich-data bubble to size itself to the revealed prefix during AI streaming,
|
||||
/// mirroring TextBubble's `clippedGlyphCountLayout = textLayout.layoutForCharacterCount(...)`.
|
||||
func revealedContentSize(revealedCount: Int, layout: InstantPageV2Layout) -> CGSize {
|
||||
let bounds = computeRevealedBounds(items: layout.items, entries: self.topLevelEntries, revealedCount: revealedCount)
|
||||
if bounds.maxY <= 0.0 {
|
||||
return CGSize(width: layout.contentSize.width, height: 0.0)
|
||||
}
|
||||
// The full layout reserves a closing spacing after the last top-level block (see
|
||||
// `closingSpacing` in layoutBlockSequence). Mirror that so a partially-revealed
|
||||
// bubble has the same bottom padding as a fully-revealed one — otherwise the last
|
||||
// revealed line sits flush against the bubble's bottom edge.
|
||||
let lastItemMaxY = layout.items.map { $0.frame.maxY }.max() ?? 0.0
|
||||
let closingPad = max(0.0, layout.contentSize.height - lastItemMaxY)
|
||||
return CGSize(width: layout.contentSize.width, height: bounds.maxY + closingPad)
|
||||
}
|
||||
|
||||
/// Returns the maxY of revealed items in `layout` coords (no closing pad). Use this to
|
||||
/// size the InstantPageV2View itself so its content never overflows past the revealed
|
||||
/// extent — the bubble's closing pad sits in containerNode space *outside* the pageView,
|
||||
/// not inside it (where unrevealed items would otherwise draw).
|
||||
func revealedItemsMaxY(revealedCount: Int, layout: InstantPageV2Layout) -> CGFloat {
|
||||
let bounds = computeRevealedBounds(items: layout.items, entries: self.topLevelEntries, revealedCount: revealedCount)
|
||||
return max(0.0, bounds.maxY)
|
||||
}
|
||||
}
|
||||
|
||||
private func computeRevealedBounds(items: [InstantPageV2LaidOutItem], entries: [InstantPageV2RevealCostMap.Entry], revealedCount: Int) -> CGRect {
|
||||
var bounds: CGRect = .zero
|
||||
var initialized = false
|
||||
let n = min(items.count, entries.count)
|
||||
for i in 0 ..< n {
|
||||
guard let extent = revealedExtent(entry: entries[i], item: items[i], revealedCount: revealedCount) else {
|
||||
continue
|
||||
}
|
||||
if initialized {
|
||||
bounds = bounds.union(extent)
|
||||
} else {
|
||||
bounds = extent
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
return bounds
|
||||
}
|
||||
|
||||
private func revealedExtent(entry: InstantPageV2RevealCostMap.Entry, item: InstantPageV2LaidOutItem, revealedCount: Int) -> CGRect? {
|
||||
switch entry {
|
||||
case let .text(start, end):
|
||||
if revealedCount <= start { return nil }
|
||||
if revealedCount >= end { return item.frame }
|
||||
if case let .text(textItem) = item {
|
||||
return revealedTextExtent(textItem: textItem.textItem, itemFrame: item.frame, localRevealedCount: revealedCount - start)
|
||||
}
|
||||
return item.frame
|
||||
case let .nonText(start, end):
|
||||
let _ = start
|
||||
if revealedCount < end { return nil }
|
||||
return item.frame
|
||||
case let .codeBlock(start, _):
|
||||
if revealedCount <= start { return nil }
|
||||
// Block backdrop appears atomically once revealing reaches the block; inner text
|
||||
// is char-revealed inside. Bubble height grows by the full block height in one
|
||||
// step rather than mid-block.
|
||||
return item.frame
|
||||
case let .details(start, _, body):
|
||||
if revealedCount <= start { return nil }
|
||||
if case let .details(detailsItem) = item {
|
||||
// Title region appears as soon as the details block is reached.
|
||||
var bounds = CGRect(
|
||||
x: detailsItem.frame.minX,
|
||||
y: detailsItem.frame.minY,
|
||||
width: detailsItem.frame.width,
|
||||
height: detailsItem.titleFrame.maxY
|
||||
)
|
||||
if let body, let innerLayout = detailsItem.innerLayout, detailsItem.isExpanded {
|
||||
let bodyBounds = computeRevealedBounds(items: innerLayout.items, entries: body.topLevelEntries, revealedCount: revealedCount)
|
||||
if !bodyBounds.isEmpty {
|
||||
let bodyAbs = bodyBounds.offsetBy(dx: detailsItem.frame.minX, dy: detailsItem.frame.minY + detailsItem.titleFrame.maxY)
|
||||
bounds = bounds.union(bodyAbs)
|
||||
}
|
||||
}
|
||||
return bounds
|
||||
}
|
||||
return item.frame
|
||||
case let .table(start, end, rows, _):
|
||||
if revealedCount <= start { return nil }
|
||||
if revealedCount >= end { return item.frame }
|
||||
if case let .table(tableItem) = item {
|
||||
let gridOffsetY = tableItem.titleFrame?.height ?? 0.0
|
||||
var lastRevealedRowIndex: Int? = nil
|
||||
for (rowIdx, row) in rows.enumerated() {
|
||||
if revealedCount >= row.startCount {
|
||||
lastRevealedRowIndex = rowIdx
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
let groupedByY = Dictionary(grouping: tableItem.cells, by: { $0.frame.minY })
|
||||
let sortedRowYs = groupedByY.keys.sorted()
|
||||
let heightInTable: CGFloat
|
||||
if let idx = lastRevealedRowIndex, idx < sortedRowYs.count {
|
||||
let rowY = sortedRowYs[idx]
|
||||
let cellsInRow = groupedByY[rowY] ?? []
|
||||
let rowMaxY = cellsInRow.map { $0.frame.maxY }.max() ?? 0.0
|
||||
heightInTable = gridOffsetY + rowMaxY
|
||||
} else {
|
||||
heightInTable = gridOffsetY
|
||||
}
|
||||
return CGRect(
|
||||
x: tableItem.frame.minX,
|
||||
y: tableItem.frame.minY,
|
||||
width: tableItem.frame.width,
|
||||
height: heightInTable
|
||||
)
|
||||
}
|
||||
return item.frame
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the y-extent (= bottom of the last revealed line) of a text item given a
|
||||
/// width-based local cursor. Walks lines summing per-line ink widths; the last line whose
|
||||
/// preceding-line-widths-sum is < cursor is the last revealed line.
|
||||
private func revealedTextExtent(textItem: InstantPageTextItem, itemFrame: CGRect, localRevealedCount: Int) -> CGRect {
|
||||
var remaining = CGFloat(localRevealedCount)
|
||||
var lastRevealedLineMaxY: CGFloat = 0.0
|
||||
for line in textItem.lines {
|
||||
if remaining <= 0 { break }
|
||||
lastRevealedLineMaxY = max(lastRevealedLineMaxY, line.frame.maxY)
|
||||
let lineWidth: CGFloat
|
||||
if let chars = line.characterRects {
|
||||
var sum: CGFloat = 0
|
||||
for r in chars where !r.isEmpty {
|
||||
sum += r.width
|
||||
}
|
||||
lineWidth = sum
|
||||
} else {
|
||||
lineWidth = line.frame.width
|
||||
}
|
||||
remaining -= lineWidth
|
||||
}
|
||||
return CGRect(x: itemFrame.minX, y: itemFrame.minY, width: itemFrame.width, height: lastRevealedLineMaxY)
|
||||
}
|
||||
|
||||
private func computeEntries(items: [InstantPageV2LaidOutItem], cursor: inout Int) -> [InstantPageV2RevealCostMap.Entry] {
|
||||
var entries: [InstantPageV2RevealCostMap.Entry] = []
|
||||
for item in items {
|
||||
switch item {
|
||||
case let .text(text):
|
||||
let start = cursor
|
||||
cursor += textInkWidth(text.textItem)
|
||||
entries.append(.text(start: start, end: cursor))
|
||||
case let .codeBlock(block):
|
||||
let start = cursor
|
||||
cursor += textInkWidth(block.textItem)
|
||||
entries.append(.codeBlock(start: start, end: cursor))
|
||||
case let .details(details):
|
||||
let start = cursor
|
||||
cursor += textInkWidth(details.titleTextItem)
|
||||
var body: InstantPageV2RevealCostMap? = nil
|
||||
if details.isExpanded, let inner = details.innerLayout {
|
||||
var innerCursor = cursor
|
||||
let innerEntries = computeEntries(items: inner.items, cursor: &innerCursor)
|
||||
let innerTotal = innerCursor - cursor
|
||||
cursor = innerCursor
|
||||
body = InstantPageV2RevealCostMap(total: innerTotal, topLevelEntries: innerEntries)
|
||||
}
|
||||
entries.append(.details(start: start, end: cursor, body: body))
|
||||
case let .table(table):
|
||||
let start = cursor
|
||||
|
||||
var titleMap: InstantPageV2RevealCostMap? = nil
|
||||
if let titleLayout = table.titleSubLayout {
|
||||
var titleCursor = cursor
|
||||
let titleEntries = computeEntries(items: titleLayout.items, cursor: &titleCursor)
|
||||
let titleTotal = titleCursor - cursor
|
||||
cursor = titleCursor
|
||||
titleMap = InstantPageV2RevealCostMap(total: titleTotal, topLevelEntries: titleEntries)
|
||||
}
|
||||
|
||||
// Group cells by frame.minY (rows in top-to-bottom order); within each row, left-to-right.
|
||||
let groupedByY = Dictionary(grouping: table.cells, by: { $0.frame.minY })
|
||||
let sortedRowYs = groupedByY.keys.sorted()
|
||||
|
||||
var rows: [InstantPageV2RevealCostMap.TableRow] = []
|
||||
for rowY in sortedRowYs {
|
||||
let cellsInRow = (groupedByY[rowY] ?? []).sorted(by: { $0.frame.minX < $1.frame.minX })
|
||||
let rowStart = cursor
|
||||
var cellMaps: [InstantPageV2RevealCostMap?] = []
|
||||
for cell in cellsInRow {
|
||||
// Each cell consumes at least its frame.width worth of cursor advance,
|
||||
// even if its inner text ink width is smaller (or it has no subLayout
|
||||
// at all). Without this floor, narrow- or empty-cell tables ran through
|
||||
// the cursor much faster than their visual width warrants — a 3-column
|
||||
// table of "1"/"2"/"3" costs ~30pt while occupying ~200pt visually.
|
||||
// Text inside a cell still char-reveals against its own ink widths; the
|
||||
// "extra" cost (cell width − inner ink) is filler time during which the
|
||||
// cell's text is fully revealed and the cursor is moving through padding
|
||||
// before reaching the next cell.
|
||||
let cellWidthFloor = max(0, Int(cell.frame.width.rounded()))
|
||||
if let subLayout = cell.subLayout {
|
||||
var cellCursor = cursor
|
||||
let cellEntries = computeEntries(items: subLayout.items, cursor: &cellCursor)
|
||||
let cellInnerCost = cellCursor - cursor
|
||||
cursor += max(cellInnerCost, cellWidthFloor)
|
||||
cellMaps.append(InstantPageV2RevealCostMap(total: cellInnerCost, topLevelEntries: cellEntries))
|
||||
} else {
|
||||
cellMaps.append(nil)
|
||||
cursor += cellWidthFloor
|
||||
}
|
||||
}
|
||||
rows.append(InstantPageV2RevealCostMap.TableRow(startCount: rowStart, cells: cellMaps))
|
||||
}
|
||||
entries.append(.table(start: start, end: cursor, rows: rows, title: titleMap))
|
||||
case .formula, .mediaImage, .mediaVideo, .mediaMap, .mediaCoverImage, .mediaPlaceholder,
|
||||
.divider, .listMarker, .blockQuoteBar, .shape, .anchor:
|
||||
let start = cursor
|
||||
cursor += itemWidthCost(item)
|
||||
entries.append(.nonText(start: start, end: cursor))
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
public extension InstantPageV2View {
|
||||
/// Push reveal state into the V2 view tree.
|
||||
///
|
||||
/// - `revealedCount == nil` (and `costMap == nil`): clear all reveal state, removing
|
||||
/// masks and showing every item fully.
|
||||
/// - Otherwise: walk `self.itemViews` alongside the cost map's `topLevelEntries`, updating
|
||||
/// text views' per-character reveal, non-text views' visibility, table row masks, and
|
||||
/// recursing into nested V2 views (details body, table cells, table title).
|
||||
///
|
||||
/// `animated` controls non-text visibility cross-fades and table-row-mask growth; per-text-view
|
||||
/// glyph counts are written directly (smoothness comes from the display-link tick frequency).
|
||||
func applyReveal(revealedCount: Int?, costMap: InstantPageV2RevealCostMap?, animated: Bool) {
|
||||
guard let costMap, let revealedCount else {
|
||||
// Clear path
|
||||
for view in self.itemViews {
|
||||
clearRevealOn(view: view, animated: animated)
|
||||
}
|
||||
self.updateEmojiReveal(animated: animated)
|
||||
self.updateImageReveal(animated: animated)
|
||||
return
|
||||
}
|
||||
|
||||
// Walk views + entries in lockstep. The orderings of itemViews and topLevelEntries
|
||||
// match because both are produced by walking layout.items in array order.
|
||||
let entries = costMap.topLevelEntries
|
||||
let entryCount = min(entries.count, self.itemViews.count)
|
||||
for i in 0 ..< entryCount {
|
||||
let view = self.itemViews[i]
|
||||
let entry = entries[i]
|
||||
applyRevealEntry(view: view, entry: entry, revealedCount: revealedCount, animated: animated)
|
||||
}
|
||||
// If counts mismatch (shouldn't in a clean apply right after update(layout:)), clear extras.
|
||||
if self.itemViews.count > entryCount {
|
||||
for i in entryCount ..< self.itemViews.count {
|
||||
clearRevealOn(view: self.itemViews[i], animated: animated)
|
||||
}
|
||||
}
|
||||
self.updateEmojiReveal(animated: animated)
|
||||
self.updateImageReveal(animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyRevealEntry(view: InstantPageItemView, entry: InstantPageV2RevealCostMap.Entry,
|
||||
revealedCount: Int, animated: Bool) {
|
||||
switch entry {
|
||||
case let .text(start, end):
|
||||
if let textView = view as? InstantPageV2TextView {
|
||||
let localWidth = max(0, min(revealedCount - start, end - start))
|
||||
let charCount = (localWidth >= (end - start))
|
||||
? textView.item.textItem.attributedString.length
|
||||
: charCountForWidthBudget(textItem: textView.item.textItem, widthBudget: localWidth)
|
||||
textView.updateRevealCharacterCount(value: charCount, animated: animated)
|
||||
}
|
||||
case let .codeBlock(start, end):
|
||||
if let codeView = view as? InstantPageV2CodeBlockView {
|
||||
let localWidth = max(0, min(revealedCount - start, end - start))
|
||||
let charCount = (localWidth >= (end - start))
|
||||
? codeView.textView.item.textItem.attributedString.length
|
||||
: charCountForWidthBudget(textItem: codeView.textView.item.textItem, widthBudget: localWidth)
|
||||
codeView.textView.updateRevealCharacterCount(value: charCount, animated: animated)
|
||||
let visible = revealedCount >= start
|
||||
applyVisibility(view: codeView, visible: visible, animated: animated)
|
||||
}
|
||||
case let .details(start, end, body):
|
||||
if let detailsView = view as? InstantPageV2DetailsView {
|
||||
let titleTextItem = detailsView.titleTextView.item.textItem
|
||||
let titleWidth = textInkWidth(titleTextItem)
|
||||
let titleLocal = max(0, min(revealedCount - start, titleWidth))
|
||||
let charCount = (titleLocal >= titleWidth)
|
||||
? titleTextItem.attributedString.length
|
||||
: charCountForWidthBudget(textItem: titleTextItem, widthBudget: titleLocal)
|
||||
detailsView.titleTextView.updateRevealCharacterCount(value: charCount, animated: animated)
|
||||
if let body, let bodyView = detailsView.bodyView {
|
||||
bodyView.applyReveal(revealedCount: revealedCount, costMap: body, animated: animated)
|
||||
}
|
||||
let _ = end
|
||||
}
|
||||
case let .table(start, end, rows, title):
|
||||
if let tableView = view as? InstantPageV2TableView {
|
||||
applyTableReveal(tableView: tableView, start: start, end: end, rows: rows, title: title,
|
||||
revealedCount: revealedCount, animated: animated)
|
||||
}
|
||||
case let .nonText(start, end):
|
||||
let visible = revealedCount >= end
|
||||
applyVisibility(view: view, visible: visible, animated: animated)
|
||||
let _ = start
|
||||
}
|
||||
}
|
||||
|
||||
private func applyVisibility(view: UIView, visible: Bool, animated: Bool) {
|
||||
let targetAlpha: CGFloat = visible ? 1.0 : 0.0
|
||||
if !animated {
|
||||
view.alpha = targetAlpha
|
||||
view.isHidden = !visible
|
||||
return
|
||||
}
|
||||
if visible {
|
||||
if view.alpha == 1.0 && !view.isHidden { return }
|
||||
view.isHidden = false
|
||||
let from = view.alpha
|
||||
view.alpha = 1.0
|
||||
let anim = CABasicAnimation(keyPath: "opacity")
|
||||
anim.fromValue = from
|
||||
anim.toValue = 1.0
|
||||
anim.duration = 0.12
|
||||
view.layer.add(anim, forKey: "opacity")
|
||||
} else {
|
||||
if view.alpha == 0.0 || view.isHidden { return }
|
||||
let from = view.alpha
|
||||
view.alpha = 0.0
|
||||
let anim = CABasicAnimation(keyPath: "opacity")
|
||||
anim.fromValue = from
|
||||
anim.toValue = 0.0
|
||||
anim.duration = 0.12
|
||||
view.layer.add(anim, forKey: "opacity")
|
||||
// Don't set isHidden so the animation can run; UIKit treats alpha==0 + hitTest as non-blocking.
|
||||
}
|
||||
}
|
||||
|
||||
private func applyTableReveal(tableView: InstantPageV2TableView, start: Int, end: Int,
|
||||
rows: [InstantPageV2RevealCostMap.TableRow],
|
||||
title: InstantPageV2RevealCostMap?,
|
||||
revealedCount: Int, animated: Bool) {
|
||||
let _ = start
|
||||
let _ = end
|
||||
|
||||
// Title sub-view, if present, recurses with the title's own sub-map.
|
||||
if let title, let titleSub = tableView.titleSubView {
|
||||
titleSub.applyReveal(revealedCount: revealedCount, costMap: title, animated: animated)
|
||||
}
|
||||
|
||||
// Determine which row index the cursor is currently in (or past).
|
||||
// A row is "revealed" once revealedCount >= rowStartCount.
|
||||
var lastRevealedRowIndex: Int? = nil
|
||||
for (idx, row) in rows.enumerated() {
|
||||
if revealedCount >= row.startCount {
|
||||
lastRevealedRowIndex = idx
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into each cell sub-view (each non-nil cell map corresponds to a cellSubView,
|
||||
// in the order they were registered when InstantPageV2TableView was constructed:
|
||||
// cell-sub-views are added in iteration order of `item.cells` for cells with subLayout).
|
||||
var cellSubViewIndex = 0
|
||||
for row in rows {
|
||||
for cellMap in row.cells {
|
||||
if let cellMap, cellSubViewIndex < tableView.cellSubViews.count {
|
||||
let cellSubView = tableView.cellSubViews[cellSubViewIndex]
|
||||
cellSubView.applyReveal(revealedCount: revealedCount, costMap: cellMap, animated: animated)
|
||||
cellSubViewIndex += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the row-mask on contentView. The mask exposes rows [0 ... lastRevealedRowIndex].
|
||||
// Compute the y-bound by taking the max maxY across cells in those rows.
|
||||
let gridOffsetY = tableView.item.titleFrame?.height ?? 0.0
|
||||
let maskHeight: CGFloat
|
||||
if let lastRevealedRowIndex {
|
||||
// Find the maxY among cells in rows [0 ... lastRevealedRowIndex]. Use the actual table item's
|
||||
// cells (the layout-time data, not the cost-map's logical row groupings).
|
||||
let groupedByY = Dictionary(grouping: tableView.item.cells, by: { $0.frame.minY })
|
||||
let sortedRowYs = groupedByY.keys.sorted()
|
||||
let safeIndex = min(lastRevealedRowIndex, sortedRowYs.count - 1)
|
||||
let rowY = sortedRowYs[safeIndex]
|
||||
let cellsInRow = groupedByY[rowY] ?? []
|
||||
let rowMaxY = cellsInRow.map { $0.frame.maxY }.max() ?? 0.0
|
||||
maskHeight = gridOffsetY + rowMaxY
|
||||
} else {
|
||||
// No rows revealed yet — but the title (if any) is still visible above the grid.
|
||||
maskHeight = (title != nil) ? gridOffsetY : 0.0
|
||||
}
|
||||
|
||||
let maskFrame = CGRect(x: 0.0, y: 0.0, width: tableView.contentView.bounds.width, height: maskHeight)
|
||||
|
||||
let maskLayer: CALayer
|
||||
if let existing = tableView.contentView.layer.mask {
|
||||
maskLayer = existing
|
||||
} else {
|
||||
let new = CALayer()
|
||||
new.backgroundColor = UIColor.white.cgColor
|
||||
tableView.contentView.layer.mask = new
|
||||
maskLayer = new
|
||||
}
|
||||
|
||||
if animated && maskLayer.frame != maskFrame {
|
||||
let anim = CABasicAnimation(keyPath: "bounds")
|
||||
anim.fromValue = maskLayer.bounds
|
||||
anim.toValue = CGRect(origin: .zero, size: maskFrame.size)
|
||||
anim.duration = 0.12
|
||||
maskLayer.add(anim, forKey: "bounds")
|
||||
}
|
||||
maskLayer.frame = maskFrame
|
||||
}
|
||||
|
||||
private func clearRevealOn(view: InstantPageItemView, animated: Bool) {
|
||||
if let textView = view as? InstantPageV2TextView {
|
||||
textView.updateRevealCharacterCount(value: nil, animated: animated)
|
||||
}
|
||||
if let codeView = view as? InstantPageV2CodeBlockView {
|
||||
codeView.textView.updateRevealCharacterCount(value: nil, animated: animated)
|
||||
applyVisibility(view: codeView, visible: true, animated: animated)
|
||||
}
|
||||
if let detailsView = view as? InstantPageV2DetailsView {
|
||||
detailsView.titleTextView.updateRevealCharacterCount(value: nil, animated: animated)
|
||||
detailsView.bodyView?.applyReveal(revealedCount: nil, costMap: nil, animated: animated)
|
||||
}
|
||||
if let tableView = view as? InstantPageV2TableView {
|
||||
tableView.titleSubView?.applyReveal(revealedCount: nil, costMap: nil, animated: animated)
|
||||
for cell in tableView.cellSubViews {
|
||||
cell.applyReveal(revealedCount: nil, costMap: nil, animated: animated)
|
||||
}
|
||||
tableView.contentView.layer.mask = nil
|
||||
}
|
||||
// Non-text item: ensure visible.
|
||||
applyVisibility(view: view, visible: true, animated: animated)
|
||||
}
|
||||
|
|
@ -321,7 +321,7 @@ public struct ItemListPeerItemShimmering {
|
|||
}
|
||||
}
|
||||
|
||||
public final class ItemListPeerItem: ListViewItem, ItemListItem {
|
||||
public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem {
|
||||
public enum Context {
|
||||
public final class Custom {
|
||||
public let accountPeerId: EnginePeer.Id
|
||||
|
|
@ -468,6 +468,10 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
|
|||
let disableInteractiveTransitionIfNecessary: Bool
|
||||
let storyStats: EnginePeerStoryStats?
|
||||
let openStories: ((UIView) -> Void)?
|
||||
|
||||
public var hasActiveRevealOptions: Bool {
|
||||
return self.editing.revealed == true
|
||||
}
|
||||
|
||||
public init(
|
||||
presentationData: ItemListPresentationData,
|
||||
|
|
@ -1434,26 +1438,29 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||
let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noCorners
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
let topStripeIsHidden: Bool
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
topStripeIsHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = !item.displayDecorations || hasCorners || !item.hasTopStripe
|
||||
topStripeIsHidden = !item.displayDecorations || hasCorners || !item.hasTopStripe
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
let bottomStripeIsHidden: Bool
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset + editingOffset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
strongSelf.bottomStripeNode.isHidden = !item.displayDecorations
|
||||
bottomStripeIsHidden = !item.displayDecorations
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners || !item.displayDecorations
|
||||
bottomStripeIsHidden = hasCorners || !item.displayDecorations
|
||||
}
|
||||
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
|
||||
|
||||
|
|
@ -1778,7 +1785,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||
}
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)), transition: transition)
|
||||
|
||||
if let presence = item.presence {
|
||||
strongSelf.peerPresenceManager?.reset(presence: presence)
|
||||
|
|
@ -1860,7 +1867,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||
var isHighlighted = false
|
||||
|
||||
var reallyHighlighted: Bool {
|
||||
var reallyHighlighted = self.isHighlighted
|
||||
var reallyHighlighted = self.isHighlighted || self.isRevealOptionsActive
|
||||
if let (item, _, _, _) = self.layoutParams, item.highlighted {
|
||||
reallyHighlighted = true
|
||||
}
|
||||
|
|
@ -1868,39 +1875,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||
}
|
||||
|
||||
func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
|
||||
if self.reallyHighlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if transition.isAnimated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.reallyHighlighted, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
|
|
@ -2002,6 +1977,12 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||
transition.updateFrame(view: avatarIconComponentView, frame: threadIconFrame)
|
||||
}
|
||||
}
|
||||
|
||||
override public func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
|
||||
self.updateIsHighlighted(transition: transition)
|
||||
}
|
||||
|
||||
override public func revealOptionsInteractivelyOpened() {
|
||||
if let (item, _, _, _) = self.layoutParams {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public enum ItemListStickerPackItemControl: Equatable {
|
|||
case check(checked: Bool)
|
||||
}
|
||||
|
||||
public final class ItemListStickerPackItem: ListViewItem, ItemListItem {
|
||||
public final class ItemListStickerPackItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let context: AccountContext
|
||||
let systemStyle: ItemListSystemStyle
|
||||
|
|
@ -56,6 +56,10 @@ public final class ItemListStickerPackItem: ListViewItem, ItemListItem {
|
|||
let removePack: () -> Void
|
||||
let toggleSelected: () -> Void
|
||||
|
||||
public var hasActiveRevealOptions: Bool {
|
||||
return self.editing.revealed
|
||||
}
|
||||
|
||||
public init(presentationData: ItemListPresentationData, context: AccountContext, systemStyle: ItemListSystemStyle = .legacy, packInfo: StickerPackCollectionInfo.Accessor, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, playAnimatedStickers: Bool, style: ItemListStyle = .blocks, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (EngineItemCollectionId?, EngineItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void, toggleSelected: @escaping () -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.context = context
|
||||
|
|
@ -180,6 +184,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
|||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
private var isHighlighted: Bool = false
|
||||
|
||||
override var canBeSelected: Bool {
|
||||
if self.selectableControlNode != nil || self.editableControlNode != nil || self.disabledOverlayNode != nil {
|
||||
|
|
@ -757,26 +762,29 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
|||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
let topStripeIsHidden: Bool
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
topStripeIsHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
topStripeIsHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
let bottomStripeIsHidden: Bool
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset + editingOffset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
bottomStripeIsHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
bottomStripeIsHidden = hasCorners
|
||||
}
|
||||
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
|
||||
|
||||
|
|
@ -870,7 +878,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
|||
strongSelf.imageNode.setSignal(updatedImageSignal)
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))
|
||||
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)), transition: transition)
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
|
|
@ -888,39 +896,8 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
|||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.isHighlighted = highlighted
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
|
|
@ -960,6 +937,12 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
|||
transition.updateFrame(node: animationNode, frame: CGRect(origin: CGPoint(x: params.leftInset + self.revealOffset + editingOffset + 15.0 + floor((boundingSize.width - animationNode.frame.size.width) / 2.0), y: animationNode.frame.minY), size: animationNode.frame.size))
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyOpened() {
|
||||
if let (item, _, _) = self.layoutParams {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ public protocol ItemListItem {
|
|||
var requestsNoInset: Bool { get }
|
||||
}
|
||||
|
||||
public protocol ItemListRevealOptionsStatefulItem: ItemListItem {
|
||||
var hasActiveRevealOptions: Bool { get }
|
||||
}
|
||||
|
||||
public extension ItemListItem {
|
||||
//let accessoryItem: ListViewAccessoryItem?
|
||||
|
||||
|
|
@ -62,10 +66,14 @@ public enum ItemListNeighbor {
|
|||
public struct ItemListNeighbors {
|
||||
public var top: ItemListNeighbor
|
||||
public var bottom: ItemListNeighbor
|
||||
public var topHasActiveRevealOptions: Bool
|
||||
public var bottomHasActiveRevealOptions: Bool
|
||||
|
||||
public init(top: ItemListNeighbor, bottom: ItemListNeighbor) {
|
||||
public init(top: ItemListNeighbor, bottom: ItemListNeighbor, topHasActiveRevealOptions: Bool = false, bottomHasActiveRevealOptions: Bool = false) {
|
||||
self.top = top
|
||||
self.bottom = bottom
|
||||
self.topHasActiveRevealOptions = topHasActiveRevealOptions
|
||||
self.bottomHasActiveRevealOptions = bottomHasActiveRevealOptions
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +116,12 @@ public func itemListNeighbors(item: ItemListItem, topItem: ItemListItem?, bottom
|
|||
bottomNeighbor = .none
|
||||
}
|
||||
|
||||
return ItemListNeighbors(top: topNeighbor, bottom: bottomNeighbor)
|
||||
return ItemListNeighbors(
|
||||
top: topNeighbor,
|
||||
bottom: bottomNeighbor,
|
||||
topHasActiveRevealOptions: (topItem as? ItemListRevealOptionsStatefulItem)?.hasActiveRevealOptions ?? false,
|
||||
bottomHasActiveRevealOptions: (bottomItem as? ItemListRevealOptionsStatefulItem)?.hasActiveRevealOptions ?? false
|
||||
)
|
||||
}
|
||||
|
||||
public func itemListNeighborsPlainInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets {
|
||||
|
|
|
|||
|
|
@ -86,8 +86,17 @@ private let optionExpandedTransitionDistance: CGFloat = 16.0
|
|||
private let optionIconlessTitleHorizontalInset: CGFloat = 10.0
|
||||
private let optionIconAnimationResponse: CGFloat = 18.0
|
||||
private let optionIconAnimationSnapDistance: CGFloat = 0.5
|
||||
private let optionIconAnimationSnapSize: CGFloat = 0.5
|
||||
private let optionIconAnimationSnapAlpha: CGFloat = 0.01
|
||||
|
||||
private extension ItemListRevealOptionIcon {
|
||||
var hasVisualIcon: Bool {
|
||||
switch self {
|
||||
case .none:
|
||||
return false
|
||||
case .image, .animation:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ItemListRevealOptionLayoutMetrics {
|
||||
let shapeSize: CGSize
|
||||
|
|
@ -105,11 +114,11 @@ private struct ItemListRevealOptionLayoutMetrics {
|
|||
return floor((self.slotWidth - self.shapeSize.width) / 2.0)
|
||||
}
|
||||
|
||||
static func metrics(for height: CGFloat) -> ItemListRevealOptionLayoutMetrics {
|
||||
static func metrics(for height: CGFloat, hasVisualIcons: Bool) -> ItemListRevealOptionLayoutMetrics {
|
||||
let regularShapeSize = CGSize(width: 50.0, height: 50.0)
|
||||
let compactShapeSize = CGSize(width: 60.0, height: 32.0)
|
||||
let regularContentHeight = regularShapeSize.height + optionTitleSpacing + ceil(titleFont.lineHeight)
|
||||
if height < regularContentHeight {
|
||||
if height < regularContentHeight || !hasVisualIcons {
|
||||
return ItemListRevealOptionLayoutMetrics(shapeSize: compactShapeSize, slotWidth: 70.0, titleWidth: 70.0, iconMaxSide: 20.0, cornerRadius: 16.0, expandedIconInset: 16.0)
|
||||
} else {
|
||||
return ItemListRevealOptionLayoutMetrics(shapeSize: regularShapeSize, slotWidth: 60.0, titleWidth: 60.0, iconMaxSide: 40.0, cornerRadius: 25.0, expandedIconInset: 20.0)
|
||||
|
|
@ -151,10 +160,6 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
private weak var manuallyAnimatedIconNode: ASDisplayNode?
|
||||
private var currentIconCenter: CGPoint?
|
||||
private var targetIconCenter: CGPoint?
|
||||
private var currentIconSize: CGSize?
|
||||
private var targetIconSize: CGSize?
|
||||
private var currentTitleAlpha: CGFloat?
|
||||
private var targetTitleAlpha: CGFloat?
|
||||
|
||||
private var didApplyLayout = false
|
||||
var isExpanded: Bool = false
|
||||
|
|
@ -250,33 +255,18 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
self.stopManualIconAnimation()
|
||||
}
|
||||
|
||||
private func currentIconPresentationFrame(iconNode: ASDisplayNode) -> CGRect {
|
||||
return iconNode.layer.presentation()?.frame ?? iconNode.frame
|
||||
private func currentIconPresentationCenter(iconNode: ASDisplayNode) -> CGPoint {
|
||||
return iconNode.layer.presentation()?.position ?? iconNode.position
|
||||
}
|
||||
|
||||
private func currentTitlePresentationAlpha() -> CGFloat {
|
||||
if let presentation = self.titleNode.layer.presentation() {
|
||||
return CGFloat(presentation.opacity)
|
||||
} else {
|
||||
return self.titleNode.alpha
|
||||
}
|
||||
}
|
||||
|
||||
private func applyManualIconState(iconNode: ASDisplayNode, center: CGPoint, size: CGSize, titleAlpha: CGFloat) {
|
||||
iconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(center.x - size.width / 2.0), y: floorToScreenPixels(center.y - size.height / 2.0)), size: size)
|
||||
self.titleNode.alpha = titleAlpha
|
||||
}
|
||||
|
||||
private func isManualIconAnimationAtTarget(center: CGPoint, size: CGSize, titleAlpha: CGFloat) -> Bool {
|
||||
guard let targetIconCenter = self.targetIconCenter, let targetIconSize = self.targetIconSize, let targetTitleAlpha = self.targetTitleAlpha else {
|
||||
private func isManualIconAnimationAtTarget(center: CGPoint) -> Bool {
|
||||
guard let targetIconCenter = self.targetIconCenter else {
|
||||
return true
|
||||
}
|
||||
let centerDeltaX = targetIconCenter.x - center.x
|
||||
let centerDeltaY = targetIconCenter.y - center.y
|
||||
let centerDistance = sqrt(centerDeltaX * centerDeltaX + centerDeltaY * centerDeltaY)
|
||||
let sizeDistance = max(abs(targetIconSize.width - size.width), abs(targetIconSize.height - size.height))
|
||||
let alphaDistance = abs(targetTitleAlpha - titleAlpha)
|
||||
return centerDistance <= optionIconAnimationSnapDistance && sizeDistance <= optionIconAnimationSnapSize && alphaDistance <= optionIconAnimationSnapAlpha
|
||||
return centerDistance <= optionIconAnimationSnapDistance
|
||||
}
|
||||
|
||||
private func stopManualIconAnimation() {
|
||||
|
|
@ -286,40 +276,26 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
self.manuallyAnimatedIconNode = nil
|
||||
self.currentIconCenter = nil
|
||||
self.targetIconCenter = nil
|
||||
self.currentIconSize = nil
|
||||
self.targetIconSize = nil
|
||||
self.currentTitleAlpha = nil
|
||||
self.targetTitleAlpha = nil
|
||||
}
|
||||
|
||||
private func updateManualIconAnimation(iconNode: ASDisplayNode, targetFrame: CGRect, targetTitleAlpha: CGFloat, forceImmediate: Bool) {
|
||||
private func updateManualIconCenter(iconNode: ASDisplayNode, targetCenter: CGPoint, forceImmediate: Bool) {
|
||||
iconNode.layer.removeAnimation(forKey: "position")
|
||||
iconNode.layer.removeAnimation(forKey: "bounds")
|
||||
self.titleNode.layer.removeAnimation(forKey: "opacity")
|
||||
|
||||
let targetCenter = frameCenter(targetFrame)
|
||||
let targetSize = targetFrame.size
|
||||
|
||||
if self.manuallyAnimatedIconNode !== iconNode || self.currentIconCenter == nil || self.currentIconSize == nil || self.currentTitleAlpha == nil {
|
||||
let currentFrame = self.currentIconPresentationFrame(iconNode: iconNode)
|
||||
self.currentIconCenter = frameCenter(currentFrame)
|
||||
self.currentIconSize = currentFrame.size
|
||||
self.currentTitleAlpha = self.currentTitlePresentationAlpha()
|
||||
if self.manuallyAnimatedIconNode !== iconNode || self.currentIconCenter == nil {
|
||||
self.currentIconCenter = self.currentIconPresentationCenter(iconNode: iconNode)
|
||||
self.manuallyAnimatedIconNode = iconNode
|
||||
}
|
||||
|
||||
self.targetIconCenter = targetCenter
|
||||
self.targetIconSize = targetSize
|
||||
self.targetTitleAlpha = targetTitleAlpha
|
||||
|
||||
if forceImmediate {
|
||||
self.applyManualIconState(iconNode: iconNode, center: targetCenter, size: targetSize, titleAlpha: targetTitleAlpha)
|
||||
iconNode.position = targetCenter
|
||||
self.stopManualIconAnimation()
|
||||
return
|
||||
}
|
||||
|
||||
if let currentIconCenter = self.currentIconCenter, let currentIconSize = self.currentIconSize, let currentTitleAlpha = self.currentTitleAlpha, self.isManualIconAnimationAtTarget(center: currentIconCenter, size: currentIconSize, titleAlpha: currentTitleAlpha) {
|
||||
self.applyManualIconState(iconNode: iconNode, center: targetCenter, size: targetSize, titleAlpha: targetTitleAlpha)
|
||||
if let currentIconCenter = self.currentIconCenter, self.isManualIconAnimationAtTarget(center: currentIconCenter) {
|
||||
iconNode.position = targetCenter
|
||||
self.stopManualIconAnimation()
|
||||
return
|
||||
}
|
||||
|
|
@ -333,7 +309,7 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
private func tickManualIconAnimation(deltaTime: CGFloat) {
|
||||
guard let iconNode = self.manuallyAnimatedIconNode, let currentIconCenter = self.currentIconCenter, let targetIconCenter = self.targetIconCenter, let currentIconSize = self.currentIconSize, let targetIconSize = self.targetIconSize, let currentTitleAlpha = self.currentTitleAlpha, let targetTitleAlpha = self.targetTitleAlpha else {
|
||||
guard let iconNode = self.manuallyAnimatedIconNode, let currentIconCenter = self.currentIconCenter, let targetIconCenter = self.targetIconCenter else {
|
||||
self.stopManualIconAnimation()
|
||||
return
|
||||
}
|
||||
|
|
@ -344,20 +320,13 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
x: currentIconCenter.x + (targetIconCenter.x - currentIconCenter.x) * progress,
|
||||
y: currentIconCenter.y + (targetIconCenter.y - currentIconCenter.y) * progress
|
||||
)
|
||||
let updatedSize = CGSize(
|
||||
width: currentIconSize.width + (targetIconSize.width - currentIconSize.width) * progress,
|
||||
height: currentIconSize.height + (targetIconSize.height - currentIconSize.height) * progress
|
||||
)
|
||||
let updatedTitleAlpha = currentTitleAlpha + (targetTitleAlpha - currentTitleAlpha) * progress
|
||||
|
||||
if self.isManualIconAnimationAtTarget(center: updatedCenter, size: updatedSize, titleAlpha: updatedTitleAlpha) {
|
||||
self.applyManualIconState(iconNode: iconNode, center: targetIconCenter, size: targetIconSize, titleAlpha: targetTitleAlpha)
|
||||
if self.isManualIconAnimationAtTarget(center: updatedCenter) {
|
||||
iconNode.position = targetIconCenter
|
||||
self.stopManualIconAnimation()
|
||||
} else {
|
||||
self.currentIconCenter = updatedCenter
|
||||
self.currentIconSize = updatedSize
|
||||
self.currentTitleAlpha = updatedTitleAlpha
|
||||
self.applyManualIconState(iconNode: iconNode, center: updatedCenter, size: updatedSize, titleAlpha: updatedTitleAlpha)
|
||||
iconNode.position = updatedCenter
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -400,6 +369,7 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
transition.updateCornerRadius(node: self.backgroundNode, cornerRadius: metrics.cornerRadius)
|
||||
transition.updateCornerRadius(node: self.highlightNode, cornerRadius: metrics.cornerRadius)
|
||||
|
||||
let wasExpanded = self.isExpanded
|
||||
self.isExpanded = isExpanded
|
||||
self.didApplyLayout = true
|
||||
let contentAlpha: CGFloat
|
||||
|
|
@ -413,7 +383,7 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
transition.updateTransform(node: self.contentContainerNode, transform: CGAffineTransform(scaleX: contentScale, y: contentScale))
|
||||
|
||||
let titleAlpha: CGFloat = isPrimary && !self.displaysTitleInsidePill ? (1.0 - expandedProgress) : 1.0
|
||||
var didApplyManualIconAnimation = false
|
||||
var didApplyManualIconCenter = false
|
||||
|
||||
let centeredIconCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX
|
||||
let iconCenterX: CGFloat
|
||||
|
|
@ -440,8 +410,17 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconCenterX - imageSize.width / 2.0), y: floorToScreenPixels(iconCenterY - imageSize.height / 2.0) + 6.0 + self.animationNodeOffset), size: imageSize)
|
||||
|
||||
if isPrimary {
|
||||
didApplyManualIconAnimation = true
|
||||
self.updateManualIconAnimation(iconNode: animationNode, targetFrame: iconFrame, targetTitleAlpha: titleAlpha, forceImmediate: !didApplyLayout || revealProgress < CGFloat.ulpOfOne)
|
||||
didApplyManualIconCenter = true
|
||||
transition.updateBounds(node: animationNode, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
|
||||
let targetCenter = frameCenter(iconFrame)
|
||||
if didApplyLayout && wasExpanded != isExpanded && revealProgress >= CGFloat.ulpOfOne {
|
||||
self.updateManualIconCenter(iconNode: animationNode, targetCenter: targetCenter, forceImmediate: false)
|
||||
} else if self.manuallyAnimatedIconNode === animationNode && self.iconAnimationLink != nil && revealProgress >= CGFloat.ulpOfOne {
|
||||
self.updateManualIconCenter(iconNode: animationNode, targetCenter: targetCenter, forceImmediate: false)
|
||||
} else {
|
||||
self.stopManualIconAnimation()
|
||||
transition.updatePosition(node: animationNode, position: targetCenter)
|
||||
}
|
||||
} else {
|
||||
transition.updateFrame(node: animationNode, frame: iconFrame)
|
||||
}
|
||||
|
|
@ -461,17 +440,26 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
}
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconCenterX - fittedSize.width / 2.0), y: floorToScreenPixels(iconCenterY - fittedSize.height / 2.0)), size: fittedSize)
|
||||
if isPrimary {
|
||||
didApplyManualIconAnimation = true
|
||||
self.updateManualIconAnimation(iconNode: iconNode, targetFrame: iconFrame, targetTitleAlpha: titleAlpha, forceImmediate: !didApplyLayout || revealProgress < CGFloat.ulpOfOne)
|
||||
didApplyManualIconCenter = true
|
||||
transition.updateBounds(node: iconNode, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
|
||||
let targetCenter = frameCenter(iconFrame)
|
||||
if didApplyLayout && wasExpanded != isExpanded && revealProgress >= CGFloat.ulpOfOne {
|
||||
self.updateManualIconCenter(iconNode: iconNode, targetCenter: targetCenter, forceImmediate: false)
|
||||
} else if self.manuallyAnimatedIconNode === iconNode && self.iconAnimationLink != nil && revealProgress >= CGFloat.ulpOfOne {
|
||||
self.updateManualIconCenter(iconNode: iconNode, targetCenter: targetCenter, forceImmediate: false)
|
||||
} else {
|
||||
self.stopManualIconAnimation()
|
||||
transition.updatePosition(node: iconNode, position: targetCenter)
|
||||
}
|
||||
} else {
|
||||
transition.updateFrame(node: iconNode, frame: iconFrame)
|
||||
}
|
||||
}
|
||||
|
||||
if !didApplyManualIconAnimation {
|
||||
if !didApplyManualIconCenter {
|
||||
self.stopManualIconAnimation()
|
||||
transition.updateAlpha(node: self.titleNode, alpha: titleAlpha)
|
||||
}
|
||||
transition.updateAlpha(node: self.titleNode, alpha: titleAlpha)
|
||||
|
||||
let titleFrame: CGRect
|
||||
if self.displaysTitleInsidePill {
|
||||
|
|
@ -484,7 +472,7 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
let metrics = ItemListRevealOptionLayoutMetrics.metrics(for: constrainedSize.height)
|
||||
let metrics = ItemListRevealOptionLayoutMetrics.metrics(for: constrainedSize.height, hasVisualIcons: !self.displaysTitleInsidePill)
|
||||
let _ = self.titleNode.measure(CGSize(width: metrics.titleWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
return CGSize(width: metrics.slotWidth, height: constrainedSize.height)
|
||||
}
|
||||
|
|
@ -561,7 +549,7 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
let metrics = ItemListRevealOptionLayoutMetrics.metrics(for: constrainedSize.height)
|
||||
let metrics = ItemListRevealOptionLayoutMetrics.metrics(for: constrainedSize.height, hasVisualIcons: self.options.contains(where: { $0.icon.hasVisualIcon }))
|
||||
for node in self.optionNodes {
|
||||
let _ = node.measure(constrainedSize)
|
||||
}
|
||||
|
|
@ -579,7 +567,7 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
|
|||
if size.width.isLessThanOrEqualTo(0.0) || self.optionNodes.isEmpty {
|
||||
return
|
||||
}
|
||||
let metrics = ItemListRevealOptionLayoutMetrics.metrics(for: size.height)
|
||||
let metrics = ItemListRevealOptionLayoutMetrics.metrics(for: size.height, hasVisualIcons: self.options.contains(where: { $0.icon.hasVisualIcon }))
|
||||
let revealedDistance = abs(self.revealOffset)
|
||||
let boundedRevealedDistance = min(revealedDistance, size.width)
|
||||
let overswipeDistance = max(0.0, revealedDistance - size.width)
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
|
|||
private let subtitleNode: TextNode
|
||||
|
||||
private var item: ItemListCheckboxItem?
|
||||
private var isHighlighted = false
|
||||
|
||||
override public var controlsContainer: ASDisplayNode {
|
||||
return self.contentParentNode
|
||||
|
|
@ -336,27 +337,31 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
|
|||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
let topStripeIsHidden: Bool
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
topStripeIsHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
topStripeIsHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeIsHidden: Bool
|
||||
if item.zeroSeparatorInsets {
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeIsHidden = false
|
||||
} else {
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
bottomStripeIsHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
bottomStripeIsHidden = hasCorners
|
||||
}
|
||||
}
|
||||
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
|
||||
|
||||
|
|
@ -384,7 +389,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
|
|||
strongSelf.imageNode.image = nil
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))
|
||||
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)))
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
|
|
@ -401,39 +406,8 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
|
|||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted && (self.item?.enabled ?? false) {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.isHighlighted = highlighted && (self.item?.enabled ?? false)
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.4, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
|
|
@ -446,10 +420,16 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.updateRevealOffset(offset: offset, transition: transition)
|
||||
|
||||
|
||||
transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(x: offset, y: self.contentContainerNode.frame.minY), size: self.contentContainerNode.bounds.size))
|
||||
}
|
||||
|
||||
override public func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
|
||||
self.setRevealOptionsOpened(false, animated: true)
|
||||
self.revealOptionsInteractivelyClosed()
|
||||
|
|
|
|||
|
|
@ -69,16 +69,171 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
private var initialRevealOffset: CGFloat = 0.0
|
||||
private var hasActiveRevealGestureOffset: Bool = false
|
||||
public private(set) var revealOffset: CGFloat = 0.0
|
||||
|
||||
public private(set) var isRevealOptionsActive: Bool = false
|
||||
public private(set) var isNextRevealOptionsActive: Bool = false
|
||||
private weak var revealOptionsActivePreviousItemNode: ItemListRevealOptionsItemNode?
|
||||
|
||||
private weak var revealOptionsTopSeparatorNode: ASDisplayNode?
|
||||
private weak var revealOptionsBottomSeparatorNode: ASDisplayNode?
|
||||
private var revealOptionsTopSeparatorBaseIsHidden: Bool = false
|
||||
private var revealOptionsBottomSeparatorBaseIsHidden: Bool = false
|
||||
private var revealOptionsTopSeparatorHiddenByPreviousRevealOptions: Bool = false
|
||||
private var revealOptionsBottomSeparatorHiddenByNextRevealOptions: Bool = false
|
||||
|
||||
private weak var revealOptionsHighlightedBackgroundNode: ASDisplayNode?
|
||||
private var revealOptionsHighlightedBackgroundBaseFrame: CGRect?
|
||||
private var revealOptionsHighlightedBackgroundTracksRevealOffset: Bool = false
|
||||
private var revealOptionsHighlightedBackgroundActiveCornerRadius: CGFloat = 0.0
|
||||
|
||||
private var recognizer: ItemListRevealOptionsGestureRecognizer?
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
private var hapticFeedback: HapticFeedback?
|
||||
|
||||
private var allowAnyDirection = false
|
||||
|
||||
|
||||
public var isDisplayingRevealedOptions: Bool {
|
||||
return !self.revealOffset.isZero
|
||||
}
|
||||
|
||||
private func updateRevealOptionsActive(_ isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
if self.isRevealOptionsActive != isActive {
|
||||
self.isRevealOptionsActive = isActive
|
||||
self.updatePreviousRevealOptionsItemNode(isActive: isActive, transition: transition)
|
||||
self.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateNextRevealOptionsActive(_ isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
if self.isNextRevealOptionsActive != isActive {
|
||||
self.isNextRevealOptionsActive = isActive
|
||||
self.nextRevealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func previousRevealOptionsItemNode() -> ItemListRevealOptionsItemNode? {
|
||||
guard let subnodes = self.supernode?.subnodes else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let index = self.index {
|
||||
var candidate: ItemListRevealOptionsItemNode?
|
||||
for subnode in subnodes {
|
||||
guard let itemNode = subnode as? ItemListRevealOptionsItemNode, itemNode !== self, let itemIndex = itemNode.index, itemIndex < index else {
|
||||
continue
|
||||
}
|
||||
if let currentCandidate = candidate, let candidateIndex = currentCandidate.index {
|
||||
if itemIndex > candidateIndex {
|
||||
candidate = itemNode
|
||||
}
|
||||
} else {
|
||||
candidate = itemNode
|
||||
}
|
||||
}
|
||||
if let candidate = candidate {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
var candidate: ItemListRevealOptionsItemNode?
|
||||
for subnode in subnodes {
|
||||
guard let itemNode = subnode as? ItemListRevealOptionsItemNode, itemNode !== self else {
|
||||
continue
|
||||
}
|
||||
if itemNode.frame.maxY <= self.frame.minY + UIScreenPixel {
|
||||
if let currentCandidate = candidate {
|
||||
if itemNode.frame.maxY > currentCandidate.frame.maxY {
|
||||
candidate = itemNode
|
||||
}
|
||||
} else {
|
||||
candidate = itemNode
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
|
||||
private func updatePreviousRevealOptionsItemNode(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
let previousItemNode = isActive ? self.previousRevealOptionsItemNode() : nil
|
||||
if self.revealOptionsActivePreviousItemNode !== previousItemNode {
|
||||
self.revealOptionsActivePreviousItemNode?.updateNextRevealOptionsActive(false, transition: transition)
|
||||
self.revealOptionsActivePreviousItemNode = previousItemNode
|
||||
}
|
||||
previousItemNode?.updateNextRevealOptionsActive(isActive, transition: transition)
|
||||
if !isActive {
|
||||
self.revealOptionsActivePreviousItemNode = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRevealOptionsSeparatorVisibility() {
|
||||
self.revealOptionsTopSeparatorNode?.isHidden = self.revealOptionsTopSeparatorBaseIsHidden || self.revealOptionsTopSeparatorHiddenByPreviousRevealOptions || self.isRevealOptionsActive
|
||||
self.revealOptionsBottomSeparatorNode?.isHidden = self.revealOptionsBottomSeparatorBaseIsHidden || self.revealOptionsBottomSeparatorHiddenByNextRevealOptions || self.isRevealOptionsActive || self.isNextRevealOptionsActive
|
||||
}
|
||||
|
||||
private func updateRevealOptionsHighlightedBackgroundGeometry(transition: ContainedViewLayoutTransition) {
|
||||
guard let highlightedBackgroundNode = self.revealOptionsHighlightedBackgroundNode, let baseFrame = self.revealOptionsHighlightedBackgroundBaseFrame else {
|
||||
return
|
||||
}
|
||||
|
||||
let frame: CGRect
|
||||
if self.revealOptionsHighlightedBackgroundTracksRevealOffset && self.isRevealOptionsActive {
|
||||
frame = baseFrame.offsetBy(dx: self.revealOffset, dy: 0.0)
|
||||
} else {
|
||||
frame = baseFrame
|
||||
}
|
||||
transition.updateFrame(node: highlightedBackgroundNode, frame: frame)
|
||||
transition.updateCornerRadius(node: highlightedBackgroundNode, cornerRadius: self.isRevealOptionsActive ? self.revealOptionsHighlightedBackgroundActiveCornerRadius : 0.0)
|
||||
}
|
||||
|
||||
public func updateRevealOptionsSeparatorNodes(top: ASDisplayNode?, bottom: ASDisplayNode?, topIsHidden: Bool, bottomIsHidden: Bool, topHiddenByPreviousRevealOptions: Bool = false, bottomHiddenByNextRevealOptions: Bool = false) {
|
||||
self.revealOptionsTopSeparatorNode = top
|
||||
self.revealOptionsBottomSeparatorNode = bottom
|
||||
self.revealOptionsTopSeparatorBaseIsHidden = topIsHidden
|
||||
self.revealOptionsBottomSeparatorBaseIsHidden = bottomIsHidden
|
||||
self.revealOptionsTopSeparatorHiddenByPreviousRevealOptions = topHiddenByPreviousRevealOptions
|
||||
self.revealOptionsBottomSeparatorHiddenByNextRevealOptions = bottomHiddenByNextRevealOptions
|
||||
self.updateRevealOptionsSeparatorVisibility()
|
||||
}
|
||||
|
||||
public func updateRevealOptionsHighlightedBackgroundFrame(_ highlightedBackgroundNode: ASDisplayNode, frame: CGRect, tracksRevealOffset: Bool = true, activeCornerRadius: CGFloat = 26.0, transition: ContainedViewLayoutTransition = .immediate) {
|
||||
self.revealOptionsHighlightedBackgroundNode = highlightedBackgroundNode
|
||||
self.revealOptionsHighlightedBackgroundBaseFrame = frame
|
||||
self.revealOptionsHighlightedBackgroundTracksRevealOffset = tracksRevealOffset
|
||||
self.revealOptionsHighlightedBackgroundActiveCornerRadius = activeCornerRadius
|
||||
self.updateRevealOptionsHighlightedBackgroundGeometry(transition: transition)
|
||||
}
|
||||
|
||||
public func updateRevealOptionsHighlightedBackgroundNode(_ highlightedBackgroundNode: ASDisplayNode, isHighlighted: Bool, transition: ContainedViewLayoutTransition, aboveNodes: [ASDisplayNode?]) {
|
||||
self.revealOptionsHighlightedBackgroundNode = highlightedBackgroundNode
|
||||
if isHighlighted {
|
||||
highlightedBackgroundNode.alpha = 1.0
|
||||
if highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
for node in aboveNodes {
|
||||
if let node = node, node.supernode != nil {
|
||||
anchorNode = node
|
||||
break
|
||||
}
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else if highlightedBackgroundNode.supernode != nil {
|
||||
if transition.isAnimated {
|
||||
highlightedBackgroundNode.layer.animateAlpha(from: highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak highlightedBackgroundNode] completed in
|
||||
if completed {
|
||||
highlightedBackgroundNode?.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
self.updateRevealOptionsHighlightedBackgroundGeometry(transition: transition)
|
||||
}
|
||||
|
||||
override open var canBeSelected: Bool {
|
||||
return !self.isDisplayingRevealedOptions
|
||||
|
|
@ -87,6 +242,10 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
override public init(layerBacked: Bool, rotated: Bool, seeThrough: Bool) {
|
||||
super.init(layerBacked: layerBacked, rotated: rotated, seeThrough: seeThrough)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.revealOptionsActivePreviousItemNode?.updateNextRevealOptionsActive(false, transition: .immediate)
|
||||
}
|
||||
|
||||
open var controlsContainer: ASDisplayNode {
|
||||
return self
|
||||
|
|
@ -385,6 +544,9 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
|
||||
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
self.validLayout = (size, leftInset, rightInset)
|
||||
if self.isRevealOptionsActive {
|
||||
self.updatePreviousRevealOptionsItemNode(isActive: true, transition: .immediate)
|
||||
}
|
||||
|
||||
if let leftRevealNode = self.leftRevealNode {
|
||||
var revealSize = leftRevealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height))
|
||||
|
|
@ -401,6 +563,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
|
||||
open func updateRevealOffsetInternal(offset: CGFloat, transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) {
|
||||
self.revealOffset = offset
|
||||
self.updateRevealOptionsActive(!offset.isZero, transition: transition)
|
||||
guard let (size, leftInset, rightInset) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
|
@ -470,11 +633,21 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
}
|
||||
|
||||
self.updateRevealOffset(offset: offset, transition: transition)
|
||||
self.updateRevealOptionsHighlightedBackgroundGeometry(transition: transition)
|
||||
}
|
||||
|
||||
open func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
|
||||
}
|
||||
|
||||
open func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
self.updateRevealOptionsSeparatorVisibility()
|
||||
self.updateRevealOptionsHighlightedBackgroundGeometry(transition: transition)
|
||||
}
|
||||
|
||||
open func nextRevealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
self.updateRevealOptionsSeparatorVisibility()
|
||||
}
|
||||
|
||||
open func revealOptionsInteractivelyOpened() {
|
||||
|
||||
|
|
|
|||
|
|
@ -596,7 +596,7 @@ private final class LegacyMediaPickerSendActionMenuReferenceContentSource: Conte
|
|||
guard let sourceView = self.sourceView else {
|
||||
return nil
|
||||
}
|
||||
return ContextControllerReferenceViewInfo(referenceView: sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
return ContextControllerReferenceViewInfo(referenceView: sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func faqSearchableItems(context: AccountContext, resolvedUrl: Signal<ResolvedUrl
|
|||
case let .list(items, false):
|
||||
if let currentSection = currentSection {
|
||||
for item in items {
|
||||
if case let .text(itemText, _) = item, case let .url(text, url, _) = itemText {
|
||||
if case let .text(itemText, _, _) = item, case let .url(text, url, _) = itemText {
|
||||
let (_, anchor) = extractAnchor(string: url)
|
||||
guard let anchor else {
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -911,13 +911,9 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
|
|||
}
|
||||
})
|
||||
|
||||
let preferencesKey: EngineRawPostboxViewKey = .preferences(keys: Set([ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]))
|
||||
let preferences = context.account.postbox.combinedView(keys: [preferencesKey])
|
||||
|> map { views -> MediaAutoSaveSettings in
|
||||
guard let view = views.views[preferencesKey] as? EngineRawPreferencesView else {
|
||||
return .default
|
||||
}
|
||||
return view.values[ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]?.get(MediaAutoSaveSettings.self) ?? MediaAutoSaveSettings.default
|
||||
let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings))
|
||||
|> map { entry -> MediaAutoSaveSettings in
|
||||
return entry?.get(MediaAutoSaveSettings.self) ?? MediaAutoSaveSettings.default
|
||||
}
|
||||
|
||||
let autosaveExceptionPeers: Signal<[EnginePeer.Id: EnginePeer?], NoError> = preferences
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ struct ProxySettingsServerItemEditing: Equatable {
|
|||
let revealed: Bool
|
||||
}
|
||||
|
||||
final class ProxySettingsServerItem: ListViewItem, ItemListItem {
|
||||
final class ProxySettingsServerItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let systemStyle: ItemListSystemStyle
|
||||
|
|
@ -34,6 +34,10 @@ final class ProxySettingsServerItem: ListViewItem, ItemListItem {
|
|||
let infoAction: () -> Void
|
||||
let setServerWithRevealedOptions: (ProxyServerSettings?, ProxyServerSettings?) -> Void
|
||||
let removeServer: (ProxyServerSettings) -> Void
|
||||
|
||||
var hasActiveRevealOptions: Bool {
|
||||
return self.editing.revealed
|
||||
}
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, server: ProxyServerSettings, activity: Bool, active: Bool, color: ItemListCheckboxItemColor, label: String, labelAccent: Bool, editing: ProxySettingsServerItemEditing, sectionId: ItemListSectionId, action: @escaping () -> Void, infoAction: @escaping () -> Void, setServerWithRevealedOptions: @escaping (ProxyServerSettings?, ProxyServerSettings?) -> Void, removeServer: @escaping (ProxyServerSettings) -> Void) {
|
||||
self.theme = theme
|
||||
|
|
@ -123,6 +127,7 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
private var item: ProxySettingsServerItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
private var isHighlighted = false
|
||||
|
||||
override var canBeSelected: Bool {
|
||||
if self.editableControlNode != nil {
|
||||
|
|
@ -383,26 +388,29 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode {
|
|||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
let topStripeIsHidden: Bool
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
topStripeIsHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
topStripeIsHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
let bottomStripeIsHidden: Bool
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset + editingOffset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
bottomStripeIsHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
bottomStripeIsHidden = hasCorners
|
||||
}
|
||||
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
|
||||
|
||||
|
|
@ -432,7 +440,7 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode {
|
|||
strongSelf.infoButtonNode.isUserInteractionEnabled = revealOffset.isZero && !item.editing.editing
|
||||
strongSelf.infoButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 55.0, y: 0.0), size: CGSize(width: 55.0, height: layout.contentSize.height))
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)), transition: transition)
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
|
|
@ -446,39 +454,8 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode {
|
|||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.isHighlighted = highlighted
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.4, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
|
|
@ -522,10 +499,16 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode {
|
|||
var infoIconFrame = self.infoIconNode.frame
|
||||
infoIconFrame.origin.x = offset + params.width - params.rightInset - 55.0 + floor((55.0 - infoIconFrame.width) / 2.0)
|
||||
transition.updateFrame(node: self.infoIconNode, frame: infoIconFrame)
|
||||
|
||||
|
||||
self.infoButtonNode.isUserInteractionEnabled = offset.isZero
|
||||
}
|
||||
|
||||
|
||||
override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyOpened() {
|
||||
if let item = self.item {
|
||||
item.setServerWithRevealedOptions(item.server, nil)
|
||||
|
|
|
|||
|
|
@ -514,13 +514,9 @@ public func saveIncomingMediaController(context: AccountContext, scope: SaveInco
|
|||
controller.peerSelected = { [weak controller] peer, _ in
|
||||
let peerId = peer.id
|
||||
|
||||
let preferencesKey: EngineRawPostboxViewKey = .preferences(keys: Set([ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]))
|
||||
let preferences = context.account.postbox.combinedView(keys: [preferencesKey])
|
||||
|> map { views -> MediaAutoSaveSettings in
|
||||
guard let view = views.views[preferencesKey] as? EngineRawPreferencesView else {
|
||||
return .default
|
||||
}
|
||||
return view.values[ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]?.get(MediaAutoSaveSettings.self) ?? MediaAutoSaveSettings.default
|
||||
let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings))
|
||||
|> map { entry -> MediaAutoSaveSettings in
|
||||
return entry?.get(MediaAutoSaveSettings.self) ?? MediaAutoSaveSettings.default
|
||||
}
|
||||
|
||||
let _ = (preferences
|
||||
|
|
@ -621,13 +617,9 @@ public func saveIncomingMediaController(context: AccountContext, scope: SaveInco
|
|||
}
|
||||
)
|
||||
|
||||
let preferencesKey: EngineRawPostboxViewKey = .preferences(keys: Set([ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]))
|
||||
let preferences = context.account.postbox.combinedView(keys: [preferencesKey])
|
||||
|> map { views -> MediaAutoSaveSettings in
|
||||
guard let view = views.views[preferencesKey] as? EngineRawPreferencesView else {
|
||||
return .default
|
||||
}
|
||||
return view.values[ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]?.get(MediaAutoSaveSettings.self) ?? MediaAutoSaveSettings.default
|
||||
let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings))
|
||||
|> map { entry -> MediaAutoSaveSettings in
|
||||
return entry?.get(MediaAutoSaveSettings.self) ?? MediaAutoSaveSettings.default
|
||||
}
|
||||
|
||||
let peer: Signal<(EnginePeer?, EnginePeer.Presence?), NoError>
|
||||
|
|
|
|||
|
|
@ -230,17 +230,9 @@ public func storageUsageExceptionsScreen(
|
|||
return cacheSettings
|
||||
})
|
||||
|
||||
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
|
||||
let accountSpecificSettings: Signal<AccountSpecificCacheStorageSettings, NoError> = context.account.postbox.combinedView(keys: [viewKey])
|
||||
|> map { views -> AccountSpecificCacheStorageSettings in
|
||||
let cacheSettings: AccountSpecificCacheStorageSettings
|
||||
if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
|
||||
cacheSettings = value
|
||||
} else {
|
||||
cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
|
||||
}
|
||||
|
||||
return cacheSettings
|
||||
let accountSpecificSettings: Signal<AccountSpecificCacheStorageSettings, NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.accountSpecificCacheStorageSettings))
|
||||
|> map { entry -> AccountSpecificCacheStorageSettings in
|
||||
return entry?.get(AccountSpecificCacheStorageSettings.self) ?? AccountSpecificCacheStorageSettings.defaultSettings
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ enum ItemListRecentSessionItemText {
|
|||
case none
|
||||
}
|
||||
|
||||
final class ItemListRecentSessionItem: ListViewItem, ItemListItem {
|
||||
final class ItemListRecentSessionItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let systemStyle: ItemListSystemStyle
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
|
|
@ -47,6 +47,10 @@ final class ItemListRecentSessionItem: ListViewItem, ItemListItem {
|
|||
let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void
|
||||
let removeSession: (Int64) -> Void
|
||||
let action: (() -> Void)?
|
||||
|
||||
var hasActiveRevealOptions: Bool {
|
||||
return self.revealed
|
||||
}
|
||||
|
||||
init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editable: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, action: (() -> Void)?) {
|
||||
self.presentationData = presentationData
|
||||
|
|
@ -193,6 +197,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode {
|
|||
private var layoutParams: (ItemListRecentSessionItem, ListViewItemLayoutParams, ItemListNeighbors)?
|
||||
|
||||
private var editableControlNode: ItemListEditableControlNode?
|
||||
private var isHighlighted = false
|
||||
|
||||
override public var canBeSelected: Bool {
|
||||
if let item = self.layoutParams?.0, let _ = item.action {
|
||||
|
|
@ -473,26 +478,29 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode {
|
|||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
let topStripeIsHidden: Bool
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
topStripeIsHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
topStripeIsHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
let bottomStripeIsHidden: Bool
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset + editingOffset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
bottomStripeIsHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
bottomStripeIsHidden = hasCorners
|
||||
}
|
||||
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
|
||||
|
||||
|
|
@ -507,7 +515,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode {
|
|||
transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: appLayout.size))
|
||||
transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.appNode.frame.maxY + textSpacing), size: locationLayout.size))
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))), transition: transition)
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
|
|
@ -521,39 +529,8 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode {
|
|||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted && (self.layoutParams?.0.enabled ?? false) {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.isHighlighted = highlighted && (self.layoutParams?.0.enabled ?? false)
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
|
|
@ -588,6 +565,12 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode {
|
|||
transition.updateFrame(node: self.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.appNode.frame.minY), size: self.appNode.bounds.size))
|
||||
transition.updateFrame(node: self.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.locationNode.frame.minY), size: self.locationNode.bounds.size))
|
||||
}
|
||||
|
||||
override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyOpened() {
|
||||
if let (item, _, _) = self.layoutParams {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ struct ItemListWebsiteItemEditing: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
final class ItemListWebsiteItem: ListViewItem, ItemListItem {
|
||||
final class ItemListWebsiteItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem {
|
||||
let context: AccountContext
|
||||
let presentationData: ItemListPresentationData
|
||||
let systemStyle: ItemListSystemStyle
|
||||
|
|
@ -44,6 +44,10 @@ final class ItemListWebsiteItem: ListViewItem, ItemListItem {
|
|||
let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void
|
||||
let removeSession: (Int64) -> Void
|
||||
let action: (() -> Void)?
|
||||
|
||||
var hasActiveRevealOptions: Bool {
|
||||
return self.revealed
|
||||
}
|
||||
|
||||
init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: EnginePeer?, enabled: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, action: (() -> Void)?) {
|
||||
self.context = context
|
||||
|
|
@ -142,6 +146,7 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode {
|
|||
private var layoutParams: (ItemListWebsiteItem, ListViewItemLayoutParams, ItemListNeighbors)?
|
||||
|
||||
private var editableControlNode: ItemListEditableControlNode?
|
||||
private var isHighlighted = false
|
||||
|
||||
override public var canBeSelected: Bool {
|
||||
if let item = self.layoutParams?.0, let _ = item.action {
|
||||
|
|
@ -410,26 +415,29 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode {
|
|||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
let topStripeIsHidden: Bool
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
topStripeIsHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
topStripeIsHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
let bottomStripeIsHidden: Bool
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset + editingOffset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
bottomStripeIsHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
bottomStripeIsHidden = hasCorners
|
||||
}
|
||||
strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
|
||||
|
||||
|
|
@ -446,7 +454,7 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode {
|
|||
transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: appLayout.size))
|
||||
transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.appNode.frame.maxY + textSpacing), size: locationLayout.size))
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))), transition: transition)
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
|
|
@ -460,39 +468,8 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode {
|
|||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted && (self.layoutParams?.0.enabled ?? false) {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.isHighlighted = highlighted && (self.layoutParams?.0.enabled ?? false)
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
|
|
@ -527,6 +504,12 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode {
|
|||
transition.updateFrame(node: self.appNode, frame: CGRect(origin: CGPoint(x: leftInset + self.revealOffset + editingOffset, y: self.appNode.frame.minY), size: self.appNode.bounds.size))
|
||||
transition.updateFrame(node: self.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + self.revealOffset + editingOffset, y: self.locationNode.frame.minY), size: self.locationNode.bounds.size))
|
||||
}
|
||||
|
||||
override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyOpened() {
|
||||
if let (item, _, _) = self.layoutParams {
|
||||
|
|
@ -657,6 +640,7 @@ private final class ItemListConnectedBotSessionItemNode: ItemListRevealOptionsIt
|
|||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var layoutParams: (ItemListConnectedBotSessionItem, ListViewItemLayoutParams, ItemListNeighbors)?
|
||||
private var isHighlighted = false
|
||||
|
||||
override public var canBeSelected: Bool {
|
||||
if let item = self.layoutParams?.0, let _ = item.action {
|
||||
|
|
@ -856,26 +840,29 @@ private final class ItemListConnectedBotSessionItemNode: ItemListRevealOptionsIt
|
|||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
let topStripeIsHidden: Bool
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
self.topStripeNode.isHidden = true
|
||||
topStripeIsHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
self.topStripeNode.isHidden = hasCorners
|
||||
topStripeIsHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
let bottomStripeIsHidden: Bool
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
self.bottomStripeNode.isHidden = false
|
||||
bottomStripeIsHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
self.bottomStripeNode.isHidden = hasCorners
|
||||
bottomStripeIsHidden = hasCorners
|
||||
}
|
||||
self.updateRevealOptionsSeparatorNodes(top: self.topStripeNode, bottom: self.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions)
|
||||
|
||||
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
|
||||
|
||||
|
|
@ -890,47 +877,26 @@ private final class ItemListConnectedBotSessionItemNode: ItemListRevealOptionsIt
|
|||
transition.updateFrame(node: self.appNode, frame: CGRect(origin: CGPoint(x: leftInset, y: self.titleNode.frame.maxY + titleSpacing), size: appLayout.size))
|
||||
transition.updateFrame(node: self.locationNode, frame: CGRect(origin: CGPoint(x: leftInset, y: self.appNode.frame.maxY + textSpacing), size: locationLayout.size))
|
||||
|
||||
self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
self.updateRevealOptionsHighlightedBackgroundFrame(self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))), transition: transition)
|
||||
self.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
self.setRevealOptions((left: [], right: []))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted && (self.layoutParams?.0.enabled ?? false) {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
} else if self.backgroundNode.supernode != nil {
|
||||
anchorNode = self.backgroundNode
|
||||
}
|
||||
if let anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let self, completed {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
self.isHighlighted = highlighted && (self.layoutParams?.0.enabled ?? false)
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.4, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
|
||||
override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) {
|
||||
super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition)
|
||||
|
||||
self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode])
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[1815593308] = { return Api.DocumentAttribute.parse_documentAttributeImageSize($0) }
|
||||
dict[1662637586] = { return Api.DocumentAttribute.parse_documentAttributeSticker($0) }
|
||||
dict[1137015880] = { return Api.DocumentAttribute.parse_documentAttributeVideo($0) }
|
||||
dict[-1763006997] = { return Api.DraftMessage.parse_draftMessage($0) }
|
||||
dict[-1743452271] = { return Api.DraftMessage.parse_draftMessage($0) }
|
||||
dict[453805082] = { return Api.DraftMessage.parse_draftMessageEmpty($0) }
|
||||
dict[-1764723459] = { return Api.EmailVerification.parse_emailVerificationApple($0) }
|
||||
dict[-1842457175] = { return Api.EmailVerification.parse_emailVerificationCode($0) }
|
||||
|
|
@ -443,6 +443,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[1548122514] = { return Api.InputNotifyPeer.parse_inputNotifyForumTopic($0) }
|
||||
dict[-1195615476] = { return Api.InputNotifyPeer.parse_inputNotifyPeer($0) }
|
||||
dict[423314455] = { return Api.InputNotifyPeer.parse_inputNotifyUsers($0) }
|
||||
dict[2105227266] = { return Api.InputPageListOrderedItem.parse_inputPageListOrderedItemBlocks($0) }
|
||||
dict[-1682665696] = { return Api.InputPageListOrderedItem.parse_inputPageListOrderedItemText($0) }
|
||||
dict[1528613672] = { return Api.InputPasskeyCredential.parse_inputPasskeyCredentialFirebasePNV($0) }
|
||||
dict[1009235855] = { return Api.InputPasskeyCredential.parse_inputPasskeyCredentialPublicKey($0) }
|
||||
dict[-1021329078] = { return Api.InputPasskeyResponse.parse_inputPasskeyResponseLogin($0) }
|
||||
|
|
@ -493,6 +495,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[1003796418] = { return Api.InputReplyTo.parse_inputReplyToMessage($0) }
|
||||
dict[1775660101] = { return Api.InputReplyTo.parse_inputReplyToMonoForum($0) }
|
||||
dict[1484862010] = { return Api.InputReplyTo.parse_inputReplyToStory($0) }
|
||||
dict[-456898052] = { return Api.InputRichMessage.parse_inputRichMessage($0) }
|
||||
dict[-722815663] = { return Api.InputRichMessage.parse_inputRichMessageHTML($0) }
|
||||
dict[162300294] = { return Api.InputRichMessage.parse_inputRichMessageMarkdown($0) }
|
||||
dict[-251549057] = { return Api.InputSavedStarGift.parse_inputSavedStarGiftChat($0) }
|
||||
dict[545636920] = { return Api.InputSavedStarGift.parse_inputSavedStarGiftSlug($0) }
|
||||
dict[1764202389] = { return Api.InputSavedStarGift.parse_inputSavedStarGiftUser($0) }
|
||||
|
|
@ -589,7 +594,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) }
|
||||
dict[1235637404] = { return Api.MediaArea.parse_mediaAreaWeather($0) }
|
||||
dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) }
|
||||
dict[-1779470549] = { return Api.Message.parse_message($0) }
|
||||
dict[1979759059] = { return Api.Message.parse_message($0) }
|
||||
dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) }
|
||||
dict[2055212554] = { return Api.Message.parse_messageService($0) }
|
||||
dict[-872240531] = { return Api.MessageAction.parse_messageActionBoostApply($0) }
|
||||
|
|
@ -750,6 +755,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-1261946036] = { return Api.NotifyPeer.parse_notifyUsers($0) }
|
||||
dict[1001931436] = { return Api.OutboxReadDate.parse_outboxReadDate($0) }
|
||||
dict[-1738178803] = { return Api.Page.parse_page($0) }
|
||||
dict[1464557951] = { return Api.PageBlock.parse_inputPageBlockMap($0) }
|
||||
dict[-1186155733] = { return Api.PageBlock.parse_inputPageBlockOrderedList($0) }
|
||||
dict[-837994576] = { return Api.PageBlock.parse_pageBlockAnchor($0) }
|
||||
dict[-2143067670] = { return Api.PageBlock.parse_pageBlockAudio($0) }
|
||||
dict[-1162877472] = { return Api.PageBlock.parse_pageBlockAuthorDate($0) }
|
||||
|
|
@ -763,9 +770,16 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-229005301] = { return Api.PageBlock.parse_pageBlockEmbedPost($0) }
|
||||
dict[1216809369] = { return Api.PageBlock.parse_pageBlockFooter($0) }
|
||||
dict[-1076861716] = { return Api.PageBlock.parse_pageBlockHeader($0) }
|
||||
dict[-1157691601] = { return Api.PageBlock.parse_pageBlockHeading1($0) }
|
||||
dict[158018284] = { return Api.PageBlock.parse_pageBlockHeading2($0) }
|
||||
dict[1743204781] = { return Api.PageBlock.parse_pageBlockHeading3($0) }
|
||||
dict[-1254983893] = { return Api.PageBlock.parse_pageBlockHeading4($0) }
|
||||
dict[-608277398] = { return Api.PageBlock.parse_pageBlockHeading5($0) }
|
||||
dict[1747599785] = { return Api.PageBlock.parse_pageBlockHeading6($0) }
|
||||
dict[504660880] = { return Api.PageBlock.parse_pageBlockKicker($0) }
|
||||
dict[-454524911] = { return Api.PageBlock.parse_pageBlockList($0) }
|
||||
dict[-1538310410] = { return Api.PageBlock.parse_pageBlockMap($0) }
|
||||
dict[1493699616] = { return Api.PageBlock.parse_pageBlockMath($0) }
|
||||
dict[-1702174239] = { return Api.PageBlock.parse_pageBlockOrderedList($0) }
|
||||
dict[1182402406] = { return Api.PageBlock.parse_pageBlockParagraph($0) }
|
||||
dict[391759200] = { return Api.PageBlock.parse_pageBlockPhoto($0) }
|
||||
|
|
@ -776,14 +790,15 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-248793375] = { return Api.PageBlock.parse_pageBlockSubheader($0) }
|
||||
dict[-1879401953] = { return Api.PageBlock.parse_pageBlockSubtitle($0) }
|
||||
dict[-1085412734] = { return Api.PageBlock.parse_pageBlockTable($0) }
|
||||
dict[1009361890] = { return Api.PageBlock.parse_pageBlockThinking($0) }
|
||||
dict[1890305021] = { return Api.PageBlock.parse_pageBlockTitle($0) }
|
||||
dict[324435594] = { return Api.PageBlock.parse_pageBlockUnsupported($0) }
|
||||
dict[2089805750] = { return Api.PageBlock.parse_pageBlockVideo($0) }
|
||||
dict[1869903447] = { return Api.PageCaption.parse_pageCaption($0) }
|
||||
dict[635466748] = { return Api.PageListItem.parse_pageListItemBlocks($0) }
|
||||
dict[-1188055347] = { return Api.PageListItem.parse_pageListItemText($0) }
|
||||
dict[-1730311882] = { return Api.PageListOrderedItem.parse_pageListOrderedItemBlocks($0) }
|
||||
dict[1577484359] = { return Api.PageListOrderedItem.parse_pageListOrderedItemText($0) }
|
||||
dict[1674209194] = { return Api.PageListItem.parse_pageListItemBlocks($0) }
|
||||
dict[794323004] = { return Api.PageListItem.parse_pageListItemText($0) }
|
||||
dict[1109995988] = { return Api.PageListOrderedItem.parse_pageListOrderedItemBlocks($0) }
|
||||
dict[-851533770] = { return Api.PageListOrderedItem.parse_pageListOrderedItemText($0) }
|
||||
dict[-1282352120] = { return Api.PageRelatedArticle.parse_pageRelatedArticle($0) }
|
||||
dict[878078826] = { return Api.PageTableCell.parse_pageTableCell($0) }
|
||||
dict[-524237339] = { return Api.PageTableRow.parse_pageTableRow($0) }
|
||||
|
|
@ -926,17 +941,31 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-1258914157] = { return Api.RequirementToContact.parse_requirementToContactPaidMessages($0) }
|
||||
dict[-444472087] = { return Api.RequirementToContact.parse_requirementToContactPremium($0) }
|
||||
dict[-797791052] = { return Api.RestrictionReason.parse_restrictionReason($0) }
|
||||
dict[-1158439541] = { return Api.RichMessage.parse_richMessage($0) }
|
||||
dict[894777186] = { return Api.RichText.parse_textAnchor($0) }
|
||||
dict[-984177571] = { return Api.RichText.parse_textAutoEmail($0) }
|
||||
dict[616720265] = { return Api.RichText.parse_textAutoPhone($0) }
|
||||
dict[-1402305622] = { return Api.RichText.parse_textAutoUrl($0) }
|
||||
dict[-1185513171] = { return Api.RichText.parse_textBankCard($0) }
|
||||
dict[1730456516] = { return Api.RichText.parse_textBold($0) }
|
||||
dict[50276819] = { return Api.RichText.parse_textBotCommand($0) }
|
||||
dict[2073958401] = { return Api.RichText.parse_textCashtag($0) }
|
||||
dict[2120376535] = { return Api.RichText.parse_textConcat($0) }
|
||||
dict[-1570679104] = { return Api.RichText.parse_textCustomEmoji($0) }
|
||||
dict[-1514906069] = { return Api.RichText.parse_textDate($0) }
|
||||
dict[-564523562] = { return Api.RichText.parse_textEmail($0) }
|
||||
dict[-599948721] = { return Api.RichText.parse_textEmpty($0) }
|
||||
dict[1816074681] = { return Api.RichText.parse_textFixed($0) }
|
||||
dict[1368728810] = { return Api.RichText.parse_textHashtag($0) }
|
||||
dict[136105807] = { return Api.RichText.parse_textImage($0) }
|
||||
dict[-653089380] = { return Api.RichText.parse_textItalic($0) }
|
||||
dict[55281185] = { return Api.RichText.parse_textMarked($0) }
|
||||
dict[-1657885545] = { return Api.RichText.parse_textMath($0) }
|
||||
dict[-853225660] = { return Api.RichText.parse_textMention($0) }
|
||||
dict[27917308] = { return Api.RichText.parse_textMentionName($0) }
|
||||
dict[483104362] = { return Api.RichText.parse_textPhone($0) }
|
||||
dict[1950782688] = { return Api.RichText.parse_textPlain($0) }
|
||||
dict[1277844834] = { return Api.RichText.parse_textSpoiler($0) }
|
||||
dict[-1678197867] = { return Api.RichText.parse_textStrike($0) }
|
||||
dict[-311786236] = { return Api.RichText.parse_textSubscript($0) }
|
||||
dict[-939827711] = { return Api.RichText.parse_textSuperscript($0) }
|
||||
|
|
@ -987,6 +1016,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-368907213] = { return Api.SecureValueType.parse_secureValueTypeTemporaryRegistration($0) }
|
||||
dict[-63531698] = { return Api.SecureValueType.parse_secureValueTypeUtilityBill($0) }
|
||||
dict[-1206095820] = { return Api.SendAsPeer.parse_sendAsPeer($0) }
|
||||
dict[-491635887] = { return Api.SendMessageAction.parse_inputSendMessageRichMessageDraftAction($0) }
|
||||
dict[-44119819] = { return Api.SendMessageAction.parse_sendMessageCancelAction($0) }
|
||||
dict[1653390447] = { return Api.SendMessageAction.parse_sendMessageChooseContactAction($0) }
|
||||
dict[-1336228175] = { return Api.SendMessageAction.parse_sendMessageChooseStickerAction($0) }
|
||||
|
|
@ -998,6 +1028,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-718310409] = { return Api.SendMessageAction.parse_sendMessageRecordAudioAction($0) }
|
||||
dict[-1997373508] = { return Api.SendMessageAction.parse_sendMessageRecordRoundAction($0) }
|
||||
dict[-1584933265] = { return Api.SendMessageAction.parse_sendMessageRecordVideoAction($0) }
|
||||
dict[-1563745031] = { return Api.SendMessageAction.parse_sendMessageRichMessageDraftAction($0) }
|
||||
dict[929929052] = { return Api.SendMessageAction.parse_sendMessageTextDraftAction($0) }
|
||||
dict[381645902] = { return Api.SendMessageAction.parse_sendMessageTypingAction($0) }
|
||||
dict[-212740181] = { return Api.SendMessageAction.parse_sendMessageUploadAudioAction($0) }
|
||||
|
|
@ -1464,7 +1495,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[-438840932] = { return Api.messages.ChatFull.parse_chatFull($0) }
|
||||
dict[-2118733814] = { return Api.messages.ChatInviteImporters.parse_chatInviteImporters($0) }
|
||||
dict[1146512295] = { return Api.messages.ChatInviteJoinResult.parse_chatInviteJoinResultOk($0) }
|
||||
dict[2001452532] = { return Api.messages.ChatInviteJoinResult.parse_chatInviteJoinResultWebView($0) }
|
||||
dict[793887543] = { return Api.messages.ChatInviteJoinResult.parse_chatInviteJoinResultWebView($0) }
|
||||
dict[1694474197] = { return Api.messages.Chats.parse_chats($0) }
|
||||
dict[-1663561404] = { return Api.messages.Chats.parse_chatsSlice($0) }
|
||||
dict[-1571952873] = { return Api.messages.CheckedHistoryImportPeer.parse_checkedHistoryImportPeer($0) }
|
||||
|
|
@ -2011,6 +2042,8 @@ public extension Api {
|
|||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.InputNotifyPeer:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.InputPageListOrderedItem:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.InputPasskeyCredential:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.InputPasskeyResponse:
|
||||
|
|
@ -2033,6 +2066,8 @@ public extension Api {
|
|||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.InputReplyTo:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.InputRichMessage:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.InputSavedStarGift:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.InputSecureFile:
|
||||
|
|
@ -2255,6 +2290,8 @@ public extension Api {
|
|||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.RestrictionReason:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.RichMessage:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.RichText:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.SavedContact:
|
||||
|
|
|
|||
|
|
@ -1451,85 +1451,131 @@ public extension Api {
|
|||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum InputPasskeyCredential: TypeConstructorDescription {
|
||||
public class Cons_inputPasskeyCredentialFirebasePNV: TypeConstructorDescription {
|
||||
public var pnvToken: String
|
||||
public init(pnvToken: String) {
|
||||
self.pnvToken = pnvToken
|
||||
indirect enum InputPageListOrderedItem: TypeConstructorDescription {
|
||||
public class Cons_inputPageListOrderedItemBlocks: TypeConstructorDescription {
|
||||
public var flags: Int32
|
||||
public var blocks: [Api.PageBlock]
|
||||
public var value: Int32?
|
||||
public var type: String?
|
||||
public init(flags: Int32, blocks: [Api.PageBlock], value: Int32?, type: String?) {
|
||||
self.flags = flags
|
||||
self.blocks = blocks
|
||||
self.value = value
|
||||
self.type = type
|
||||
}
|
||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
return ("inputPasskeyCredentialFirebasePNV", [("pnvToken", ConstructorParameterDescription(self.pnvToken))])
|
||||
return ("inputPageListOrderedItemBlocks", [("flags", ConstructorParameterDescription(self.flags)), ("blocks", ConstructorParameterDescription(self.blocks)), ("value", ConstructorParameterDescription(self.value)), ("type", ConstructorParameterDescription(self.type))])
|
||||
}
|
||||
}
|
||||
public class Cons_inputPasskeyCredentialPublicKey: TypeConstructorDescription {
|
||||
public var id: String
|
||||
public var rawId: String
|
||||
public var response: Api.InputPasskeyResponse
|
||||
public init(id: String, rawId: String, response: Api.InputPasskeyResponse) {
|
||||
self.id = id
|
||||
self.rawId = rawId
|
||||
self.response = response
|
||||
public class Cons_inputPageListOrderedItemText: TypeConstructorDescription {
|
||||
public var flags: Int32
|
||||
public var text: Api.RichText
|
||||
public var value: Int32?
|
||||
public var type: String?
|
||||
public init(flags: Int32, text: Api.RichText, value: Int32?, type: String?) {
|
||||
self.flags = flags
|
||||
self.text = text
|
||||
self.value = value
|
||||
self.type = type
|
||||
}
|
||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
return ("inputPasskeyCredentialPublicKey", [("id", ConstructorParameterDescription(self.id)), ("rawId", ConstructorParameterDescription(self.rawId)), ("response", ConstructorParameterDescription(self.response))])
|
||||
return ("inputPageListOrderedItemText", [("flags", ConstructorParameterDescription(self.flags)), ("text", ConstructorParameterDescription(self.text)), ("value", ConstructorParameterDescription(self.value)), ("type", ConstructorParameterDescription(self.type))])
|
||||
}
|
||||
}
|
||||
case inputPasskeyCredentialFirebasePNV(Cons_inputPasskeyCredentialFirebasePNV)
|
||||
case inputPasskeyCredentialPublicKey(Cons_inputPasskeyCredentialPublicKey)
|
||||
case inputPageListOrderedItemBlocks(Cons_inputPageListOrderedItemBlocks)
|
||||
case inputPageListOrderedItemText(Cons_inputPageListOrderedItemText)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .inputPasskeyCredentialFirebasePNV(let _data):
|
||||
case .inputPageListOrderedItemBlocks(let _data):
|
||||
if boxed {
|
||||
buffer.appendInt32(1528613672)
|
||||
buffer.appendInt32(2105227266)
|
||||
}
|
||||
serializeInt32(_data.flags, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(_data.blocks.count))
|
||||
for item in _data.blocks {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
if Int(_data.flags) & Int(1 << 2) != 0 {
|
||||
serializeInt32(_data.value!, buffer: buffer, boxed: false)
|
||||
}
|
||||
if Int(_data.flags) & Int(1 << 3) != 0 {
|
||||
serializeString(_data.type!, buffer: buffer, boxed: false)
|
||||
}
|
||||
serializeString(_data.pnvToken, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .inputPasskeyCredentialPublicKey(let _data):
|
||||
case .inputPageListOrderedItemText(let _data):
|
||||
if boxed {
|
||||
buffer.appendInt32(1009235855)
|
||||
buffer.appendInt32(-1682665696)
|
||||
}
|
||||
serializeInt32(_data.flags, buffer: buffer, boxed: false)
|
||||
_data.text.serialize(buffer, true)
|
||||
if Int(_data.flags) & Int(1 << 2) != 0 {
|
||||
serializeInt32(_data.value!, buffer: buffer, boxed: false)
|
||||
}
|
||||
if Int(_data.flags) & Int(1 << 3) != 0 {
|
||||
serializeString(_data.type!, buffer: buffer, boxed: false)
|
||||
}
|
||||
serializeString(_data.id, buffer: buffer, boxed: false)
|
||||
serializeString(_data.rawId, buffer: buffer, boxed: false)
|
||||
_data.response.serialize(buffer, true)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
switch self {
|
||||
case .inputPasskeyCredentialFirebasePNV(let _data):
|
||||
return ("inputPasskeyCredentialFirebasePNV", [("pnvToken", ConstructorParameterDescription(_data.pnvToken))])
|
||||
case .inputPasskeyCredentialPublicKey(let _data):
|
||||
return ("inputPasskeyCredentialPublicKey", [("id", ConstructorParameterDescription(_data.id)), ("rawId", ConstructorParameterDescription(_data.rawId)), ("response", ConstructorParameterDescription(_data.response))])
|
||||
case .inputPageListOrderedItemBlocks(let _data):
|
||||
return ("inputPageListOrderedItemBlocks", [("flags", ConstructorParameterDescription(_data.flags)), ("blocks", ConstructorParameterDescription(_data.blocks)), ("value", ConstructorParameterDescription(_data.value)), ("type", ConstructorParameterDescription(_data.type))])
|
||||
case .inputPageListOrderedItemText(let _data):
|
||||
return ("inputPageListOrderedItemText", [("flags", ConstructorParameterDescription(_data.flags)), ("text", ConstructorParameterDescription(_data.text)), ("value", ConstructorParameterDescription(_data.value)), ("type", ConstructorParameterDescription(_data.type))])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_inputPasskeyCredentialFirebasePNV(_ reader: BufferReader) -> InputPasskeyCredential? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
public static func parse_inputPageListOrderedItemBlocks(_ reader: BufferReader) -> InputPageListOrderedItem? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: [Api.PageBlock]?
|
||||
if let _ = reader.readInt32() {
|
||||
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PageBlock.self)
|
||||
}
|
||||
var _3: Int32?
|
||||
if Int(_1 ?? 0) & Int(1 << 2) != 0 {
|
||||
_3 = reader.readInt32()
|
||||
}
|
||||
var _4: String?
|
||||
if Int(_1 ?? 0) & Int(1 << 3) != 0 {
|
||||
_4 = parseString(reader)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.InputPasskeyCredential.inputPasskeyCredentialFirebasePNV(Cons_inputPasskeyCredentialFirebasePNV(pnvToken: _1!))
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil
|
||||
let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 {
|
||||
return Api.InputPageListOrderedItem.inputPageListOrderedItemBlocks(Cons_inputPageListOrderedItemBlocks(flags: _1!, blocks: _2!, value: _3, type: _4))
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputPasskeyCredentialPublicKey(_ reader: BufferReader) -> InputPasskeyCredential? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
var _2: String?
|
||||
_2 = parseString(reader)
|
||||
var _3: Api.InputPasskeyResponse?
|
||||
public static func parse_inputPageListOrderedItemText(_ reader: BufferReader) -> InputPageListOrderedItem? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Api.RichText?
|
||||
if let signature = reader.readInt32() {
|
||||
_3 = Api.parse(reader, signature: signature) as? Api.InputPasskeyResponse
|
||||
_2 = Api.parse(reader, signature: signature) as? Api.RichText
|
||||
}
|
||||
var _3: Int32?
|
||||
if Int(_1 ?? 0) & Int(1 << 2) != 0 {
|
||||
_3 = reader.readInt32()
|
||||
}
|
||||
var _4: String?
|
||||
if Int(_1 ?? 0) & Int(1 << 3) != 0 {
|
||||
_4 = parseString(reader)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.InputPasskeyCredential.inputPasskeyCredentialPublicKey(Cons_inputPasskeyCredentialPublicKey(id: _1!, rawId: _2!, response: _3!))
|
||||
let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil
|
||||
let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 {
|
||||
return Api.InputPageListOrderedItem.inputPageListOrderedItemText(Cons_inputPageListOrderedItemText(flags: _1!, text: _2!, value: _3, type: _4))
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,3 +1,90 @@
|
|||
public extension Api {
|
||||
enum InputPasskeyCredential: TypeConstructorDescription {
|
||||
public class Cons_inputPasskeyCredentialFirebasePNV: TypeConstructorDescription {
|
||||
public var pnvToken: String
|
||||
public init(pnvToken: String) {
|
||||
self.pnvToken = pnvToken
|
||||
}
|
||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
return ("inputPasskeyCredentialFirebasePNV", [("pnvToken", ConstructorParameterDescription(self.pnvToken))])
|
||||
}
|
||||
}
|
||||
public class Cons_inputPasskeyCredentialPublicKey: TypeConstructorDescription {
|
||||
public var id: String
|
||||
public var rawId: String
|
||||
public var response: Api.InputPasskeyResponse
|
||||
public init(id: String, rawId: String, response: Api.InputPasskeyResponse) {
|
||||
self.id = id
|
||||
self.rawId = rawId
|
||||
self.response = response
|
||||
}
|
||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
return ("inputPasskeyCredentialPublicKey", [("id", ConstructorParameterDescription(self.id)), ("rawId", ConstructorParameterDescription(self.rawId)), ("response", ConstructorParameterDescription(self.response))])
|
||||
}
|
||||
}
|
||||
case inputPasskeyCredentialFirebasePNV(Cons_inputPasskeyCredentialFirebasePNV)
|
||||
case inputPasskeyCredentialPublicKey(Cons_inputPasskeyCredentialPublicKey)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .inputPasskeyCredentialFirebasePNV(let _data):
|
||||
if boxed {
|
||||
buffer.appendInt32(1528613672)
|
||||
}
|
||||
serializeString(_data.pnvToken, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .inputPasskeyCredentialPublicKey(let _data):
|
||||
if boxed {
|
||||
buffer.appendInt32(1009235855)
|
||||
}
|
||||
serializeString(_data.id, buffer: buffer, boxed: false)
|
||||
serializeString(_data.rawId, buffer: buffer, boxed: false)
|
||||
_data.response.serialize(buffer, true)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
switch self {
|
||||
case .inputPasskeyCredentialFirebasePNV(let _data):
|
||||
return ("inputPasskeyCredentialFirebasePNV", [("pnvToken", ConstructorParameterDescription(_data.pnvToken))])
|
||||
case .inputPasskeyCredentialPublicKey(let _data):
|
||||
return ("inputPasskeyCredentialPublicKey", [("id", ConstructorParameterDescription(_data.id)), ("rawId", ConstructorParameterDescription(_data.rawId)), ("response", ConstructorParameterDescription(_data.response))])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_inputPasskeyCredentialFirebasePNV(_ reader: BufferReader) -> InputPasskeyCredential? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.InputPasskeyCredential.inputPasskeyCredentialFirebasePNV(Cons_inputPasskeyCredentialFirebasePNV(pnvToken: _1!))
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputPasskeyCredentialPublicKey(_ reader: BufferReader) -> InputPasskeyCredential? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
var _2: String?
|
||||
_2 = parseString(reader)
|
||||
var _3: Api.InputPasskeyResponse?
|
||||
if let signature = reader.readInt32() {
|
||||
_3 = Api.parse(reader, signature: signature) as? Api.InputPasskeyResponse
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.InputPasskeyCredential.inputPasskeyCredentialPublicKey(Cons_inputPasskeyCredentialPublicKey(id: _1!, rawId: _2!, response: _3!))
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum InputPasskeyResponse: TypeConstructorDescription {
|
||||
public class Cons_inputPasskeyResponseLogin: TypeConstructorDescription {
|
||||
|
|
@ -882,246 +969,3 @@ public extension Api {
|
|||
}
|
||||
}
|
||||
}
|
||||
public extension Api {
|
||||
enum InputPrivacyRule: TypeConstructorDescription {
|
||||
public class Cons_inputPrivacyValueAllowChatParticipants: TypeConstructorDescription {
|
||||
public var chats: [Int64]
|
||||
public init(chats: [Int64]) {
|
||||
self.chats = chats
|
||||
}
|
||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
return ("inputPrivacyValueAllowChatParticipants", [("chats", ConstructorParameterDescription(self.chats))])
|
||||
}
|
||||
}
|
||||
public class Cons_inputPrivacyValueAllowUsers: TypeConstructorDescription {
|
||||
public var users: [Api.InputUser]
|
||||
public init(users: [Api.InputUser]) {
|
||||
self.users = users
|
||||
}
|
||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
return ("inputPrivacyValueAllowUsers", [("users", ConstructorParameterDescription(self.users))])
|
||||
}
|
||||
}
|
||||
public class Cons_inputPrivacyValueDisallowChatParticipants: TypeConstructorDescription {
|
||||
public var chats: [Int64]
|
||||
public init(chats: [Int64]) {
|
||||
self.chats = chats
|
||||
}
|
||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
return ("inputPrivacyValueDisallowChatParticipants", [("chats", ConstructorParameterDescription(self.chats))])
|
||||
}
|
||||
}
|
||||
public class Cons_inputPrivacyValueDisallowUsers: TypeConstructorDescription {
|
||||
public var users: [Api.InputUser]
|
||||
public init(users: [Api.InputUser]) {
|
||||
self.users = users
|
||||
}
|
||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
return ("inputPrivacyValueDisallowUsers", [("users", ConstructorParameterDescription(self.users))])
|
||||
}
|
||||
}
|
||||
case inputPrivacyValueAllowAll
|
||||
case inputPrivacyValueAllowBots
|
||||
case inputPrivacyValueAllowChatParticipants(Cons_inputPrivacyValueAllowChatParticipants)
|
||||
case inputPrivacyValueAllowCloseFriends
|
||||
case inputPrivacyValueAllowContacts
|
||||
case inputPrivacyValueAllowPremium
|
||||
case inputPrivacyValueAllowUsers(Cons_inputPrivacyValueAllowUsers)
|
||||
case inputPrivacyValueDisallowAll
|
||||
case inputPrivacyValueDisallowBots
|
||||
case inputPrivacyValueDisallowChatParticipants(Cons_inputPrivacyValueDisallowChatParticipants)
|
||||
case inputPrivacyValueDisallowContacts
|
||||
case inputPrivacyValueDisallowUsers(Cons_inputPrivacyValueDisallowUsers)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .inputPrivacyValueAllowAll:
|
||||
if boxed {
|
||||
buffer.appendInt32(407582158)
|
||||
}
|
||||
break
|
||||
case .inputPrivacyValueAllowBots:
|
||||
if boxed {
|
||||
buffer.appendInt32(1515179237)
|
||||
}
|
||||
break
|
||||
case .inputPrivacyValueAllowChatParticipants(let _data):
|
||||
if boxed {
|
||||
buffer.appendInt32(-2079962673)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(_data.chats.count))
|
||||
for item in _data.chats {
|
||||
serializeInt64(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
break
|
||||
case .inputPrivacyValueAllowCloseFriends:
|
||||
if boxed {
|
||||
buffer.appendInt32(793067081)
|
||||
}
|
||||
break
|
||||
case .inputPrivacyValueAllowContacts:
|
||||
if boxed {
|
||||
buffer.appendInt32(218751099)
|
||||
}
|
||||
break
|
||||
case .inputPrivacyValueAllowPremium:
|
||||
if boxed {
|
||||
buffer.appendInt32(2009975281)
|
||||
}
|
||||
break
|
||||
case .inputPrivacyValueAllowUsers(let _data):
|
||||
if boxed {
|
||||
buffer.appendInt32(320652927)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(_data.users.count))
|
||||
for item in _data.users {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
case .inputPrivacyValueDisallowAll:
|
||||
if boxed {
|
||||
buffer.appendInt32(-697604407)
|
||||
}
|
||||
break
|
||||
case .inputPrivacyValueDisallowBots:
|
||||
if boxed {
|
||||
buffer.appendInt32(-991594219)
|
||||
}
|
||||
break
|
||||
case .inputPrivacyValueDisallowChatParticipants(let _data):
|
||||
if boxed {
|
||||
buffer.appendInt32(-380694650)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(_data.chats.count))
|
||||
for item in _data.chats {
|
||||
serializeInt64(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
break
|
||||
case .inputPrivacyValueDisallowContacts:
|
||||
if boxed {
|
||||
buffer.appendInt32(195371015)
|
||||
}
|
||||
break
|
||||
case .inputPrivacyValueDisallowUsers(let _data):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1877932953)
|
||||
}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(_data.users.count))
|
||||
for item in _data.users {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
switch self {
|
||||
case .inputPrivacyValueAllowAll:
|
||||
return ("inputPrivacyValueAllowAll", [])
|
||||
case .inputPrivacyValueAllowBots:
|
||||
return ("inputPrivacyValueAllowBots", [])
|
||||
case .inputPrivacyValueAllowChatParticipants(let _data):
|
||||
return ("inputPrivacyValueAllowChatParticipants", [("chats", ConstructorParameterDescription(_data.chats))])
|
||||
case .inputPrivacyValueAllowCloseFriends:
|
||||
return ("inputPrivacyValueAllowCloseFriends", [])
|
||||
case .inputPrivacyValueAllowContacts:
|
||||
return ("inputPrivacyValueAllowContacts", [])
|
||||
case .inputPrivacyValueAllowPremium:
|
||||
return ("inputPrivacyValueAllowPremium", [])
|
||||
case .inputPrivacyValueAllowUsers(let _data):
|
||||
return ("inputPrivacyValueAllowUsers", [("users", ConstructorParameterDescription(_data.users))])
|
||||
case .inputPrivacyValueDisallowAll:
|
||||
return ("inputPrivacyValueDisallowAll", [])
|
||||
case .inputPrivacyValueDisallowBots:
|
||||
return ("inputPrivacyValueDisallowBots", [])
|
||||
case .inputPrivacyValueDisallowChatParticipants(let _data):
|
||||
return ("inputPrivacyValueDisallowChatParticipants", [("chats", ConstructorParameterDescription(_data.chats))])
|
||||
case .inputPrivacyValueDisallowContacts:
|
||||
return ("inputPrivacyValueDisallowContacts", [])
|
||||
case .inputPrivacyValueDisallowUsers(let _data):
|
||||
return ("inputPrivacyValueDisallowUsers", [("users", ConstructorParameterDescription(_data.users))])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_inputPrivacyValueAllowAll(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueAllowAll
|
||||
}
|
||||
public static func parse_inputPrivacyValueAllowBots(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueAllowBots
|
||||
}
|
||||
public static func parse_inputPrivacyValueAllowChatParticipants(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
var _1: [Int64]?
|
||||
if let _ = reader.readInt32() {
|
||||
_1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueAllowChatParticipants(Cons_inputPrivacyValueAllowChatParticipants(chats: _1!))
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputPrivacyValueAllowCloseFriends(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueAllowCloseFriends
|
||||
}
|
||||
public static func parse_inputPrivacyValueAllowContacts(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueAllowContacts
|
||||
}
|
||||
public static func parse_inputPrivacyValueAllowPremium(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueAllowPremium
|
||||
}
|
||||
public static func parse_inputPrivacyValueAllowUsers(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
var _1: [Api.InputUser]?
|
||||
if let _ = reader.readInt32() {
|
||||
_1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueAllowUsers(Cons_inputPrivacyValueAllowUsers(users: _1!))
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputPrivacyValueDisallowAll(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueDisallowAll
|
||||
}
|
||||
public static func parse_inputPrivacyValueDisallowBots(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueDisallowBots
|
||||
}
|
||||
public static func parse_inputPrivacyValueDisallowChatParticipants(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
var _1: [Int64]?
|
||||
if let _ = reader.readInt32() {
|
||||
_1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueDisallowChatParticipants(Cons_inputPrivacyValueDisallowChatParticipants(chats: _1!))
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputPrivacyValueDisallowContacts(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueDisallowContacts
|
||||
}
|
||||
public static func parse_inputPrivacyValueDisallowUsers(_ reader: BufferReader) -> InputPrivacyRule? {
|
||||
var _1: [Api.InputUser]?
|
||||
if let _ = reader.readInt32() {
|
||||
_1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.InputPrivacyRule.inputPrivacyValueDisallowUsers(Cons_inputPrivacyValueDisallowUsers(users: _1!))
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
14097
submodules/TelegramApi/Sources/Api42.swift
Normal file
14097
submodules/TelegramApi/Sources/Api42.swift
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -553,7 +553,8 @@ public extension Api {
|
|||
public var date: Int32
|
||||
public var effect: Int64?
|
||||
public var suggestedPost: Api.SuggestedPost?
|
||||
public init(flags: Int32, replyTo: Api.InputReplyTo?, message: String, entities: [Api.MessageEntity]?, media: Api.InputMedia?, date: Int32, effect: Int64?, suggestedPost: Api.SuggestedPost?) {
|
||||
public var richMessage: Api.InputRichMessage?
|
||||
public init(flags: Int32, replyTo: Api.InputReplyTo?, message: String, entities: [Api.MessageEntity]?, media: Api.InputMedia?, date: Int32, effect: Int64?, suggestedPost: Api.SuggestedPost?, richMessage: Api.InputRichMessage?) {
|
||||
self.flags = flags
|
||||
self.replyTo = replyTo
|
||||
self.message = message
|
||||
|
|
@ -562,9 +563,10 @@ public extension Api {
|
|||
self.date = date
|
||||
self.effect = effect
|
||||
self.suggestedPost = suggestedPost
|
||||
self.richMessage = richMessage
|
||||
}
|
||||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
return ("draftMessage", [("flags", ConstructorParameterDescription(self.flags)), ("replyTo", ConstructorParameterDescription(self.replyTo)), ("message", ConstructorParameterDescription(self.message)), ("entities", ConstructorParameterDescription(self.entities)), ("media", ConstructorParameterDescription(self.media)), ("date", ConstructorParameterDescription(self.date)), ("effect", ConstructorParameterDescription(self.effect)), ("suggestedPost", ConstructorParameterDescription(self.suggestedPost))])
|
||||
return ("draftMessage", [("flags", ConstructorParameterDescription(self.flags)), ("replyTo", ConstructorParameterDescription(self.replyTo)), ("message", ConstructorParameterDescription(self.message)), ("entities", ConstructorParameterDescription(self.entities)), ("media", ConstructorParameterDescription(self.media)), ("date", ConstructorParameterDescription(self.date)), ("effect", ConstructorParameterDescription(self.effect)), ("suggestedPost", ConstructorParameterDescription(self.suggestedPost)), ("richMessage", ConstructorParameterDescription(self.richMessage))])
|
||||
}
|
||||
}
|
||||
public class Cons_draftMessageEmpty: TypeConstructorDescription {
|
||||
|
|
@ -585,7 +587,7 @@ public extension Api {
|
|||
switch self {
|
||||
case .draftMessage(let _data):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1763006997)
|
||||
buffer.appendInt32(-1743452271)
|
||||
}
|
||||
serializeInt32(_data.flags, buffer: buffer, boxed: false)
|
||||
if Int(_data.flags) & Int(1 << 4) != 0 {
|
||||
|
|
@ -609,6 +611,9 @@ public extension Api {
|
|||
if Int(_data.flags) & Int(1 << 8) != 0 {
|
||||
_data.suggestedPost!.serialize(buffer, true)
|
||||
}
|
||||
if Int(_data.flags) & Int(1 << 9) != 0 {
|
||||
_data.richMessage!.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
case .draftMessageEmpty(let _data):
|
||||
if boxed {
|
||||
|
|
@ -625,7 +630,7 @@ public extension Api {
|
|||
public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {
|
||||
switch self {
|
||||
case .draftMessage(let _data):
|
||||
return ("draftMessage", [("flags", ConstructorParameterDescription(_data.flags)), ("replyTo", ConstructorParameterDescription(_data.replyTo)), ("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("media", ConstructorParameterDescription(_data.media)), ("date", ConstructorParameterDescription(_data.date)), ("effect", ConstructorParameterDescription(_data.effect)), ("suggestedPost", ConstructorParameterDescription(_data.suggestedPost))])
|
||||
return ("draftMessage", [("flags", ConstructorParameterDescription(_data.flags)), ("replyTo", ConstructorParameterDescription(_data.replyTo)), ("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("media", ConstructorParameterDescription(_data.media)), ("date", ConstructorParameterDescription(_data.date)), ("effect", ConstructorParameterDescription(_data.effect)), ("suggestedPost", ConstructorParameterDescription(_data.suggestedPost)), ("richMessage", ConstructorParameterDescription(_data.richMessage))])
|
||||
case .draftMessageEmpty(let _data):
|
||||
return ("draftMessageEmpty", [("flags", ConstructorParameterDescription(_data.flags)), ("date", ConstructorParameterDescription(_data.date))])
|
||||
}
|
||||
|
|
@ -666,6 +671,12 @@ public extension Api {
|
|||
_8 = Api.parse(reader, signature: signature) as? Api.SuggestedPost
|
||||
}
|
||||
}
|
||||
var _9: Api.InputRichMessage?
|
||||
if Int(_1 ?? 0) & Int(1 << 9) != 0 {
|
||||
if let signature = reader.readInt32() {
|
||||
_9 = Api.parse(reader, signature: signature) as? Api.InputRichMessage
|
||||
}
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
|
|
@ -674,8 +685,9 @@ public extension Api {
|
|||
let _c6 = _6 != nil
|
||||
let _c7 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _7 != nil
|
||||
let _c8 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _8 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
|
||||
return Api.DraftMessage.draftMessage(Cons_draftMessage(flags: _1!, replyTo: _2, message: _3!, entities: _4, media: _5, date: _6!, effect: _7, suggestedPost: _8))
|
||||
let _c9 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _9 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 {
|
||||
return Api.DraftMessage.draftMessage(Cons_draftMessage(flags: _1!, replyTo: _2, message: _3!, entities: _4, media: _5, date: _6!, effect: _7, suggestedPost: _8, richMessage: _9))
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
|||
shortName: " ",
|
||||
avatarImage: nil,
|
||||
audioOutput: .internalSpeaker,
|
||||
canSwitchAudioOutput: true,
|
||||
isLocalAudioMuted: false,
|
||||
isRemoteAudioMuted: false,
|
||||
localVideo: nil,
|
||||
|
|
@ -271,6 +272,11 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
|||
self.currentAudioOutput = currentOutput
|
||||
|
||||
if var callScreenState = self.callScreenState {
|
||||
var canSwitchAudioOutput = false
|
||||
if availableOutputs.count > 1 {
|
||||
canSwitchAudioOutput = true
|
||||
}
|
||||
|
||||
let mappedOutput: PrivateCallScreen.State.AudioOutput
|
||||
if let currentOutput {
|
||||
switch currentOutput {
|
||||
|
|
@ -301,8 +307,9 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
|||
mappedOutput = .internalSpeaker
|
||||
}
|
||||
|
||||
if callScreenState.audioOutput != mappedOutput {
|
||||
if callScreenState.audioOutput != mappedOutput || callScreenState.canSwitchAudioOutput != canSwitchAudioOutput {
|
||||
callScreenState.audioOutput = mappedOutput
|
||||
callScreenState.canSwitchAudioOutput = canSwitchAudioOutput
|
||||
self.callScreenState = callScreenState
|
||||
self.update(transition: .animated(duration: 0.3, curve: .spring))
|
||||
|
||||
|
|
|
|||
|
|
@ -156,11 +156,11 @@ final class VideoChatActionButtonComponent: Component {
|
|||
switch component.content {
|
||||
case let .audio(audio, isEnabledValue):
|
||||
var isActive = false
|
||||
isEnabled = isEnabledValue
|
||||
switch audio {
|
||||
case .none, .builtin:
|
||||
titleText = component.strings.Call_Speaker
|
||||
case .speaker:
|
||||
isEnabled = isEnabledValue
|
||||
isActive = isEnabledValue
|
||||
titleText = component.strings.Call_Speaker
|
||||
case .headphones:
|
||||
|
|
@ -324,7 +324,7 @@ final class VideoChatActionButtonComponent: Component {
|
|||
}
|
||||
transition.setPosition(view: iconView, position: iconFrame.center)
|
||||
transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
|
||||
transition.setAlpha(view: iconView, alpha: isEnabled ? 1.0 : 0.6)
|
||||
transition.setAlpha(view: iconView, alpha: isEnabled ? 1.0 : 0.4)
|
||||
transition.setScale(view: iconView, scale: availableSize.height / 56.0)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2619,6 +2619,7 @@ final class VideoChatScreenComponent: Component {
|
|||
|
||||
let videoButtonContent: VideoChatActionButtonComponent.Content?
|
||||
let videoControlButtonContent: VideoChatActionButtonComponent.Content
|
||||
let videoControlButtonEnabled: Bool
|
||||
let messageButtonContent: VideoChatActionButtonComponent.Content?
|
||||
|
||||
var buttonAudio: VideoChatActionButtonComponent.Content.Audio = .speaker
|
||||
|
|
@ -2652,13 +2653,16 @@ final class VideoChatScreenComponent: Component {
|
|||
if let callState = self.callState, let muteState = callState.muteState, !muteState.canUnmute {
|
||||
videoButtonContent = nil
|
||||
videoControlButtonContent = .audio(audio: buttonAudio, isEnabled: buttonIsEnabled)
|
||||
videoControlButtonEnabled = buttonIsEnabled
|
||||
} else {
|
||||
let isVideoActive = self.callState?.isMyVideoActive ?? false
|
||||
videoButtonContent = .video(isActive: isVideoActive)
|
||||
if isVideoActive {
|
||||
videoControlButtonContent = .rotateCamera
|
||||
videoControlButtonEnabled = true
|
||||
} else {
|
||||
videoControlButtonContent = .audio(audio: buttonAudio, isEnabled: buttonIsEnabled)
|
||||
videoControlButtonEnabled = buttonIsEnabled
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3225,7 +3229,7 @@ final class VideoChatScreenComponent: Component {
|
|||
transition.setPosition(view: microphoneButtonView, position: microphoneButtonFrame.center)
|
||||
transition.setBounds(view: microphoneButtonView, bounds: CGRect(origin: CGPoint(), size: microphoneButtonFrame.size))
|
||||
}
|
||||
|
||||
|
||||
let _ = self.speakerButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
|
|
@ -3248,6 +3252,7 @@ final class VideoChatScreenComponent: Component {
|
|||
self.onAudioRoutePressed()
|
||||
}
|
||||
},
|
||||
isEnabled: videoControlButtonEnabled,
|
||||
animateAlpha: false
|
||||
)),
|
||||
environment: {},
|
||||
|
|
|
|||
|
|
@ -214,11 +214,13 @@ table InstantPageListItem {
|
|||
table InstantPageListItem_Text {
|
||||
text:RichText (id: 0, required);
|
||||
number:string (id: 1);
|
||||
checkState:int32 (id: 2);
|
||||
}
|
||||
|
||||
table InstantPageListItem_Blocks {
|
||||
blocks:[InstantPageBlock] (id: 0, required);
|
||||
number:string (id: 1);
|
||||
checkState:int32 (id: 2);
|
||||
}
|
||||
|
||||
table InstantPageListItem_Unknown {}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,18 @@ union RichText_Value {
|
|||
RichText_Phone,
|
||||
RichText_Image,
|
||||
RichText_Anchor,
|
||||
RichText_Formula
|
||||
RichText_Formula,
|
||||
RichText_CustomEmoji,
|
||||
RichText_AutoEmail,
|
||||
RichText_AutoPhone,
|
||||
RichText_AutoUrl,
|
||||
RichText_BankCard,
|
||||
RichText_BotCommand,
|
||||
RichText_Cashtag,
|
||||
RichText_Hashtag,
|
||||
RichText_Mention,
|
||||
RichText_MentionName,
|
||||
RichText_Spoiler
|
||||
}
|
||||
|
||||
table RichText {
|
||||
|
|
@ -98,3 +109,49 @@ table RichText_Anchor {
|
|||
table RichText_Formula {
|
||||
latex:string (id: 0, required);
|
||||
}
|
||||
|
||||
table RichText_CustomEmoji {
|
||||
fileId:long (id: 0);
|
||||
alt:string (id: 1, required);
|
||||
}
|
||||
|
||||
table RichText_AutoEmail {
|
||||
text:RichText (id: 0, required);
|
||||
}
|
||||
|
||||
table RichText_AutoPhone {
|
||||
text:RichText (id: 0, required);
|
||||
}
|
||||
|
||||
table RichText_AutoUrl {
|
||||
text:RichText (id: 0, required);
|
||||
}
|
||||
|
||||
table RichText_BankCard {
|
||||
text:RichText (id: 0, required);
|
||||
}
|
||||
|
||||
table RichText_BotCommand {
|
||||
text:RichText (id: 0, required);
|
||||
}
|
||||
|
||||
table RichText_Cashtag {
|
||||
text:RichText (id: 0, required);
|
||||
}
|
||||
|
||||
table RichText_Hashtag {
|
||||
text:RichText (id: 0, required);
|
||||
}
|
||||
|
||||
table RichText_Mention {
|
||||
text:RichText (id: 0, required);
|
||||
}
|
||||
|
||||
table RichText_MentionName {
|
||||
text:RichText (id: 0, required);
|
||||
peerId:long (id: 1);
|
||||
}
|
||||
|
||||
table RichText_Spoiler {
|
||||
text:RichText (id: 0, required);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ struct AccountStateChannelState: Equatable {
|
|||
var pts: Int32
|
||||
}
|
||||
|
||||
enum PeerLiveTypingDraftUpdateContent {
|
||||
case plain(text: String, entities: [MessageTextEntity])
|
||||
case rich(RichTextMessageAttribute)
|
||||
}
|
||||
|
||||
final class AccountInitialState {
|
||||
let state: AuthorizedAccountState.State
|
||||
let peerIds: Set<PeerId>
|
||||
|
|
@ -59,6 +64,16 @@ enum AccountStateGlobalNotificationSettingsSubject {
|
|||
case channels
|
||||
}
|
||||
|
||||
struct UpdatedApiPresence {
|
||||
var status: Api.UserStatus
|
||||
var isMin: Bool
|
||||
|
||||
init(status: Api.UserStatus, isMin: Bool) {
|
||||
self.status = status
|
||||
self.isMin = isMin
|
||||
}
|
||||
}
|
||||
|
||||
enum AccountStateMutationOperation {
|
||||
case AddMessages([StoreMessage], AddMessagesLocation)
|
||||
case AddScheduledMessages([StoreMessage])
|
||||
|
|
@ -88,12 +103,12 @@ enum AccountStateMutationOperation {
|
|||
case UpdateCachedPeerData(PeerId, (CachedPeerData?) -> CachedPeerData?)
|
||||
case UpdateMessagesPinned([MessageId], Bool)
|
||||
case MergeApiUsers([Api.User])
|
||||
case MergePeerPresences([PeerId: Api.UserStatus], Bool)
|
||||
case MergePeerPresences([PeerId: UpdatedApiPresence], Bool)
|
||||
case UpdateSecretChat(chat: Api.EncryptedChat, timestamp: Int32)
|
||||
case AddSecretMessages([Api.EncryptedMessage])
|
||||
case ReadSecretOutbox(peerId: PeerId, maxTimestamp: Int32, actionTimestamp: Int32)
|
||||
case AddPeerInputActivity(chatPeerId: PeerActivitySpace, peerId: PeerId?, activity: PeerInputActivity?)
|
||||
case AddPeerLiveTypingDraftUpdate(peerAndThreadId: PeerAndThreadId, id: Int64, timestamp: Int32, peerId: PeerId, text: String, entities: [MessageTextEntity])
|
||||
case AddPeerLiveTypingDraftUpdate(peerAndThreadId: PeerAndThreadId, id: Int64, timestamp: Int32, peerId: PeerId, content: PeerLiveTypingDraftUpdateContent)
|
||||
case UpdatePinnedItemIds(PeerGroupId, AccountStateUpdatePinnedItemIdsOperation)
|
||||
case UpdatePinnedSavedItemIds(AccountStateUpdatePinnedItemIdsOperation)
|
||||
case UpdatePinnedTopic(peerId: PeerId, threadId: Int64, isPinned: Bool)
|
||||
|
|
@ -575,17 +590,18 @@ struct AccountMutableState {
|
|||
mutating func mergeUsers(_ users: [Api.User]) {
|
||||
self.addOperation(.MergeApiUsers(users))
|
||||
|
||||
var presences: [PeerId: Api.UserStatus] = [:]
|
||||
var presences: [PeerId: UpdatedApiPresence] = [:]
|
||||
for user in users {
|
||||
switch user {
|
||||
case let .user(userData):
|
||||
let (id, status) = (userData.id, userData.status)
|
||||
if let status = status {
|
||||
presences[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))] = status
|
||||
}
|
||||
break
|
||||
case .userEmpty:
|
||||
break
|
||||
case let .user(userData):
|
||||
let (id, status) = (userData.id, userData.status)
|
||||
let isMin = (userData.flags & (1 << 20)) != 0
|
||||
if let status = status {
|
||||
presences[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))] = UpdatedApiPresence(status: status, isMin: isMin)
|
||||
}
|
||||
break
|
||||
case .userEmpty:
|
||||
break
|
||||
}
|
||||
}
|
||||
if !presences.isEmpty {
|
||||
|
|
@ -594,7 +610,7 @@ struct AccountMutableState {
|
|||
}
|
||||
|
||||
mutating func mergePeerPresences(_ presences: [PeerId: Api.UserStatus], explicit: Bool) {
|
||||
self.addOperation(.MergePeerPresences(presences, explicit))
|
||||
self.addOperation(.MergePeerPresences(presences.mapValues({ UpdatedApiPresence(status: $0, isMin: false) }), explicit))
|
||||
}
|
||||
|
||||
mutating func updateSecretChat(chat: Api.EncryptedChat, timestamp: Int32) {
|
||||
|
|
@ -613,8 +629,8 @@ struct AccountMutableState {
|
|||
self.addOperation(.AddPeerInputActivity(chatPeerId: chatPeerId, peerId: peerId, activity: activity))
|
||||
}
|
||||
|
||||
mutating func addPeerLiveTypingDraftUpdate(peerAndThreadId: PeerAndThreadId, id: Int64, timestamp: Int32, peerId: PeerId, text: String, entities: [MessageTextEntity]) {
|
||||
self.addOperation(.AddPeerLiveTypingDraftUpdate(peerAndThreadId: peerAndThreadId, id: id, timestamp: timestamp, peerId: peerId, text: text, entities: entities))
|
||||
mutating func addPeerLiveTypingDraftUpdate(peerAndThreadId: PeerAndThreadId, id: Int64, timestamp: Int32, peerId: PeerId, content: PeerLiveTypingDraftUpdateContent) {
|
||||
self.addOperation(.AddPeerLiveTypingDraftUpdate(peerAndThreadId: peerAndThreadId, id: id, timestamp: timestamp, peerId: peerId, content: content))
|
||||
}
|
||||
|
||||
mutating func addUpdatePinnedItemIds(groupId: PeerGroupId, operation: AccountStateUpdatePinnedItemIdsOperation) {
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ private var declaredEncodables: Void = {
|
|||
declareEncodable(InlineBotMessageAttribute.self, f: { InlineBotMessageAttribute(decoder: $0) })
|
||||
declareEncodable(InlineBusinessBotMessageAttribute.self, f: { InlineBusinessBotMessageAttribute(decoder: $0) })
|
||||
declareEncodable(TextEntitiesMessageAttribute.self, f: { TextEntitiesMessageAttribute(decoder: $0) })
|
||||
declareEncodable(RichTextMessageAttribute.self, f: { RichTextMessageAttribute(decoder: $0) })
|
||||
declareEncodable(ReplyMessageAttribute.self, f: { ReplyMessageAttribute(decoder: $0) })
|
||||
declareEncodable(QuotedReplyMessageAttribute.self, f: { QuotedReplyMessageAttribute(decoder: $0) })
|
||||
declareEncodable(ReplyStoryAttribute.self, f: { ReplyStoryAttribute(decoder: $0) })
|
||||
|
|
|
|||
|
|
@ -16,14 +16,43 @@ extension InstantPageCaption {
|
|||
public extension InstantPageListItem {
|
||||
var num: String? {
|
||||
switch self {
|
||||
case let .text(_, num):
|
||||
case let .text(_, num, _):
|
||||
return num
|
||||
case let .blocks(_, num):
|
||||
case let .blocks(_, num, _):
|
||||
return num
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var checked: Bool? {
|
||||
switch self {
|
||||
case let .text(_, _, checked):
|
||||
return checked
|
||||
case let .blocks(_, _, checked):
|
||||
return checked
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func checkedFromApiFlags(_ flags: Int32) -> Bool? {
|
||||
guard (flags & (1 << 0)) != 0 else {
|
||||
return nil
|
||||
}
|
||||
return (flags & (1 << 1)) != 0
|
||||
}
|
||||
|
||||
static func apiFlags(fromChecked checked: Bool?) -> Int32 {
|
||||
guard let checked else {
|
||||
return 0
|
||||
}
|
||||
var flags: Int32 = 1 << 0
|
||||
if checked {
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
return flags
|
||||
}
|
||||
}
|
||||
|
||||
extension InstantPageListItem {
|
||||
|
|
@ -31,10 +60,10 @@ extension InstantPageListItem {
|
|||
switch apiListItem {
|
||||
case let .pageListItemText(pageListItemTextData):
|
||||
let text = pageListItemTextData.text
|
||||
self = .text(RichText(apiText: text), nil)
|
||||
self = .text(RichText(apiText: text), nil, InstantPageListItem.checkedFromApiFlags(pageListItemTextData.flags))
|
||||
case let .pageListItemBlocks(pageListItemBlocksData):
|
||||
let blocks = pageListItemBlocksData.blocks
|
||||
self = .blocks(blocks.map({ InstantPageBlock(apiBlock: $0) }), nil)
|
||||
self = .blocks(blocks.map({ InstantPageBlock(apiBlock: $0) }), nil, InstantPageListItem.checkedFromApiFlags(pageListItemBlocksData.flags))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,10 +71,48 @@ extension InstantPageListItem {
|
|||
switch apiListOrderedItem {
|
||||
case let .pageListOrderedItemText(pageListOrderedItemTextData):
|
||||
let (num, text) = (pageListOrderedItemTextData.num, pageListOrderedItemTextData.text)
|
||||
self = .text(RichText(apiText: text), num)
|
||||
self = .text(RichText(apiText: text), num, InstantPageListItem.checkedFromApiFlags(pageListOrderedItemTextData.flags))
|
||||
case let .pageListOrderedItemBlocks(pageListOrderedItemBlocksData):
|
||||
let (num, blocks) = (pageListOrderedItemBlocksData.num, pageListOrderedItemBlocksData.blocks)
|
||||
self = .blocks(blocks.map({ InstantPageBlock(apiBlock: $0) }), num)
|
||||
self = .blocks(blocks.map({ InstantPageBlock(apiBlock: $0) }), num, InstantPageListItem.checkedFromApiFlags(pageListOrderedItemBlocksData.flags))
|
||||
}
|
||||
}
|
||||
|
||||
func apiInputPageListItem() -> Api.PageListItem {
|
||||
switch self {
|
||||
case let .text(value, _, checked):
|
||||
return .pageListItemText(Api.PageListItem.Cons_pageListItemText(flags: InstantPageListItem.apiFlags(fromChecked: checked), text: value.apiRichText()))
|
||||
case let .blocks(blocks, _, checked):
|
||||
return .pageListItemBlocks(Api.PageListItem.Cons_pageListItemBlocks(flags: InstantPageListItem.apiFlags(fromChecked: checked), blocks: blocks.compactMap { $0.apiInputBlock() }))
|
||||
case .unknown:
|
||||
return .pageListItemText(Api.PageListItem.Cons_pageListItemText(flags: 0, text: .textPlain(Api.RichText.Cons_textPlain(text: ""))))
|
||||
}
|
||||
}
|
||||
|
||||
func apiInputPageOrderedListItem() -> Api.InputPageListOrderedItem {
|
||||
switch self {
|
||||
case let .text(value, num, checked):
|
||||
var flags: Int32 = InstantPageListItem.apiFlags(fromChecked: checked)
|
||||
|
||||
var inputNum: Int32?
|
||||
if let num, let numValue = Int32(num) {
|
||||
inputNum = numValue
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
|
||||
return .inputPageListOrderedItemText(Api.InputPageListOrderedItem.Cons_inputPageListOrderedItemText(flags: flags, text: value.apiRichText(), value: inputNum, type: nil))
|
||||
case let .blocks(blocks, num, checked):
|
||||
var flags: Int32 = InstantPageListItem.apiFlags(fromChecked: checked)
|
||||
|
||||
var inputNum: Int32?
|
||||
if let num, let numValue = Int32(num) {
|
||||
inputNum = numValue
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
|
||||
return .inputPageListOrderedItemBlocks(Api.InputPageListOrderedItem.Cons_inputPageListOrderedItemBlocks(flags: flags, blocks: blocks.compactMap { $0.apiInputBlock() }, value: inputNum, type: nil))
|
||||
case .unknown:
|
||||
return .inputPageListOrderedItemText(Api.InputPageListOrderedItem.Cons_inputPageListOrderedItemText(flags: 0, text: .textPlain(Api.RichText.Cons_textPlain(text: "")), value: nil, type: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,33 +120,85 @@ extension InstantPageListItem {
|
|||
extension InstantPageTableCell {
|
||||
convenience init(apiTableCell: Api.PageTableCell) {
|
||||
switch apiTableCell {
|
||||
case let .pageTableCell(pageTableCellData):
|
||||
let (flags, text, colspan, rowspan) = (pageTableCellData.flags, pageTableCellData.text, pageTableCellData.colspan, pageTableCellData.rowspan)
|
||||
var alignment = TableHorizontalAlignment.left
|
||||
if (flags & (1 << 3)) != 0 {
|
||||
alignment = .center
|
||||
} else if (flags & (1 << 4)) != 0 {
|
||||
alignment = .right
|
||||
}
|
||||
var verticalAlignment = TableVerticalAlignment.top
|
||||
if (flags & (1 << 5)) != 0 {
|
||||
verticalAlignment = .middle
|
||||
} else if (flags & (1 << 6)) != 0 {
|
||||
verticalAlignment = .bottom
|
||||
}
|
||||
self.init(text: text != nil ? RichText(apiText: text!) : nil, header: (flags & (1 << 0)) != 0, alignment: alignment, verticalAlignment: verticalAlignment, colspan: colspan ?? 0, rowspan: rowspan ?? 0)
|
||||
case let .pageTableCell(pageTableCellData):
|
||||
let (flags, text, colspan, rowspan) = (pageTableCellData.flags, pageTableCellData.text, pageTableCellData.colspan, pageTableCellData.rowspan)
|
||||
var alignment = TableHorizontalAlignment.left
|
||||
if (flags & (1 << 3)) != 0 {
|
||||
alignment = .center
|
||||
} else if (flags & (1 << 4)) != 0 {
|
||||
alignment = .right
|
||||
}
|
||||
var verticalAlignment = TableVerticalAlignment.top
|
||||
if (flags & (1 << 5)) != 0 {
|
||||
verticalAlignment = .middle
|
||||
} else if (flags & (1 << 6)) != 0 {
|
||||
verticalAlignment = .bottom
|
||||
}
|
||||
self.init(text: text != nil ? RichText(apiText: text!) : nil, header: (flags & (1 << 0)) != 0, alignment: alignment, verticalAlignment: verticalAlignment, colspan: colspan ?? 0, rowspan: rowspan ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
func inputPageTableCell() -> Api.PageTableCell {
|
||||
var flags: Int32 = 0
|
||||
|
||||
switch self.alignment {
|
||||
case .left:
|
||||
break
|
||||
case .center:
|
||||
flags |= (1 << 3)
|
||||
case .right:
|
||||
flags |= (1 << 4)
|
||||
}
|
||||
|
||||
switch self.verticalAlignment {
|
||||
case .top:
|
||||
break
|
||||
case .middle:
|
||||
flags |= (1 << 5)
|
||||
case .bottom:
|
||||
flags |= (1 << 6)
|
||||
}
|
||||
|
||||
if self.header {
|
||||
flags |= (1 << 0)
|
||||
}
|
||||
|
||||
var inputText: Api.RichText?
|
||||
if let text = self.text {
|
||||
inputText = text.apiRichText()
|
||||
if inputText != nil {
|
||||
flags |= (1 << 7)
|
||||
}
|
||||
}
|
||||
|
||||
var inputColspan: Int32?
|
||||
if self.colspan != 0 {
|
||||
inputColspan = self.colspan
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
|
||||
var inputRowspan: Int32?
|
||||
if self.rowspan != 0 {
|
||||
inputRowspan = self.rowspan
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
|
||||
return .pageTableCell(Api.PageTableCell.Cons_pageTableCell(flags: flags, text: inputText, colspan: inputColspan, rowspan: inputRowspan))
|
||||
}
|
||||
}
|
||||
|
||||
extension InstantPageTableRow {
|
||||
convenience init(apiTableRow: Api.PageTableRow) {
|
||||
switch apiTableRow {
|
||||
case let .pageTableRow(pageTableRowData):
|
||||
let cells = pageTableRowData.cells
|
||||
self.init(cells: cells.map({ InstantPageTableCell(apiTableCell: $0) }))
|
||||
case let .pageTableRow(pageTableRowData):
|
||||
let cells = pageTableRowData.cells
|
||||
self.init(cells: cells.map({ InstantPageTableCell(apiTableCell: $0) }))
|
||||
}
|
||||
}
|
||||
|
||||
func inputPageTableRow() -> Api.PageTableRow {
|
||||
return .pageTableRow(Api.PageTableRow.Cons_pageTableRow(cells: self.cells.map { $0.inputPageTableCell() }))
|
||||
}
|
||||
}
|
||||
|
||||
extension InstantPageRelatedArticle {
|
||||
|
|
@ -198,6 +317,106 @@ extension InstantPageBlock {
|
|||
default:
|
||||
self = .unsupported
|
||||
}
|
||||
case let .pageBlockHeading1(pageBlockHeading1):
|
||||
self = .heading(text: RichText(apiText: pageBlockHeading1.text), level: 1)
|
||||
case let .pageBlockHeading2(pageBlockHeading2):
|
||||
self = .heading(text: RichText(apiText: pageBlockHeading2.text), level: 2)
|
||||
case let .pageBlockHeading3(pageBlockHeading3):
|
||||
self = .heading(text: RichText(apiText: pageBlockHeading3.text), level: 3)
|
||||
case let .pageBlockHeading4(pageBlockHeading4):
|
||||
self = .heading(text: RichText(apiText: pageBlockHeading4.text), level: 4)
|
||||
case let .pageBlockHeading5(pageBlockHeading5):
|
||||
self = .heading(text: RichText(apiText: pageBlockHeading5.text), level: 5)
|
||||
case let .pageBlockHeading6(pageBlockHeading6):
|
||||
self = .heading(text: RichText(apiText: pageBlockHeading6.text), level: 6)
|
||||
case let .pageBlockMath(pageBlockMath):
|
||||
self = .formula(latex: pageBlockMath.source)
|
||||
case .inputPageBlockMap, .inputPageBlockOrderedList, .pageBlockThinking:
|
||||
self = .unsupported
|
||||
}
|
||||
}
|
||||
|
||||
func apiInputBlock() -> Api.PageBlock? {
|
||||
switch self {
|
||||
case .unsupported, .title, .subtitle, .kicker, .header, .subheader, .cover, .channelBanner, .authorDate, .relatedArticles, .webEmbed, .postEmbed:
|
||||
return nil
|
||||
case let .heading(text, level):
|
||||
let block: Api.PageBlock
|
||||
switch level {
|
||||
case 0, 1:
|
||||
block = .pageBlockHeading1(Api.PageBlock.Cons_pageBlockHeading1(text: text.apiRichText()))
|
||||
case 2:
|
||||
block = .pageBlockHeading2(Api.PageBlock.Cons_pageBlockHeading2(text: text.apiRichText()))
|
||||
case 3:
|
||||
block = .pageBlockHeading3(Api.PageBlock.Cons_pageBlockHeading3(text: text.apiRichText()))
|
||||
case 4:
|
||||
block = .pageBlockHeading4(Api.PageBlock.Cons_pageBlockHeading4(text: text.apiRichText()))
|
||||
case 5:
|
||||
block = .pageBlockHeading5(Api.PageBlock.Cons_pageBlockHeading5(text: text.apiRichText()))
|
||||
default:
|
||||
block = .pageBlockHeading6(Api.PageBlock.Cons_pageBlockHeading6(text: text.apiRichText()))
|
||||
}
|
||||
return block
|
||||
case let .formula(latex):
|
||||
return .pageBlockMath(Api.PageBlock.Cons_pageBlockMath(source: latex))
|
||||
case let .paragraph(value):
|
||||
return .pageBlockParagraph(Api.PageBlock.Cons_pageBlockParagraph(text: value.apiRichText()))
|
||||
case let .preformatted(text, language):
|
||||
return .pageBlockPreformatted(Api.PageBlock.Cons_pageBlockPreformatted(text: text.apiRichText(), language: language ?? ""))
|
||||
case let .footer(value):
|
||||
return .pageBlockFooter(Api.PageBlock.Cons_pageBlockFooter(text: value.apiRichText()))
|
||||
case .divider:
|
||||
return .pageBlockDivider
|
||||
case let .anchor(value):
|
||||
return .pageBlockAnchor(Api.PageBlock.Cons_pageBlockAnchor(name: value))
|
||||
case let .list(items, ordered):
|
||||
if ordered {
|
||||
return .inputPageBlockOrderedList(Api.PageBlock.Cons_inputPageBlockOrderedList(flags: 0, items: items.map { $0.apiInputPageOrderedListItem() }, start: nil, type: nil))
|
||||
} else {
|
||||
return .pageBlockList(Api.PageBlock.Cons_pageBlockList(items: items.map { $0.apiInputPageListItem() }))
|
||||
}
|
||||
case let .blockQuote(text, caption):
|
||||
return .pageBlockBlockquote(Api.PageBlock.Cons_pageBlockBlockquote(text: text.apiRichText(), caption: caption.apiRichText()))
|
||||
case let .pullQuote(text, caption):
|
||||
return .pageBlockPullquote(Api.PageBlock.Cons_pageBlockPullquote(text: text.apiRichText(), caption: caption.apiRichText()))
|
||||
case let .image(id, caption, url, webpageId):
|
||||
var flags: Int32 = 0
|
||||
if url != nil && webpageId != nil {
|
||||
flags |= 1 << 0
|
||||
}
|
||||
return .pageBlockPhoto(Api.PageBlock.Cons_pageBlockPhoto(flags: flags, photoId: id.id, caption: .pageCaption(Api.PageCaption.Cons_pageCaption(text: caption.text.apiRichText(), credit: caption.credit.apiRichText())), url: url, webpageId: webpageId?.id))
|
||||
case let .video(id, caption, autoplay, loop):
|
||||
var flags: Int32 = 0
|
||||
if autoplay {
|
||||
flags |= 1 << 0
|
||||
}
|
||||
if loop {
|
||||
flags |= 1 << 1
|
||||
}
|
||||
return .pageBlockVideo(Api.PageBlock.Cons_pageBlockVideo(flags: flags, videoId: id.id, caption: .pageCaption(Api.PageCaption.Cons_pageCaption(text: caption.text.apiRichText(), credit: caption.credit.apiRichText()))))
|
||||
case let .audio(id, caption):
|
||||
return .pageBlockAudio(Api.PageBlock.Cons_pageBlockAudio(audioId: id.id, caption: .pageCaption(Api.PageCaption.Cons_pageCaption(text: caption.text.apiRichText(), credit: caption.credit.apiRichText()))))
|
||||
case let .collage(items, caption):
|
||||
return .pageBlockCollage(Api.PageBlock.Cons_pageBlockCollage(items: items.compactMap { $0.apiInputBlock() }, caption: .pageCaption(Api.PageCaption.Cons_pageCaption(text: caption.text.apiRichText(), credit: caption.credit.apiRichText()))))
|
||||
case let .slideshow(items, caption):
|
||||
return .pageBlockSlideshow(Api.PageBlock.Cons_pageBlockSlideshow(items: items.compactMap { $0.apiInputBlock() }, caption: .pageCaption(Api.PageCaption.Cons_pageCaption(text: caption.text.apiRichText(), credit: caption.credit.apiRichText()))))
|
||||
case let .table(title, rows, bordered, striped):
|
||||
var flags: Int32 = 0
|
||||
if bordered {
|
||||
flags |= (1 << 0)
|
||||
}
|
||||
if striped {
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
return .pageBlockTable(Api.PageBlock.Cons_pageBlockTable(flags: flags, title: title.apiRichText(), rows: rows.map { $0.inputPageTableRow() }))
|
||||
case let .details(title, blocks, expanded):
|
||||
var flags: Int32 = 0
|
||||
if expanded {
|
||||
flags |= (1 << 0)
|
||||
}
|
||||
return .pageBlockDetails(Api.PageBlock.Cons_pageBlockDetails(flags: flags, blocks: blocks.compactMap { $0.apiInputBlock() }, title: title.apiRichText()))
|
||||
case let .map(latitude, longitude, zoom, dimensions, caption):
|
||||
return .inputPageBlockMap(Api.PageBlock.Cons_inputPageBlockMap(geo: .inputGeoPoint(Api.InputGeoPoint.Cons_inputGeoPoint(flags: 0, lat: latitude, long: longitude, accuracyRadius: nil)), zoom: zoom, w: dimensions.width, h: dimensions.height, caption: .pageCaption(Api.PageCaption.Cons_pageCaption(text: caption.text.apiRichText(), credit: caption.credit.apiRichText()))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -212,15 +431,15 @@ extension InstantPage {
|
|||
let url: String
|
||||
let views: Int32?
|
||||
switch apiPage {
|
||||
case let .page(pageData):
|
||||
let (flags, pageUrl, pageBlocks, pagePhotos, pageDocuments, pageViews) = (pageData.flags, pageData.url, pageData.blocks, pageData.photos, pageData.documents, pageData.views)
|
||||
url = pageUrl
|
||||
blocks = pageBlocks
|
||||
photos = pagePhotos
|
||||
files = pageDocuments
|
||||
isComplete = (flags & (1 << 0)) == 0
|
||||
rtl = (flags & (1 << 1)) != 0
|
||||
views = pageViews
|
||||
case let .page(pageData):
|
||||
let (flags, pageUrl, pageBlocks, pagePhotos, pageDocuments, pageViews) = (pageData.flags, pageData.url, pageData.blocks, pageData.photos, pageData.documents, pageData.views)
|
||||
url = pageUrl
|
||||
blocks = pageBlocks
|
||||
photos = pagePhotos
|
||||
files = pageDocuments
|
||||
isComplete = (flags & (1 << 0)) == 0
|
||||
rtl = (flags & (1 << 1)) != 0
|
||||
views = pageViews
|
||||
}
|
||||
var media: [MediaId: Media] = [:]
|
||||
for photo in photos {
|
||||
|
|
|
|||
|
|
@ -6,53 +6,142 @@ import TelegramApi
|
|||
extension RichText {
|
||||
init(apiText: Api.RichText) {
|
||||
switch apiText {
|
||||
case .textEmpty:
|
||||
self = .empty
|
||||
case let .textPlain(textPlainData):
|
||||
let text = textPlainData.text
|
||||
self = .plain(text)
|
||||
case let .textBold(textBoldData):
|
||||
let text = textBoldData.text
|
||||
self = .bold(RichText(apiText: text))
|
||||
case let .textItalic(textItalicData):
|
||||
let text = textItalicData.text
|
||||
self = .italic(RichText(apiText: text))
|
||||
case let .textUnderline(textUnderlineData):
|
||||
let text = textUnderlineData.text
|
||||
self = .underline(RichText(apiText: text))
|
||||
case let .textStrike(textStrikeData):
|
||||
let text = textStrikeData.text
|
||||
self = .strikethrough(RichText(apiText: text))
|
||||
case let .textFixed(textFixedData):
|
||||
let text = textFixedData.text
|
||||
self = .fixed(RichText(apiText: text))
|
||||
case let .textUrl(textUrlData):
|
||||
let (text, url, webpageId) = (textUrlData.text, textUrlData.url, textUrlData.webpageId)
|
||||
self = .url(text: RichText(apiText: text), url: url, webpageId: webpageId == 0 ? nil : MediaId(namespace: Namespaces.Media.CloudWebpage, id: webpageId))
|
||||
case let .textEmail(textEmailData):
|
||||
let (text, email) = (textEmailData.text, textEmailData.email)
|
||||
self = .email(text: RichText(apiText: text), email: email)
|
||||
case let .textConcat(textConcatData):
|
||||
let texts = textConcatData.texts
|
||||
self = .concat(texts.map({ RichText(apiText: $0) }))
|
||||
case let .textSubscript(textSubscriptData):
|
||||
let text = textSubscriptData.text
|
||||
self = .subscript(RichText(apiText: text))
|
||||
case let .textSuperscript(textSuperscriptData):
|
||||
let text = textSuperscriptData.text
|
||||
self = .superscript(RichText(apiText: text))
|
||||
case let .textMarked(textMarkedData):
|
||||
let text = textMarkedData.text
|
||||
self = .marked(RichText(apiText: text))
|
||||
case let .textPhone(textPhoneData):
|
||||
let (text, phone) = (textPhoneData.text, textPhoneData.phone)
|
||||
self = .phone(text: RichText(apiText: text), phone: phone)
|
||||
case let .textImage(textImageData):
|
||||
let (documentId, w, h) = (textImageData.documentId, textImageData.w, textImageData.h)
|
||||
self = .image(id: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), dimensions: PixelDimensions(width: w, height: h))
|
||||
case let .textAnchor(textAnchorData):
|
||||
let (text, name) = (textAnchorData.text, textAnchorData.name)
|
||||
self = .anchor(text: RichText(apiText: text), name: name)
|
||||
case .textEmpty:
|
||||
self = .empty
|
||||
case let .textPlain(textPlainData):
|
||||
let text = textPlainData.text
|
||||
self = .plain(text)
|
||||
case let .textBold(textBoldData):
|
||||
let text = textBoldData.text
|
||||
self = .bold(RichText(apiText: text))
|
||||
case let .textItalic(textItalicData):
|
||||
let text = textItalicData.text
|
||||
self = .italic(RichText(apiText: text))
|
||||
case let .textUnderline(textUnderlineData):
|
||||
let text = textUnderlineData.text
|
||||
self = .underline(RichText(apiText: text))
|
||||
case let .textStrike(textStrikeData):
|
||||
let text = textStrikeData.text
|
||||
self = .strikethrough(RichText(apiText: text))
|
||||
case let .textFixed(textFixedData):
|
||||
let text = textFixedData.text
|
||||
self = .fixed(RichText(apiText: text))
|
||||
case let .textUrl(textUrlData):
|
||||
let (text, url, webpageId) = (textUrlData.text, textUrlData.url, textUrlData.webpageId)
|
||||
self = .url(text: RichText(apiText: text), url: url, webpageId: webpageId == 0 ? nil : MediaId(namespace: Namespaces.Media.CloudWebpage, id: webpageId))
|
||||
case let .textEmail(textEmailData):
|
||||
let (text, email) = (textEmailData.text, textEmailData.email)
|
||||
self = .email(text: RichText(apiText: text), email: email)
|
||||
case let .textConcat(textConcatData):
|
||||
let texts = textConcatData.texts
|
||||
self = .concat(texts.map({ RichText(apiText: $0) }))
|
||||
case let .textSubscript(textSubscriptData):
|
||||
let text = textSubscriptData.text
|
||||
self = .subscript(RichText(apiText: text))
|
||||
case let .textSuperscript(textSuperscriptData):
|
||||
let text = textSuperscriptData.text
|
||||
self = .superscript(RichText(apiText: text))
|
||||
case let .textMarked(textMarkedData):
|
||||
let text = textMarkedData.text
|
||||
self = .marked(RichText(apiText: text))
|
||||
case let .textPhone(textPhoneData):
|
||||
let (text, phone) = (textPhoneData.text, textPhoneData.phone)
|
||||
self = .phone(text: RichText(apiText: text), phone: phone)
|
||||
case let .textImage(textImageData):
|
||||
let (documentId, w, h) = (textImageData.documentId, textImageData.w, textImageData.h)
|
||||
self = .image(id: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), dimensions: PixelDimensions(width: w, height: h))
|
||||
case let .textAnchor(textAnchorData):
|
||||
let (text, name) = (textAnchorData.text, textAnchorData.name)
|
||||
self = .anchor(text: RichText(apiText: text), name: name)
|
||||
case let .textCustomEmoji(data):
|
||||
self = .textCustomEmoji(fileId: data.documentId, alt: data.alt)
|
||||
case let .textMath(textMath):
|
||||
self = .formula(latex: textMath.source)
|
||||
case let .textAutoEmail(textAutoEmailData):
|
||||
self = .textAutoEmail(text: RichText(apiText: textAutoEmailData.text))
|
||||
case let .textAutoPhone(textAutoPhoneData):
|
||||
self = .textAutoPhone(text: RichText(apiText: textAutoPhoneData.text))
|
||||
case let .textAutoUrl(textAutoUrlData):
|
||||
self = .textAutoUrl(text: RichText(apiText: textAutoUrlData.text))
|
||||
case let .textBankCard(textBankCardData):
|
||||
self = .textBankCard(text: RichText(apiText: textBankCardData.text))
|
||||
case let .textBotCommand(textBotCommandData):
|
||||
self = .textBotCommand(text: RichText(apiText: textBotCommandData.text))
|
||||
case let .textCashtag(textCashtagData):
|
||||
self = .textCashtag(text: RichText(apiText: textCashtagData.text))
|
||||
case let .textDate(value):
|
||||
let _ = value
|
||||
//TODO:localize
|
||||
self = .plain("")
|
||||
case let .textHashtag(textHashtagData):
|
||||
self = .textHashtag(text: RichText(apiText: textHashtagData.text))
|
||||
case let .textMention(textMentionData):
|
||||
self = .textMention(text: RichText(apiText: textMentionData.text))
|
||||
case let .textMentionName(textMentionNameData):
|
||||
self = .textMentionName(text: RichText(apiText: textMentionNameData.text), peerId: textMentionNameData.userId)
|
||||
case let .textSpoiler(textSpoilerData):
|
||||
self = .textSpoiler(text: RichText(apiText: textSpoilerData.text))
|
||||
}
|
||||
}
|
||||
|
||||
func apiRichText() -> Api.RichText {
|
||||
switch self {
|
||||
case .empty:
|
||||
return .textPlain(Api.RichText.Cons_textPlain(text: ""))
|
||||
case let .plain(value):
|
||||
return .textPlain(Api.RichText.Cons_textPlain(text: value))
|
||||
case let .bold(value):
|
||||
return .textBold(Api.RichText.Cons_textBold(text: value.apiRichText()))
|
||||
case let .italic(value):
|
||||
return .textItalic(Api.RichText.Cons_textItalic(text: value.apiRichText()))
|
||||
case let .underline(value):
|
||||
return .textUnderline(Api.RichText.Cons_textUnderline(text: value.apiRichText()))
|
||||
case let .strikethrough(value):
|
||||
return .textStrike(Api.RichText.Cons_textStrike(text: value.apiRichText()))
|
||||
case let .fixed(value):
|
||||
return .textFixed(Api.RichText.Cons_textFixed(text: value.apiRichText()))
|
||||
case let .url(text, url, webpageId):
|
||||
return .textUrl(Api.RichText.Cons_textUrl(text: text.apiRichText(), url: url, webpageId: webpageId?.id ?? 0))
|
||||
case let .email(text, email):
|
||||
return .textEmail(Api.RichText.Cons_textEmail(text: text.apiRichText(), email: email))
|
||||
case let .concat(values):
|
||||
return .textConcat(Api.RichText.Cons_textConcat(texts: values.map { $0.apiRichText() }))
|
||||
case let .`subscript`(value):
|
||||
return .textSubscript(Api.RichText.Cons_textSubscript(text: value.apiRichText()))
|
||||
case let .superscript(value):
|
||||
return .textSuperscript(Api.RichText.Cons_textSuperscript(text: value.apiRichText()))
|
||||
case let .marked(value):
|
||||
return .textMarked(Api.RichText.Cons_textMarked(text: value.apiRichText()))
|
||||
case let .phone(text, phone):
|
||||
return .textPhone(Api.RichText.Cons_textPhone(text: text.apiRichText(), phone: phone))
|
||||
case let .image(id, dimensions):
|
||||
return .textImage(Api.RichText.Cons_textImage(documentId: id.id, w: dimensions.width, h: dimensions.height))
|
||||
case let .anchor(text, name):
|
||||
return .textAnchor(Api.RichText.Cons_textAnchor(text: text.apiRichText(), name: name))
|
||||
case let .formula(latex):
|
||||
return .textMath(Api.RichText.Cons_textMath(source: latex))
|
||||
case let .textCustomEmoji(fileId, alt):
|
||||
return .textCustomEmoji(Api.RichText.Cons_textCustomEmoji(documentId: fileId, alt: alt))
|
||||
case let .textAutoEmail(text):
|
||||
return .textAutoEmail(Api.RichText.Cons_textAutoEmail(text: text.apiRichText()))
|
||||
case let .textAutoPhone(text):
|
||||
return .textAutoPhone(Api.RichText.Cons_textAutoPhone(text: text.apiRichText()))
|
||||
case let .textAutoUrl(text):
|
||||
return .textAutoUrl(Api.RichText.Cons_textAutoUrl(text: text.apiRichText()))
|
||||
case let .textBankCard(text):
|
||||
return .textBankCard(Api.RichText.Cons_textBankCard(text: text.apiRichText()))
|
||||
case let .textBotCommand(text):
|
||||
return .textBotCommand(Api.RichText.Cons_textBotCommand(text: text.apiRichText()))
|
||||
case let .textCashtag(text):
|
||||
return .textCashtag(Api.RichText.Cons_textCashtag(text: text.apiRichText()))
|
||||
case let .textHashtag(text):
|
||||
return .textHashtag(Api.RichText.Cons_textHashtag(text: text.apiRichText()))
|
||||
case let .textMention(text):
|
||||
return .textMention(Api.RichText.Cons_textMention(text: text.apiRichText()))
|
||||
case let .textMentionName(text, peerId):
|
||||
return .textMentionName(Api.RichText.Cons_textMentionName(text: text.apiRichText(), userId: peerId))
|
||||
case let .textSpoiler(text):
|
||||
return .textSpoiler(Api.RichText.Cons_textSpoiler(text: text.apiRichText()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -848,7 +848,7 @@ extension StoreMessage {
|
|||
convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) {
|
||||
switch apiMessage {
|
||||
case let .message(messageData):
|
||||
let (flags, flags2, id, fromId, boosts, rank, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, guestChatViaFrom, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId, messageEffectId, factCheck, reportDeliveryUntilDate, paidMessageStars, suggestedPost, scheduledRepeatPeriod, summaryFromLanguage) = (messageData.flags, messageData.flags2, messageData.id, messageData.fromId, messageData.fromBoostsApplied, messageData.fromRank, messageData.peerId, messageData.savedPeerId, messageData.fwdFrom, messageData.viaBotId, messageData.viaBusinessBotId, messageData.guestchatViaFrom, messageData.replyTo, messageData.date, messageData.message, messageData.media, messageData.replyMarkup, messageData.entities, messageData.views, messageData.forwards, messageData.replies, messageData.editDate, messageData.postAuthor, messageData.groupedId, messageData.reactions, messageData.restrictionReason, messageData.ttlPeriod, messageData.quickReplyShortcutId, messageData.effect, messageData.factcheck, messageData.reportDeliveryUntilDate, messageData.paidMessageStars, messageData.suggestedPost, messageData.scheduleRepeatPeriod, messageData.summaryFromLanguage)
|
||||
let (flags, flags2, id, fromId, boosts, rank, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, guestChatViaFrom, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId, messageEffectId, factCheck, reportDeliveryUntilDate, paidMessageStars, suggestedPost, scheduledRepeatPeriod, summaryFromLanguage) = (messageData.flags, messageData.flags2, messageData.id, messageData.fromId, messageData.fromBoostsApplied, messageData.fromRank, messageData.peerId, messageData.savedPeerId, messageData.fwdFrom, messageData.viaBotId, messageData.viaBusinessBotId, messageData.guestchatViaFrom, messageData.replyTo, messageData.date, messageData.message, messageData.media, messageData.replyMarkup, messageData.entities, messageData.views, messageData.forwards, messageData.replies, messageData.editDate, messageData.postAuthor, messageData.groupedId, messageData.reactions, messageData.restrictionReason, messageData.ttlPeriod, messageData.quickReplyShortcutId, messageData.effect, messageData.factcheck, messageData.reportDeliveryUntilDate, messageData.paidMessageStars, messageData.suggestedPost, messageData.scheduleRepeatPeriod, messageData.summaryFromLanguage)
|
||||
var attributes: [MessageAttribute] = []
|
||||
|
||||
if (flags2 & (1 << 4)) != 0 {
|
||||
|
|
@ -1133,7 +1133,7 @@ extension StoreMessage {
|
|||
}
|
||||
|
||||
var entitiesAttribute: TextEntitiesMessageAttribute?
|
||||
if let entities = entities, !entities.isEmpty {
|
||||
if let entities, !entities.isEmpty {
|
||||
let attribute = TextEntitiesMessageAttribute(entities: messageTextEntitiesFromApiEntities(entities))
|
||||
entitiesAttribute = attribute
|
||||
attributes.append(attribute)
|
||||
|
|
@ -1155,6 +1155,10 @@ extension StoreMessage {
|
|||
attributes.append(attribute)
|
||||
}
|
||||
}
|
||||
|
||||
if let richMessage = messageData.richMessage {
|
||||
attributes.append(RichTextMessageAttribute(apiRichMessage: richMessage))
|
||||
}
|
||||
|
||||
if (flags & (1 << 19)) != 0 {
|
||||
attributes.append(ContentRequiresValidationMessageAttribute())
|
||||
|
|
|
|||
|
|
@ -255,6 +255,8 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt
|
|||
switch attribute {
|
||||
case _ as TextEntitiesMessageAttribute:
|
||||
return true
|
||||
case _ as RichTextMessageAttribute:
|
||||
return true
|
||||
case _ as InlineBotMessageAttribute:
|
||||
return true
|
||||
case _ as OutgoingMessageInfoAttribute:
|
||||
|
|
@ -306,6 +308,8 @@ private func filterMessageAttributesForForwardedMessage(_ attributes: [MessageAt
|
|||
switch attribute {
|
||||
case _ as TextEntitiesMessageAttribute:
|
||||
return true
|
||||
case _ as RichTextMessageAttribute:
|
||||
return true
|
||||
case _ as InlineBotMessageAttribute:
|
||||
return true
|
||||
case _ as NotificationInfoMessageAttribute:
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ private final class PendingUpdateMessageManagerImpl {
|
|||
}
|
||||
}
|
||||
|
||||
func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, invertMediaAttribute: InvertMediaMessageAttribute?, disableUrlPreview: Bool) {
|
||||
func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, richText: RichTextMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, invertMediaAttribute: InvertMediaMessageAttribute?, disableUrlPreview: Bool) {
|
||||
if let context = self.contexts[messageId] {
|
||||
self.contexts.removeValue(forKey: messageId)
|
||||
context.disposable.dispose()
|
||||
|
|
@ -75,7 +75,7 @@ private final class PendingUpdateMessageManagerImpl {
|
|||
self.contexts[messageId] = context
|
||||
|
||||
let queue = self.queue
|
||||
disposable.set((requestEditMessage(accountPeerId: self.stateManager.accountPeerId, postbox: self.postbox, network: self.network, stateManager: self.stateManager, transformOutgoingMessageMedia: self.transformOutgoingMessageMedia, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleInfoAttribute: nil, invertMediaAttribute: invertMediaAttribute)
|
||||
disposable.set((requestEditMessage(accountPeerId: self.stateManager.accountPeerId, postbox: self.postbox, network: self.network, stateManager: self.stateManager, transformOutgoingMessageMedia: self.transformOutgoingMessageMedia, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, richText: richText, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleInfoAttribute: nil, invertMediaAttribute: invertMediaAttribute)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self, weak context] value in
|
||||
queue.async {
|
||||
guard let strongSelf = self, let initialContext = context else {
|
||||
|
|
@ -163,9 +163,9 @@ public final class PendingUpdateMessageManager {
|
|||
})
|
||||
}
|
||||
|
||||
public func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute? = nil, invertMediaAttribute: InvertMediaMessageAttribute? = nil, disableUrlPreview: Bool = false) {
|
||||
public func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, richText: RichTextMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute? = nil, invertMediaAttribute: InvertMediaMessageAttribute? = nil, disableUrlPreview: Bool = false) {
|
||||
self.impl.with { impl in
|
||||
impl.add(messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, invertMediaAttribute: invertMediaAttribute, disableUrlPreview: disableUrlPreview)
|
||||
impl.add(messageId: messageId, text: text, media: media, entities: entities, richText: richText, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, invertMediaAttribute: invertMediaAttribute, disableUrlPreview: disableUrlPreview)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,15 +27,15 @@ public enum RequestEditMessageError {
|
|||
case invalidGrouping
|
||||
}
|
||||
|
||||
func _internal_requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute?, invertMediaAttribute: InvertMediaMessageAttribute?) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
|
||||
return requestEditMessage(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, stateManager: account.stateManager, transformOutgoingMessageMedia: account.transformOutgoingMessageMedia, messageMediaPreuploadManager: account.messageMediaPreuploadManager, mediaReferenceRevalidationContext: account.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleInfoAttribute: scheduleInfoAttribute, invertMediaAttribute: invertMediaAttribute)
|
||||
func _internal_requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, richText: RichTextMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute?, invertMediaAttribute: InvertMediaMessageAttribute?) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
|
||||
return requestEditMessage(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, stateManager: account.stateManager, transformOutgoingMessageMedia: account.transformOutgoingMessageMedia, messageMediaPreuploadManager: account.messageMediaPreuploadManager, mediaReferenceRevalidationContext: account.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, richText: richText, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleInfoAttribute: scheduleInfoAttribute, invertMediaAttribute: invertMediaAttribute)
|
||||
}
|
||||
|
||||
func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute?, invertMediaAttribute: InvertMediaMessageAttribute?) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
|
||||
return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, invertMediaAttribute: invertMediaAttribute, disableUrlPreview: disableUrlPreview, scheduleInfoAttribute: scheduleInfoAttribute, forceReupload: false)
|
||||
func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, richText: RichTextMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute?, invertMediaAttribute: InvertMediaMessageAttribute?) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
|
||||
return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, richText: richText, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, invertMediaAttribute: invertMediaAttribute, disableUrlPreview: disableUrlPreview, scheduleInfoAttribute: scheduleInfoAttribute, forceReupload: false)
|
||||
|> `catch` { error -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> in
|
||||
if case .invalidReference = error {
|
||||
return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, invertMediaAttribute: invertMediaAttribute, disableUrlPreview: disableUrlPreview, scheduleInfoAttribute: scheduleInfoAttribute, forceReupload: true)
|
||||
return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, richText: richText, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, invertMediaAttribute: invertMediaAttribute, disableUrlPreview: disableUrlPreview, scheduleInfoAttribute: scheduleInfoAttribute, forceReupload: true)
|
||||
} else {
|
||||
return .fail(error)
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Networ
|
|||
}
|
||||
}
|
||||
|
||||
private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, invertMediaAttribute: InvertMediaMessageAttribute?, disableUrlPreview: Bool, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute?, forceReupload: Bool) -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> {
|
||||
private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, richText: RichTextMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, invertMediaAttribute: InvertMediaMessageAttribute?, disableUrlPreview: Bool, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute?, forceReupload: Bool) -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> {
|
||||
let uploadedMedia: Signal<PendingMessageUploadedContentResult?, NoError>
|
||||
switch media {
|
||||
case .keep:
|
||||
|
|
@ -146,15 +146,21 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
|
|||
}
|
||||
|> mapError { _ -> RequestEditMessageInternalError in }
|
||||
|> mapToSignal { peer, message, associatedPeers -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> in
|
||||
if let peer = peer, let message = message, let inputPeer = apiInputPeer(peer) {
|
||||
if let peer, let message, let inputPeer = apiInputPeer(peer) {
|
||||
var flags: Int32 = 1 << 11
|
||||
|
||||
var apiEntities: [Api.MessageEntity]?
|
||||
if let entities = entities {
|
||||
if let entities {
|
||||
apiEntities = apiTextAttributeEntities(entities, associatedPeers: associatedPeers)
|
||||
flags |= Int32(1 << 3)
|
||||
}
|
||||
|
||||
var apiRichMessage: Api.InputRichMessage?
|
||||
if let richText {
|
||||
apiRichMessage = richText.apiInputRichMessage()
|
||||
flags |= Int32(1 << 23)
|
||||
}
|
||||
|
||||
if disableUrlPreview {
|
||||
flags |= Int32(1 << 1)
|
||||
}
|
||||
|
|
@ -201,7 +207,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
|
|||
flags |= Int32(1 << 17)
|
||||
}
|
||||
|
||||
return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime, scheduleRepeatPeriod: effectiveScheduleRepeatPeriod, quickReplyShortcutId: quickReplyShortcutId))
|
||||
return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime, scheduleRepeatPeriod: effectiveScheduleRepeatPeriod, quickReplyShortcutId: quickReplyShortcutId, richMessage: apiRichMessage))
|
||||
|> map { result -> Api.Updates? in
|
||||
return result
|
||||
}
|
||||
|
|
@ -429,7 +435,7 @@ func _internal_requestEditLiveLocation(postbox: Postbox, network: Network, state
|
|||
inputMedia = .inputMediaGeoLive(.init(flags: 1 << 0, geoPoint: .inputGeoPoint(.init(flags: 0, lat: media.latitude, long: media.longitude, accuracyRadius: nil)), heading: nil, period: nil, proximityNotificationRadius: nil))
|
||||
}
|
||||
|
||||
return network.request(Api.functions.messages.editMessage(flags: 1 << 14, peer: inputPeer, id: messageId.id, message: nil, media: inputMedia, replyMarkup: nil, entities: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, quickReplyShortcutId: nil))
|
||||
return network.request(Api.functions.messages.editMessage(flags: 1 << 14, peer: inputPeer, id: messageId.id, message: nil, media: inputMedia, replyMarkup: nil, entities: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, quickReplyShortcutId: nil, richMessage: nil))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
|
|
|
|||
|
|
@ -366,6 +366,7 @@ private func sendUploadedMessageContent(
|
|||
var uniqueId: Int64 = 0
|
||||
var forwardSourceInfoAttribute: ForwardSourceInfoAttribute?
|
||||
var messageEntities: [Api.MessageEntity]?
|
||||
var apiRichMessage: Api.InputRichMessage?
|
||||
var replyMessageId: Int32?
|
||||
var topMsgId: Int32?
|
||||
var monoforumPeerId: Api.InputPeer?
|
||||
|
|
@ -442,6 +443,9 @@ private func sendUploadedMessageContent(
|
|||
allowPaidStars = attribute.stars.value
|
||||
} else if let attribute = attribute as? SuggestedPostMessageAttribute {
|
||||
suggestedPost = attribute.apiSuggestedPost(fixMinTime: Int32(Date().timeIntervalSince1970 + 10))
|
||||
} else if let attribute = attribute as? RichTextMessageAttribute {
|
||||
apiRichMessage = attribute.apiInputRichMessage()
|
||||
flags |= Int32(1 << 23)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -512,7 +516,7 @@ private func sendUploadedMessageContent(
|
|||
flags |= 1 << 22
|
||||
}
|
||||
|
||||
sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), info: .acknowledgement, tag: dependencyTag)
|
||||
sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost, richMessage: apiRichMessage), info: .acknowledgement, tag: dependencyTag)
|
||||
case let .media(inputMedia, text):
|
||||
if bubbleUpEmojiOrStickersets {
|
||||
flags |= Int32(1 << 15)
|
||||
|
|
@ -692,6 +696,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M
|
|||
var uniqueId: Int64 = Int64.random(in: Int64.min ... Int64.max)
|
||||
//var forwardSourceInfoAttribute: ForwardSourceInfoAttribute?
|
||||
var messageEntities: [Api.MessageEntity]?
|
||||
var apiRichMessage: Api.InputRichMessage?
|
||||
var replyMessageId: Int32?
|
||||
var replyToStoryId: StoryId?
|
||||
var scheduleTime: Int32?
|
||||
|
|
@ -731,6 +736,9 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M
|
|||
allowPaidStars = attribute.stars.value
|
||||
} else if let attribute = attribute as? SuggestedPostMessageAttribute {
|
||||
suggestedPost = attribute.apiSuggestedPost(fixMinTime: Int32(Date().timeIntervalSince1970 + 10))
|
||||
} else if let attribute = attribute as? RichTextMessageAttribute {
|
||||
apiRichMessage = attribute.apiInputRichMessage()
|
||||
flags |= Int32(1 << 23)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -767,7 +775,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M
|
|||
replyTo = .inputReplyToMessage(.init(flags: flags, replyToMsgId: threadId, topMsgId: threadId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil, pollOption: nil))
|
||||
}
|
||||
|
||||
sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars, suggestedPost: nil))
|
||||
sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars, suggestedPost: nil, richMessage: apiRichMessage))
|
||||
|> `catch` { _ -> Signal<Api.Updates, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -924,6 +924,11 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
|
|||
|
||||
var missingUpdatesFromChannels = Set<PeerId>()
|
||||
|
||||
enum TypingDraftText {
|
||||
case plain(Api.TextWithEntities)
|
||||
case rich(Api.RichMessage)
|
||||
}
|
||||
|
||||
for update in sortedUpdates(updates) {
|
||||
switch update {
|
||||
case let .updateChannelTooLong(updateChannelTooLongData):
|
||||
|
|
@ -1516,12 +1521,22 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
|
|||
let threadId = topMsgId.flatMap { Int64($0) }
|
||||
|
||||
if let date = updatesDate, date + 60 > serverTime {
|
||||
var typingDraftData: (randomId: Int64, text: TypingDraftText)?
|
||||
|
||||
if case let .sendMessageTextDraftAction(sendMessageTextDraftActionData) = type {
|
||||
let (randomId, text) = (sendMessageTextDraftActionData.randomId, sendMessageTextDraftActionData.text)
|
||||
switch text {
|
||||
case let .textWithEntities(textWithEntitiesData):
|
||||
let (text, entities) = (textWithEntitiesData.text, textWithEntitiesData.entities)
|
||||
updatedState.addPeerLiveTypingDraftUpdate(peerAndThreadId: PeerAndThreadId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), threadId: threadId), id: randomId, timestamp: date, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), text: text, entities: messageTextEntitiesFromApiEntities(entities))
|
||||
typingDraftData = (sendMessageTextDraftActionData.randomId, .plain(sendMessageTextDraftActionData.text))
|
||||
} else if case let .sendMessageRichMessageDraftAction(sendMessageRichMessageDraftActionData) = type {
|
||||
typingDraftData = (sendMessageRichMessageDraftActionData.randomId, .rich(sendMessageRichMessageDraftActionData.richMessage))
|
||||
}
|
||||
if let typingDraftData {
|
||||
switch typingDraftData.text {
|
||||
case let .plain(plain):
|
||||
if case let .textWithEntities(textWithEntitiesData) = plain {
|
||||
updatedState.addPeerLiveTypingDraftUpdate(peerAndThreadId: PeerAndThreadId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), threadId: threadId), id: typingDraftData.randomId, timestamp: date, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), content: .plain(text: textWithEntitiesData.text, entities: messageTextEntitiesFromApiEntities(textWithEntitiesData.entities)))
|
||||
}
|
||||
case let .rich(richMessage):
|
||||
let parsedRichMessage = RichTextMessageAttribute(apiRichMessage: richMessage)
|
||||
updatedState.addPeerLiveTypingDraftUpdate(peerAndThreadId: PeerAndThreadId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), threadId: threadId), id: typingDraftData.randomId, timestamp: date, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), content: .rich(parsedRichMessage))
|
||||
}
|
||||
} else {
|
||||
let activity = PeerInputActivity(apiType: type, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), timestamp: date)
|
||||
|
|
@ -1555,8 +1570,11 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
|
|||
switch text {
|
||||
case let .textWithEntities(textWithEntitiesData):
|
||||
let (text, entities) = (textWithEntitiesData.text, textWithEntitiesData.entities)
|
||||
updatedState.addPeerLiveTypingDraftUpdate(peerAndThreadId: PeerAndThreadId(peerId: channelPeerId, threadId: threadId), id: randomId, timestamp: date, peerId: userId.peerId, text: text, entities: messageTextEntitiesFromApiEntities(entities))
|
||||
updatedState.addPeerLiveTypingDraftUpdate(peerAndThreadId: PeerAndThreadId(peerId: channelPeerId, threadId: threadId), id: randomId, timestamp: date, peerId: userId.peerId, content: .plain(text: text, entities: messageTextEntitiesFromApiEntities(entities)))
|
||||
}
|
||||
} else if case let .sendMessageRichMessageDraftAction(sendMessageRichMessageDraftActionData) = type {
|
||||
let parsedRichMessage = RichTextMessageAttribute(apiRichMessage: sendMessageRichMessageDraftActionData.richMessage)
|
||||
updatedState.addPeerLiveTypingDraftUpdate(peerAndThreadId: PeerAndThreadId(peerId: channelPeerId, threadId: threadId), id: sendMessageRichMessageDraftActionData.randomId, timestamp: date, peerId: userId.peerId, content: .rich(parsedRichMessage))
|
||||
} else {
|
||||
let activity = PeerInputActivity(apiType: type, peerId: nil, timestamp: date)
|
||||
var category: PeerActivitySpace.Category = .global
|
||||
|
|
@ -4021,16 +4039,14 @@ func replayFinalState(
|
|||
var threadId: Int64?
|
||||
var authorId: PeerId
|
||||
var timestamp: Int32
|
||||
var text: String
|
||||
var entities: [MessageTextEntity]
|
||||
var content: PeerLiveTypingDraftUpdateContent
|
||||
|
||||
init(id: Int64, threadId: Int64?, authorId: PeerId, timestamp: Int32, text: String, entities: [MessageTextEntity]) {
|
||||
init(id: Int64, threadId: Int64?, authorId: PeerId, timestamp: Int32, content: PeerLiveTypingDraftUpdateContent) {
|
||||
self.id = id
|
||||
self.threadId = threadId
|
||||
self.authorId = authorId
|
||||
self.timestamp = timestamp
|
||||
self.text = text
|
||||
self.entities = entities
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4924,26 +4940,24 @@ func replayFinalState(
|
|||
})
|
||||
}
|
||||
case let .MergePeerPresences(statuses, explicit):
|
||||
var presences: [PeerId: PeerPresence] = [:]
|
||||
var presences: [PeerId: UpdatedApiPresence] = [:]
|
||||
for (peerId, status) in statuses {
|
||||
if peerId == accountPeerId {
|
||||
if explicit {
|
||||
switch status {
|
||||
case let .userStatusOnline(userStatusOnlineData):
|
||||
let timestamp = userStatusOnlineData.expires
|
||||
delayNotificatonsUntil = timestamp + 30
|
||||
case let .userStatusOffline(userStatusOfflineData):
|
||||
let timestamp = userStatusOfflineData.wasOnline
|
||||
delayNotificatonsUntil = timestamp
|
||||
default:
|
||||
break
|
||||
switch status.status {
|
||||
case let .userStatusOnline(userStatusOnlineData):
|
||||
let timestamp = userStatusOnlineData.expires
|
||||
delayNotificatonsUntil = timestamp + 30
|
||||
case let .userStatusOffline(userStatusOfflineData):
|
||||
let timestamp = userStatusOfflineData.wasOnline
|
||||
delayNotificatonsUntil = timestamp
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let presence = TelegramUserPresence(apiStatus: status)
|
||||
presences[peerId] = presence
|
||||
presences[peerId] = status
|
||||
}
|
||||
|
||||
}
|
||||
updatePeerPresencesClean(transaction: transaction, accountPeerId: accountPeerId, peerPresences: presences)
|
||||
case let .UpdateSecretChat(chat, _):
|
||||
|
|
@ -4966,7 +4980,7 @@ func replayFinalState(
|
|||
} else if chatPeerId.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
updatedSecretChatTypingActivities.insert(chatPeerId.peerId)
|
||||
}
|
||||
case let .AddPeerLiveTypingDraftUpdate(peerAndThreadId, id, timestamp, authorId, text, entities):
|
||||
case let .AddPeerLiveTypingDraftUpdate(peerAndThreadId, id, timestamp, authorId, content):
|
||||
if liveTypingDraftUpdates[peerAndThreadId] == nil {
|
||||
liveTypingDraftUpdates[peerAndThreadId] = []
|
||||
}
|
||||
|
|
@ -4975,8 +4989,7 @@ func replayFinalState(
|
|||
threadId: peerAndThreadId.threadId,
|
||||
authorId: authorId,
|
||||
timestamp: timestamp,
|
||||
text: text,
|
||||
entities: entities
|
||||
content: content
|
||||
)))
|
||||
if peerAndThreadId.threadId != nil {
|
||||
let allKey = PeerAndThreadId(peerId: peerAndThreadId.peerId, threadId: nil)
|
||||
|
|
@ -4988,8 +5001,7 @@ func replayFinalState(
|
|||
threadId: peerAndThreadId.threadId,
|
||||
authorId: authorId,
|
||||
timestamp: timestamp,
|
||||
text: text,
|
||||
entities: entities
|
||||
content: content
|
||||
)))
|
||||
}
|
||||
case let .UpdatePinnedItemIds(groupId, pinnedOperation):
|
||||
|
|
@ -6172,17 +6184,32 @@ func replayFinalState(
|
|||
timestamp = max(timestamp, index.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
let draftText: String
|
||||
let draftAttributes: [MessageAttribute]
|
||||
switch update.content {
|
||||
case let .plain(text, entities):
|
||||
draftText = text
|
||||
draftAttributes = [
|
||||
TypingDraftMessageAttribute(),
|
||||
TextEntitiesMessageAttribute(entities: entities)
|
||||
]
|
||||
case let .rich(richData):
|
||||
draftText = ""
|
||||
draftAttributes = [
|
||||
TypingDraftMessageAttribute(),
|
||||
richData
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
update.id,
|
||||
Namespaces.Message.Cloud,
|
||||
update.threadId,
|
||||
update.authorId,
|
||||
timestamp,
|
||||
update.text,
|
||||
[
|
||||
TypingDraftMessageAttribute(),
|
||||
TextEntitiesMessageAttribute(entities: update.entities)
|
||||
]
|
||||
draftText,
|
||||
draftAttributes
|
||||
)
|
||||
case .cancel:
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -260,12 +260,12 @@ public final class AccountStateManager {
|
|||
public var dismissBotWebViews: Signal<[Int64], NoError> {
|
||||
return self.dismissBotWebViewsPipe.signal()
|
||||
}
|
||||
|
||||
|
||||
private let joinChatWebViewDecisionsPipe = ValuePipe<[JoinChatWebViewDecision]>()
|
||||
public var joinChatWebViewDecisions: Signal<[JoinChatWebViewDecision], NoError> {
|
||||
return self.joinChatWebViewDecisionsPipe.signal()
|
||||
}
|
||||
|
||||
|
||||
private let externallyUpdatedPeerIdsPipe = ValuePipe<[PeerId]>()
|
||||
var externallyUpdatedPeerIds: Signal<[PeerId], NoError> {
|
||||
return self.externallyUpdatedPeerIdsPipe.signal()
|
||||
|
|
|
|||
|
|
@ -405,12 +405,12 @@ private func updateContactPresences(postbox: Postbox, network: Network, accountP
|
|||
}
|
||||
|> mapToSignal { statuses -> Signal<Never, NoError> in
|
||||
return postbox.transaction { transaction -> Void in
|
||||
var peerPresences: [PeerId: PeerPresence] = [:]
|
||||
var peerPresences: [PeerId: UpdatedApiPresence] = [:]
|
||||
for status in statuses {
|
||||
switch status {
|
||||
case let .contactStatus(contactStatusData):
|
||||
let (userId, status) = (contactStatusData.userId, contactStatusData.status)
|
||||
peerPresences[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))] = TelegramUserPresence(apiStatus: status)
|
||||
case let .contactStatus(contactStatusData):
|
||||
let (userId, status) = (contactStatusData.userId, contactStatusData.status)
|
||||
peerPresences[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))] = UpdatedApiPresence(status: status, isMin: false)
|
||||
}
|
||||
}
|
||||
updatePeerPresencesClean(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences)
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ private func synchronizeChatInputState(transaction: Transaction, postbox: Postbo
|
|||
flags |= 1 << 8
|
||||
}
|
||||
|
||||
return network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: inputState?.text ?? "", entities: apiEntitiesFromMessageTextEntities(inputState?.entities ?? [], associatedPeers: SimpleDictionary()), media: nil, effect: nil, suggestedPost: suggestedPost))
|
||||
return network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: inputState?.text ?? "", entities: apiEntitiesFromMessageTextEntities(inputState?.entities ?? [], associatedPeers: SimpleDictionary()), media: nil, effect: nil, suggestedPost: suggestedPost, richMessage: nil))
|
||||
|> delay(2.0, queue: Queue.concurrentDefaultQueue())
|
||||
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||
return .single(.boolFalse)
|
||||
|
|
|
|||
|
|
@ -117,46 +117,50 @@ public enum PeerInputActivity: Comparable {
|
|||
extension PeerInputActivity {
|
||||
init?(apiType: Api.SendMessageAction, peerId: PeerId?, timestamp: Int32) {
|
||||
switch apiType {
|
||||
case .sendMessageCancelAction, .sendMessageChooseContactAction, .sendMessageGeoLocationAction, .sendMessageRecordVideoAction:
|
||||
return nil
|
||||
case .sendMessageGamePlayAction:
|
||||
self = .playingGame
|
||||
case .sendMessageRecordAudioAction, .sendMessageUploadAudioAction:
|
||||
self = .recordingVoice
|
||||
case .sendMessageTypingAction:
|
||||
self = .typingText
|
||||
case let .sendMessageUploadDocumentAction(sendMessageUploadDocumentActionData):
|
||||
let progress = sendMessageUploadDocumentActionData.progress
|
||||
self = .uploadingFile(progress: progress)
|
||||
case let .sendMessageUploadPhotoAction(sendMessageUploadPhotoActionData):
|
||||
let progress = sendMessageUploadPhotoActionData.progress
|
||||
self = .uploadingPhoto(progress: progress)
|
||||
case let .sendMessageUploadVideoAction(sendMessageUploadVideoActionData):
|
||||
let progress = sendMessageUploadVideoActionData.progress
|
||||
self = .uploadingVideo(progress: progress)
|
||||
case .sendMessageRecordRoundAction:
|
||||
self = .recordingInstantVideo
|
||||
case let .sendMessageUploadRoundAction(sendMessageUploadRoundActionData):
|
||||
let progress = sendMessageUploadRoundActionData.progress
|
||||
self = .uploadingInstantVideo(progress: progress)
|
||||
case .speakingInGroupCallAction:
|
||||
self = .speakingInGroupCall(timestamp: timestamp)
|
||||
case .sendMessageChooseStickerAction:
|
||||
self = .choosingSticker
|
||||
case .sendMessageHistoryImportAction:
|
||||
return nil
|
||||
case let .sendMessageEmojiInteraction(sendMessageEmojiInteractionData):
|
||||
let (emoticon, messageId, interaction) = (sendMessageEmojiInteractionData.emoticon, sendMessageEmojiInteractionData.msgId, sendMessageEmojiInteractionData.interaction)
|
||||
if let peerId = peerId {
|
||||
self = .interactingWithEmoji(emoticon: emoticon, messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId), interaction: EmojiInteraction(apiDataJson: interaction))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case let .sendMessageEmojiInteractionSeen(sendMessageEmojiInteractionSeenData):
|
||||
let emoticon = sendMessageEmojiInteractionSeenData.emoticon
|
||||
self = .seeingEmojiInteraction(emoticon: emoticon)
|
||||
case .sendMessageTextDraftAction:
|
||||
case .sendMessageCancelAction, .sendMessageChooseContactAction, .sendMessageGeoLocationAction, .sendMessageRecordVideoAction:
|
||||
return nil
|
||||
case .sendMessageGamePlayAction:
|
||||
self = .playingGame
|
||||
case .sendMessageRecordAudioAction, .sendMessageUploadAudioAction:
|
||||
self = .recordingVoice
|
||||
case .sendMessageTypingAction:
|
||||
self = .typingText
|
||||
case let .sendMessageUploadDocumentAction(sendMessageUploadDocumentActionData):
|
||||
let progress = sendMessageUploadDocumentActionData.progress
|
||||
self = .uploadingFile(progress: progress)
|
||||
case let .sendMessageUploadPhotoAction(sendMessageUploadPhotoActionData):
|
||||
let progress = sendMessageUploadPhotoActionData.progress
|
||||
self = .uploadingPhoto(progress: progress)
|
||||
case let .sendMessageUploadVideoAction(sendMessageUploadVideoActionData):
|
||||
let progress = sendMessageUploadVideoActionData.progress
|
||||
self = .uploadingVideo(progress: progress)
|
||||
case .sendMessageRecordRoundAction:
|
||||
self = .recordingInstantVideo
|
||||
case let .sendMessageUploadRoundAction(sendMessageUploadRoundActionData):
|
||||
let progress = sendMessageUploadRoundActionData.progress
|
||||
self = .uploadingInstantVideo(progress: progress)
|
||||
case .speakingInGroupCallAction:
|
||||
self = .speakingInGroupCall(timestamp: timestamp)
|
||||
case .sendMessageChooseStickerAction:
|
||||
self = .choosingSticker
|
||||
case .sendMessageHistoryImportAction:
|
||||
return nil
|
||||
case let .sendMessageEmojiInteraction(sendMessageEmojiInteractionData):
|
||||
let (emoticon, messageId, interaction) = (sendMessageEmojiInteractionData.emoticon, sendMessageEmojiInteractionData.msgId, sendMessageEmojiInteractionData.interaction)
|
||||
if let peerId = peerId {
|
||||
self = .interactingWithEmoji(emoticon: emoticon, messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId), interaction: EmojiInteraction(apiDataJson: interaction))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case let .sendMessageEmojiInteractionSeen(sendMessageEmojiInteractionSeenData):
|
||||
let emoticon = sendMessageEmojiInteractionSeenData.emoticon
|
||||
self = .seeingEmojiInteraction(emoticon: emoticon)
|
||||
case .sendMessageTextDraftAction:
|
||||
return nil
|
||||
case .sendMessageRichMessageDraftAction:
|
||||
return nil
|
||||
case .inputSendMessageRichMessageDraftAction:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1720,6 +1720,7 @@ public final class PendingMessageManager {
|
|||
var uniqueId: Int64 = 0
|
||||
var forwardSourceInfoAttribute: ForwardSourceInfoAttribute?
|
||||
var messageEntities: [Api.MessageEntity]?
|
||||
var apiRichMessage: Api.InputRichMessage?
|
||||
var replyMessageId: Int32?
|
||||
var replyPeerId: PeerId?
|
||||
var replyQuote: EngineMessageReplyQuote?
|
||||
|
|
@ -1803,6 +1804,9 @@ public final class PendingMessageManager {
|
|||
allowPaidStars = attribute.stars.value
|
||||
} else if let attribute = attribute as? SuggestedPostMessageAttribute {
|
||||
suggestedPost = attribute.apiSuggestedPost(fixMinTime: Int32(Date().timeIntervalSince1970 + 10))
|
||||
} else if let attribute = attribute as? RichTextMessageAttribute {
|
||||
apiRichMessage = attribute.apiInputRichMessage()
|
||||
flags |= Int32(1 << 23)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1928,7 +1932,7 @@ public final class PendingMessageManager {
|
|||
flags |= 1 << 22
|
||||
}
|
||||
|
||||
sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: message.text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, effect: messageEffectId, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), info: .acknowledgement, tag: dependencyTag)
|
||||
sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: message.text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, effect: messageEffectId, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost, richMessage: apiRichMessage), info: .acknowledgement, tag: dependencyTag)
|
||||
case let .media(inputMedia, text):
|
||||
if bubbleUpEmojiOrStickersets {
|
||||
flags |= Int32(1 << 15)
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ public class BoxedMessage: NSObject {
|
|||
|
||||
public class Serialization: NSObject, MTSerialization {
|
||||
public func currentLayer() -> UInt {
|
||||
return 226
|
||||
return 227
|
||||
}
|
||||
|
||||
public func parseMessage(_ data: Data!) -> Any! {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class UpdateMessageService: NSObject, MTMessageService {
|
|||
}
|
||||
case let .updateShortChatMessage(updateShortChatMessageData):
|
||||
let (flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, ttlPeriod) = (updateShortChatMessageData.flags, updateShortChatMessageData.id, updateShortChatMessageData.fromId, updateShortChatMessageData.chatId, updateShortChatMessageData.message, updateShortChatMessageData.pts, updateShortChatMessageData.ptsCount, updateShortChatMessageData.date, updateShortChatMessageData.fwdFrom, updateShortChatMessageData.viaBotId, updateShortChatMessageData.replyTo, updateShortChatMessageData.entities, updateShortChatMessageData.ttlPeriod)
|
||||
let generatedMessage = Api.Message.message(.init(flags: flags, flags2: 0, id: id, fromId: .peerUser(.init(userId: fromId)), fromBoostsApplied: nil, fromRank: nil, peerId: Api.Peer.peerChat(.init(chatId: chatId)), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, guestchatViaFrom: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil, summaryFromLanguage: nil))
|
||||
let generatedMessage = Api.Message.message(.init(flags: flags, flags2: 0, id: id, fromId: .peerUser(.init(userId: fromId)), fromBoostsApplied: nil, fromRank: nil, peerId: Api.Peer.peerChat(.init(chatId: chatId)), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, guestchatViaFrom: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil, summaryFromLanguage: nil, richMessage: nil))
|
||||
let update = Api.Update.updateNewMessage(.init(message: generatedMessage, pts: pts, ptsCount: ptsCount))
|
||||
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
|
||||
if groups.count != 0 {
|
||||
|
|
@ -79,7 +79,7 @@ class UpdateMessageService: NSObject, MTMessageService {
|
|||
|
||||
let generatedPeerId = Api.Peer.peerUser(.init(userId: userId))
|
||||
|
||||
let generatedMessage = Api.Message.message(.init(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, fromRank: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, guestchatViaFrom: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil, summaryFromLanguage: nil))
|
||||
let generatedMessage = Api.Message.message(.init(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, fromRank: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, guestchatViaFrom: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil, summaryFromLanguage: nil, richMessage: nil))
|
||||
let update = Api.Update.updateNewMessage(.init(message: generatedMessage, pts: pts, ptsCount: ptsCount))
|
||||
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
|
||||
if groups.count != 0 {
|
||||
|
|
|
|||
|
|
@ -405,12 +405,12 @@ public final class CachedChannelData: CachedPeerData {
|
|||
self.sendPaidMessageStars = sendPaidMessageStars
|
||||
self.mainProfileTab = mainProfileTab
|
||||
self.guardBotId = guardBotId
|
||||
|
||||
|
||||
var peerIds = Set<PeerId>()
|
||||
for botInfo in botInfos {
|
||||
peerIds.insert(botInfo.peerId)
|
||||
}
|
||||
|
||||
|
||||
if case let .known(linkedDiscussionPeerIdValue) = linkedDiscussionPeerId {
|
||||
if let linkedDiscussionPeerIdValue {
|
||||
peerIds.insert(linkedDiscussionPeerIdValue)
|
||||
|
|
@ -590,7 +590,7 @@ public final class CachedChannelData: CachedPeerData {
|
|||
public func withUpdatedGuardBotId(_ guardBotId: EnginePeer.Id?) -> CachedChannelData {
|
||||
return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, chatTheme: self.chatTheme, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, reactionSettings: self.reactionSettings, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack, verification: self.verification, starGiftsCount: self.starGiftsCount, sendPaidMessageStars: self.sendPaidMessageStars, mainProfileTab: self.mainProfileTab, guardBotId: guardBotId)
|
||||
}
|
||||
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.isNotAccessible = decoder.decodeInt32ForKey("isNotAccessible", orElse: 0) != 0
|
||||
self.flags = CachedChannelFlags(rawValue: decoder.decodeInt32ForKey("f", orElse: 0))
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ private func decodeListItems(_ decoder: PostboxDecoder) -> [InstantPageListItem]
|
|||
if !legacyItems.isEmpty {
|
||||
var items: [InstantPageListItem] = []
|
||||
for item in legacyItems {
|
||||
items.append(.text(item, nil))
|
||||
items.append(.text(item, nil, nil))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
|
@ -1021,15 +1021,15 @@ private enum InstantPageListItemType: Int32 {
|
|||
|
||||
public indirect enum InstantPageListItem: PostboxCoding, Equatable {
|
||||
case unknown
|
||||
case text(RichText, String?)
|
||||
case blocks([InstantPageBlock], String?)
|
||||
case text(RichText, String?, Bool?)
|
||||
case blocks([InstantPageBlock], String?, Bool?)
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
switch decoder.decodeInt32ForKey("r", orElse: 0) {
|
||||
case InstantPageListItemType.text.rawValue:
|
||||
self = .text(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, decoder.decodeOptionalStringForKey("n"))
|
||||
self = .text(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, decoder.decodeOptionalStringForKey("n"), InstantPageListItem.checkedFromTriState(decoder.decodeInt32ForKey("ck", orElse: 0)))
|
||||
case InstantPageListItemType.blocks.rawValue:
|
||||
self = .blocks(decoder.decodeObjectArrayWithDecoderForKey("b"), decoder.decodeOptionalStringForKey("n"))
|
||||
self = .blocks(decoder.decodeObjectArrayWithDecoderForKey("b"), decoder.decodeOptionalStringForKey("n"), InstantPageListItem.checkedFromTriState(decoder.decodeInt32ForKey("ck", orElse: 0)))
|
||||
default:
|
||||
self = .unknown
|
||||
}
|
||||
|
|
@ -1037,7 +1037,7 @@ public indirect enum InstantPageListItem: PostboxCoding, Equatable {
|
|||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
switch self {
|
||||
case let .text(text, num):
|
||||
case let .text(text, num, checked):
|
||||
encoder.encodeInt32(InstantPageListItemType.text.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
if let num = num {
|
||||
|
|
@ -1045,7 +1045,12 @@ public indirect enum InstantPageListItem: PostboxCoding, Equatable {
|
|||
} else {
|
||||
encoder.encodeNil(forKey: "n")
|
||||
}
|
||||
case let .blocks(blocks, num):
|
||||
if let triState = InstantPageListItem.triState(fromChecked: checked) {
|
||||
encoder.encodeInt32(triState, forKey: "ck")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "ck")
|
||||
}
|
||||
case let .blocks(blocks, num, checked):
|
||||
encoder.encodeInt32(InstantPageListItemType.blocks.rawValue, forKey: "r")
|
||||
encoder.encodeObjectArray(blocks, forKey: "b")
|
||||
if let num = num {
|
||||
|
|
@ -1053,11 +1058,33 @@ public indirect enum InstantPageListItem: PostboxCoding, Equatable {
|
|||
} else {
|
||||
encoder.encodeNil(forKey: "n")
|
||||
}
|
||||
if let triState = InstantPageListItem.triState(fromChecked: checked) {
|
||||
encoder.encodeInt32(triState, forKey: "ck")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "ck")
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
static func checkedFromTriState(_ value: Int32) -> Bool? {
|
||||
switch value {
|
||||
case 1: return false
|
||||
case 2: return true
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the persisted tri-state (1 = unchecked, 2 = checked) or nil when not a checkbox item.
|
||||
static func triState(fromChecked checked: Bool?) -> Int32? {
|
||||
switch checked {
|
||||
case .some(false): return 1
|
||||
case .some(true): return 2
|
||||
case .none: return nil
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: InstantPageListItem, rhs: InstantPageListItem) -> Bool {
|
||||
switch lhs {
|
||||
case .unknown:
|
||||
|
|
@ -1066,14 +1093,14 @@ public indirect enum InstantPageListItem: PostboxCoding, Equatable {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .text(lhsText, lhsNum):
|
||||
if case let .text(rhsText, rhsNum) = rhs, lhsText == rhsText, lhsNum == rhsNum {
|
||||
case let .text(lhsText, lhsNum, lhsChecked):
|
||||
if case let .text(rhsText, rhsNum, rhsChecked) = rhs, lhsText == rhsText, lhsNum == rhsNum, lhsChecked == rhsChecked {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .blocks(lhsBlocks, lhsNum):
|
||||
if case let .blocks(rhsBlocks, rhsNum) = rhs, lhsBlocks == rhsBlocks, lhsNum == rhsNum {
|
||||
case let .blocks(lhsBlocks, lhsNum, lhsChecked):
|
||||
if case let .blocks(rhsBlocks, rhsNum, rhsChecked) = rhs, lhsBlocks == rhsBlocks, lhsNum == rhsNum, lhsChecked == rhsChecked {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
|
@ -1087,7 +1114,7 @@ public indirect enum InstantPageListItem: PostboxCoding, Equatable {
|
|||
guard let textValue = flatBuffersObject.value(type: TelegramCore_InstantPageListItem_Text.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .text(try RichText(flatBuffersObject: textValue.text), textValue.number)
|
||||
self = .text(try RichText(flatBuffersObject: textValue.text), textValue.number, InstantPageListItem.checkedFromTriState(textValue.checkState))
|
||||
|
||||
case .instantpagelistitemBlocks:
|
||||
guard let blocksValue = flatBuffersObject.value(type: TelegramCore_InstantPageListItem_Blocks.self) else {
|
||||
|
|
@ -1096,7 +1123,7 @@ public indirect enum InstantPageListItem: PostboxCoding, Equatable {
|
|||
let blocks = try (0 ..< blocksValue.blocksCount).map { i in
|
||||
return try InstantPageBlock(flatBuffersObject: blocksValue.blocks(at: i)!)
|
||||
}
|
||||
self = .blocks(blocks, blocksValue.number)
|
||||
self = .blocks(blocks, blocksValue.number, InstantPageListItem.checkedFromTriState(blocksValue.checkState))
|
||||
case .instantpagelistitemUnknown:
|
||||
self = .unknown
|
||||
case .none_:
|
||||
|
|
@ -1109,28 +1136,34 @@ public indirect enum InstantPageListItem: PostboxCoding, Equatable {
|
|||
let offset: Offset
|
||||
|
||||
switch self {
|
||||
case let .text(text, number):
|
||||
case let .text(text, number, checked):
|
||||
valueType = .instantpagelistitemText
|
||||
let textOffset = text.encodeToFlatBuffers(builder: &builder)
|
||||
let numberOffset = number.map { builder.create(string: $0) } ?? Offset()
|
||||
|
||||
|
||||
let start = TelegramCore_InstantPageListItem_Text.startInstantPageListItem_Text(&builder)
|
||||
TelegramCore_InstantPageListItem_Text.add(text: textOffset, &builder)
|
||||
if let _ = number {
|
||||
TelegramCore_InstantPageListItem_Text.add(number: numberOffset, &builder)
|
||||
}
|
||||
if let triState = InstantPageListItem.triState(fromChecked: checked) {
|
||||
TelegramCore_InstantPageListItem_Text.add(checkState: triState, &builder)
|
||||
}
|
||||
offset = TelegramCore_InstantPageListItem_Text.endInstantPageListItem_Text(&builder, start: start)
|
||||
case let .blocks(blocks, number):
|
||||
case let .blocks(blocks, number, checked):
|
||||
valueType = .instantpagelistitemBlocks
|
||||
let blocksOffsets = blocks.map { $0.encodeToFlatBuffers(builder: &builder) }
|
||||
let blocksOffset = builder.createVector(ofOffsets: blocksOffsets, len: blocksOffsets.count)
|
||||
let numberOffset = number.map { builder.create(string: $0) } ?? Offset()
|
||||
|
||||
|
||||
let start = TelegramCore_InstantPageListItem_Blocks.startInstantPageListItem_Blocks(&builder)
|
||||
TelegramCore_InstantPageListItem_Blocks.addVectorOf(blocks: blocksOffset, &builder)
|
||||
if let _ = number {
|
||||
TelegramCore_InstantPageListItem_Blocks.add(number: numberOffset, &builder)
|
||||
}
|
||||
if let triState = InstantPageListItem.triState(fromChecked: checked) {
|
||||
TelegramCore_InstantPageListItem_Blocks.add(checkState: triState, &builder)
|
||||
}
|
||||
offset = TelegramCore_InstantPageListItem_Blocks.endInstantPageListItem_Blocks(&builder, start: start)
|
||||
case .unknown:
|
||||
valueType = .instantpagelistitemUnknown
|
||||
|
|
|
|||
|
|
@ -20,6 +20,17 @@ private enum RichTextTypes: Int32 {
|
|||
case image = 14
|
||||
case anchor = 15
|
||||
case formula = 16
|
||||
case textCustomEmoji = 17
|
||||
case textAutoEmail = 18
|
||||
case textAutoPhone = 19
|
||||
case textAutoUrl = 20
|
||||
case textBankCard = 21
|
||||
case textBotCommand = 22
|
||||
case textCashtag = 23
|
||||
case textHashtag = 24
|
||||
case textMention = 25
|
||||
case textMentionName = 26
|
||||
case textSpoiler = 27
|
||||
}
|
||||
|
||||
public indirect enum RichText: PostboxCoding, Equatable {
|
||||
|
|
@ -40,7 +51,18 @@ public indirect enum RichText: PostboxCoding, Equatable {
|
|||
case image(id: MediaId, dimensions: PixelDimensions)
|
||||
case anchor(text: RichText, name: String)
|
||||
case formula(latex: String)
|
||||
|
||||
case textCustomEmoji(fileId: Int64, alt: String)
|
||||
case textAutoEmail(text: RichText)
|
||||
case textAutoPhone(text: RichText)
|
||||
case textAutoUrl(text: RichText)
|
||||
case textBankCard(text: RichText)
|
||||
case textBotCommand(text: RichText)
|
||||
case textCashtag(text: RichText)
|
||||
case textHashtag(text: RichText)
|
||||
case textMention(text: RichText)
|
||||
case textMentionName(text: RichText, peerId: Int64)
|
||||
case textSpoiler(text: RichText)
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
switch decoder.decodeInt32ForKey("r", orElse: 0) {
|
||||
case RichTextTypes.empty.rawValue:
|
||||
|
|
@ -83,6 +105,28 @@ public indirect enum RichText: PostboxCoding, Equatable {
|
|||
self = .anchor(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, name: decoder.decodeStringForKey("n", orElse: ""))
|
||||
case RichTextTypes.formula.rawValue:
|
||||
self = .formula(latex: decoder.decodeStringForKey("l", orElse: ""))
|
||||
case RichTextTypes.textCustomEmoji.rawValue:
|
||||
self = .textCustomEmoji(fileId: decoder.decodeInt64ForKey("ce.f", orElse: 0), alt: decoder.decodeStringForKey("ce.a", orElse: ""))
|
||||
case RichTextTypes.textAutoEmail.rawValue:
|
||||
self = .textAutoEmail(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
|
||||
case RichTextTypes.textAutoPhone.rawValue:
|
||||
self = .textAutoPhone(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
|
||||
case RichTextTypes.textAutoUrl.rawValue:
|
||||
self = .textAutoUrl(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
|
||||
case RichTextTypes.textBankCard.rawValue:
|
||||
self = .textBankCard(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
|
||||
case RichTextTypes.textBotCommand.rawValue:
|
||||
self = .textBotCommand(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
|
||||
case RichTextTypes.textCashtag.rawValue:
|
||||
self = .textCashtag(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
|
||||
case RichTextTypes.textHashtag.rawValue:
|
||||
self = .textHashtag(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
|
||||
case RichTextTypes.textMention.rawValue:
|
||||
self = .textMention(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
|
||||
case RichTextTypes.textMentionName.rawValue:
|
||||
self = .textMentionName(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, peerId: decoder.decodeInt64ForKey("mn.p", orElse: 0))
|
||||
case RichTextTypes.textSpoiler.rawValue:
|
||||
self = .textSpoiler(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
|
||||
default:
|
||||
self = .empty
|
||||
}
|
||||
|
|
@ -154,6 +198,41 @@ public indirect enum RichText: PostboxCoding, Equatable {
|
|||
case let .formula(latex):
|
||||
encoder.encodeInt32(RichTextTypes.formula.rawValue, forKey: "r")
|
||||
encoder.encodeString(latex, forKey: "l")
|
||||
case let .textCustomEmoji(fileId, alt):
|
||||
encoder.encodeInt32(RichTextTypes.textCustomEmoji.rawValue, forKey: "r")
|
||||
encoder.encodeInt64(fileId, forKey: "ce.f")
|
||||
encoder.encodeString(alt, forKey: "ce.a")
|
||||
case let .textAutoEmail(text):
|
||||
encoder.encodeInt32(RichTextTypes.textAutoEmail.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
case let .textAutoPhone(text):
|
||||
encoder.encodeInt32(RichTextTypes.textAutoPhone.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
case let .textAutoUrl(text):
|
||||
encoder.encodeInt32(RichTextTypes.textAutoUrl.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
case let .textBankCard(text):
|
||||
encoder.encodeInt32(RichTextTypes.textBankCard.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
case let .textBotCommand(text):
|
||||
encoder.encodeInt32(RichTextTypes.textBotCommand.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
case let .textCashtag(text):
|
||||
encoder.encodeInt32(RichTextTypes.textCashtag.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
case let .textHashtag(text):
|
||||
encoder.encodeInt32(RichTextTypes.textHashtag.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
case let .textMention(text):
|
||||
encoder.encodeInt32(RichTextTypes.textMention.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
case let .textMentionName(text, peerId):
|
||||
encoder.encodeInt32(RichTextTypes.textMentionName.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
encoder.encodeInt64(peerId, forKey: "mn.p")
|
||||
case let .textSpoiler(text):
|
||||
encoder.encodeInt32(RichTextTypes.textSpoiler.rawValue, forKey: "r")
|
||||
encoder.encodeObject(text, forKey: "t")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -261,6 +340,32 @@ public indirect enum RichText: PostboxCoding, Equatable {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .textCustomEmoji(lhsFileId, lhsAlt):
|
||||
if case let .textCustomEmoji(rhsFileId, rhsAlt) = rhs, lhsFileId == rhsFileId, lhsAlt == rhsAlt {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .textAutoEmail(text):
|
||||
if case .textAutoEmail(text) = rhs { return true } else { return false }
|
||||
case let .textAutoPhone(text):
|
||||
if case .textAutoPhone(text) = rhs { return true } else { return false }
|
||||
case let .textAutoUrl(text):
|
||||
if case .textAutoUrl(text) = rhs { return true } else { return false }
|
||||
case let .textBankCard(text):
|
||||
if case .textBankCard(text) = rhs { return true } else { return false }
|
||||
case let .textBotCommand(text):
|
||||
if case .textBotCommand(text) = rhs { return true } else { return false }
|
||||
case let .textCashtag(text):
|
||||
if case .textCashtag(text) = rhs { return true } else { return false }
|
||||
case let .textHashtag(text):
|
||||
if case .textHashtag(text) = rhs { return true } else { return false }
|
||||
case let .textMention(text):
|
||||
if case .textMention(text) = rhs { return true } else { return false }
|
||||
case let .textMentionName(lhsText, lhsPeerId):
|
||||
if case let .textMentionName(rhsText, rhsPeerId) = rhs, lhsText == rhsText, lhsPeerId == rhsPeerId { return true } else { return false }
|
||||
case let .textSpoiler(text):
|
||||
if case .textSpoiler(text) = rhs { return true } else { return false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -306,6 +411,28 @@ public extension RichText {
|
|||
return text.plainText
|
||||
case let .formula(latex):
|
||||
return latex
|
||||
case let .textCustomEmoji(_, alt):
|
||||
return alt
|
||||
case let .textAutoEmail(text):
|
||||
return text.plainText
|
||||
case let .textAutoPhone(text):
|
||||
return text.plainText
|
||||
case let .textAutoUrl(text):
|
||||
return text.plainText
|
||||
case let .textBankCard(text):
|
||||
return text.plainText
|
||||
case let .textBotCommand(text):
|
||||
return text.plainText
|
||||
case let .textCashtag(text):
|
||||
return text.plainText
|
||||
case let .textHashtag(text):
|
||||
return text.plainText
|
||||
case let .textMention(text):
|
||||
return text.plainText
|
||||
case let .textMentionName(text, _):
|
||||
return text.plainText
|
||||
case let .textSpoiler(text):
|
||||
return text.plainText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -398,6 +525,61 @@ extension RichText {
|
|||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .formula(latex: value.latex)
|
||||
case .richtextCustomemoji:
|
||||
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_CustomEmoji.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .textCustomEmoji(fileId: value.fileId, alt: value.alt)
|
||||
case .richtextAutoemail:
|
||||
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_AutoEmail.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .textAutoEmail(text: try RichText(flatBuffersObject: value.text))
|
||||
case .richtextAutophone:
|
||||
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_AutoPhone.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .textAutoPhone(text: try RichText(flatBuffersObject: value.text))
|
||||
case .richtextAutourl:
|
||||
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_AutoUrl.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .textAutoUrl(text: try RichText(flatBuffersObject: value.text))
|
||||
case .richtextBankcard:
|
||||
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_BankCard.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .textBankCard(text: try RichText(flatBuffersObject: value.text))
|
||||
case .richtextBotcommand:
|
||||
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_BotCommand.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .textBotCommand(text: try RichText(flatBuffersObject: value.text))
|
||||
case .richtextCashtag:
|
||||
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Cashtag.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .textCashtag(text: try RichText(flatBuffersObject: value.text))
|
||||
case .richtextHashtag:
|
||||
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Hashtag.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .textHashtag(text: try RichText(flatBuffersObject: value.text))
|
||||
case .richtextMention:
|
||||
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Mention.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .textMention(text: try RichText(flatBuffersObject: value.text))
|
||||
case .richtextMentionname:
|
||||
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_MentionName.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .textMentionName(text: try RichText(flatBuffersObject: value.text), peerId: value.peerId)
|
||||
case .richtextSpoiler:
|
||||
guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Spoiler.self) else {
|
||||
throw FlatBuffersError.missingRequiredField()
|
||||
}
|
||||
self = .textSpoiler(text: try RichText(flatBuffersObject: value.text))
|
||||
case .none_:
|
||||
self = .empty
|
||||
}
|
||||
|
|
@ -520,8 +702,76 @@ extension RichText {
|
|||
let start = TelegramCore_RichText_Formula.startRichText_Formula(&builder)
|
||||
TelegramCore_RichText_Formula.add(latex: latexOffset, &builder)
|
||||
offset = TelegramCore_RichText_Formula.endRichText_Formula(&builder, start: start)
|
||||
case let .textCustomEmoji(fileId, alt):
|
||||
valueType = .richtextCustomemoji
|
||||
let altOffset = builder.create(string: alt)
|
||||
let start = TelegramCore_RichText_CustomEmoji.startRichText_CustomEmoji(&builder)
|
||||
TelegramCore_RichText_CustomEmoji.add(fileId: fileId, &builder)
|
||||
TelegramCore_RichText_CustomEmoji.add(alt: altOffset, &builder)
|
||||
offset = TelegramCore_RichText_CustomEmoji.endRichText_CustomEmoji(&builder, start: start)
|
||||
case let .textAutoEmail(text):
|
||||
valueType = .richtextAutoemail
|
||||
let textOffset = text.encodeToFlatBuffers(builder: &builder)
|
||||
let start = TelegramCore_RichText_AutoEmail.startRichText_AutoEmail(&builder)
|
||||
TelegramCore_RichText_AutoEmail.add(text: textOffset, &builder)
|
||||
offset = TelegramCore_RichText_AutoEmail.endRichText_AutoEmail(&builder, start: start)
|
||||
case let .textAutoPhone(text):
|
||||
valueType = .richtextAutophone
|
||||
let textOffset = text.encodeToFlatBuffers(builder: &builder)
|
||||
let start = TelegramCore_RichText_AutoPhone.startRichText_AutoPhone(&builder)
|
||||
TelegramCore_RichText_AutoPhone.add(text: textOffset, &builder)
|
||||
offset = TelegramCore_RichText_AutoPhone.endRichText_AutoPhone(&builder, start: start)
|
||||
case let .textAutoUrl(text):
|
||||
valueType = .richtextAutourl
|
||||
let textOffset = text.encodeToFlatBuffers(builder: &builder)
|
||||
let start = TelegramCore_RichText_AutoUrl.startRichText_AutoUrl(&builder)
|
||||
TelegramCore_RichText_AutoUrl.add(text: textOffset, &builder)
|
||||
offset = TelegramCore_RichText_AutoUrl.endRichText_AutoUrl(&builder, start: start)
|
||||
case let .textBankCard(text):
|
||||
valueType = .richtextBankcard
|
||||
let textOffset = text.encodeToFlatBuffers(builder: &builder)
|
||||
let start = TelegramCore_RichText_BankCard.startRichText_BankCard(&builder)
|
||||
TelegramCore_RichText_BankCard.add(text: textOffset, &builder)
|
||||
offset = TelegramCore_RichText_BankCard.endRichText_BankCard(&builder, start: start)
|
||||
case let .textBotCommand(text):
|
||||
valueType = .richtextBotcommand
|
||||
let textOffset = text.encodeToFlatBuffers(builder: &builder)
|
||||
let start = TelegramCore_RichText_BotCommand.startRichText_BotCommand(&builder)
|
||||
TelegramCore_RichText_BotCommand.add(text: textOffset, &builder)
|
||||
offset = TelegramCore_RichText_BotCommand.endRichText_BotCommand(&builder, start: start)
|
||||
case let .textCashtag(text):
|
||||
valueType = .richtextCashtag
|
||||
let textOffset = text.encodeToFlatBuffers(builder: &builder)
|
||||
let start = TelegramCore_RichText_Cashtag.startRichText_Cashtag(&builder)
|
||||
TelegramCore_RichText_Cashtag.add(text: textOffset, &builder)
|
||||
offset = TelegramCore_RichText_Cashtag.endRichText_Cashtag(&builder, start: start)
|
||||
case let .textHashtag(text):
|
||||
valueType = .richtextHashtag
|
||||
let textOffset = text.encodeToFlatBuffers(builder: &builder)
|
||||
let start = TelegramCore_RichText_Hashtag.startRichText_Hashtag(&builder)
|
||||
TelegramCore_RichText_Hashtag.add(text: textOffset, &builder)
|
||||
offset = TelegramCore_RichText_Hashtag.endRichText_Hashtag(&builder, start: start)
|
||||
case let .textMention(text):
|
||||
valueType = .richtextMention
|
||||
let textOffset = text.encodeToFlatBuffers(builder: &builder)
|
||||
let start = TelegramCore_RichText_Mention.startRichText_Mention(&builder)
|
||||
TelegramCore_RichText_Mention.add(text: textOffset, &builder)
|
||||
offset = TelegramCore_RichText_Mention.endRichText_Mention(&builder, start: start)
|
||||
case let .textMentionName(text, peerId):
|
||||
valueType = .richtextMentionname
|
||||
let textOffset = text.encodeToFlatBuffers(builder: &builder)
|
||||
let start = TelegramCore_RichText_MentionName.startRichText_MentionName(&builder)
|
||||
TelegramCore_RichText_MentionName.add(text: textOffset, &builder)
|
||||
TelegramCore_RichText_MentionName.add(peerId: peerId, &builder)
|
||||
offset = TelegramCore_RichText_MentionName.endRichText_MentionName(&builder, start: start)
|
||||
case let .textSpoiler(text):
|
||||
valueType = .richtextSpoiler
|
||||
let textOffset = text.encodeToFlatBuffers(builder: &builder)
|
||||
let start = TelegramCore_RichText_Spoiler.startRichText_Spoiler(&builder)
|
||||
TelegramCore_RichText_Spoiler.add(text: textOffset, &builder)
|
||||
offset = TelegramCore_RichText_Spoiler.endRichText_Spoiler(&builder, start: start)
|
||||
}
|
||||
|
||||
|
||||
return TelegramCore_RichText.createRichText(&builder, valueType: valueType, valueOffset: offset)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import Foundation
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
|
||||
public class RichTextMessageAttribute: MessageAttribute, Equatable {
|
||||
public let instantPage: InstantPage
|
||||
|
||||
public var associatedPeerIds: [PeerId] {
|
||||
/*var result: [PeerId] = []
|
||||
for entity in entities {
|
||||
switch entity.type {
|
||||
case let .TextMention(peerId):
|
||||
result.append(peerId)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return result*/
|
||||
return []
|
||||
}
|
||||
|
||||
public var associatedMediaIds: [MediaId] {
|
||||
/*var result: [MediaId] = []
|
||||
for entity in self.entities {
|
||||
switch entity.type {
|
||||
case let .CustomEmoji(_, fileId):
|
||||
result.append(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
if result.isEmpty {
|
||||
return result
|
||||
} else {
|
||||
return Array(Set(result))
|
||||
}*/
|
||||
return []
|
||||
}
|
||||
|
||||
public init(instantPage: InstantPage) {
|
||||
self.instantPage = instantPage
|
||||
}
|
||||
|
||||
required public init(decoder: PostboxDecoder) {
|
||||
self.instantPage = decoder.decodeObjectForKey("instantPage", decoder: { InstantPage(decoder: $0) }) as! InstantPage
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObject(self.instantPage, forKey: "instantPage")
|
||||
}
|
||||
|
||||
public static func ==(lhs: RichTextMessageAttribute, rhs: RichTextMessageAttribute) -> Bool {
|
||||
return lhs.instantPage == rhs.instantPage
|
||||
}
|
||||
}
|
||||
|
||||
extension RichTextMessageAttribute {
|
||||
convenience init(apiRichMessage: Api.RichMessage) {
|
||||
switch apiRichMessage {
|
||||
case let .richMessage(richMessage):
|
||||
var media: [MediaId: Media] = [:]
|
||||
for photo in richMessage.photos {
|
||||
if let image = telegramMediaImageFromApiPhoto(photo), let id = image.id {
|
||||
media[id] = image
|
||||
}
|
||||
}
|
||||
for file in richMessage.documents {
|
||||
if let file = telegramMediaFileFromApiDocument(file, altDocuments: []), let id = file.id {
|
||||
media[id] = file
|
||||
}
|
||||
}
|
||||
let isRtl = (richMessage.flags & (1 << 0)) != 0
|
||||
let isPartial = (richMessage.flags & (1 << 1)) != 0
|
||||
let instantPage = InstantPage(blocks: richMessage.blocks.map({ InstantPageBlock(apiBlock: $0) }), media: media, isComplete: !isPartial, rtl: isRtl, url: "", views: nil)
|
||||
self.init(instantPage: instantPage)
|
||||
}
|
||||
}
|
||||
|
||||
func apiInputRichMessage() -> Api.InputRichMessage {
|
||||
var flags: Int32 = 0
|
||||
if self.instantPage.rtl {
|
||||
flags |= (1 << 0)
|
||||
}
|
||||
|
||||
return Api.InputRichMessage.inputRichMessage(Api.InputRichMessage.Cons_inputRichMessage(
|
||||
flags: flags,
|
||||
blocks: self.instantPage.blocks.compactMap { $0.apiInputBlock() },
|
||||
photos: nil,
|
||||
documents: nil,
|
||||
users: nil
|
||||
))
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue