Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2026-05-29 09:24:16 +02:00
commit e0f4655cc5
871 changed files with 275897 additions and 64193 deletions

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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

View 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")
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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):

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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",

View file

@ -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) {

View file

@ -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 {

View file

@ -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: {})

View file

@ -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)
}

View file

@ -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
}
}
}
}

View file

@ -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

View 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)
}
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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)
}
}
}

File diff suppressed because it is too large Load diff

View 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)
}
}

View 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)
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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)

View file

@ -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()

View file

@ -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() {

View file

@ -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)
}
}

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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>

View file

@ -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

View file

@ -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 {

View file

@ -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)
}

View file

@ -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:

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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))

View file

@ -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)
}

View file

@ -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: {},

View file

@ -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 {}

View file

@ -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);
}

View file

@ -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) {

View file

@ -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) })

View file

@ -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 {

View file

@ -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()))
}
}
}

View file

@ -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())

View file

@ -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:

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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()
}

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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
}
}
}

View file

@ -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)

View file

@ -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! {

View file

@ -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 {

View file

@ -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))

View file

@ -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

View file

@ -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)
}
}

View file

@ -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