mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
InstantPage list checkboxes (task-list style): parse, serialize, render
Add a first-class `checked: Bool?` to InstantPageListItem (orthogonal to the ordered-list `num`), replacing the prior sentinel-string-in-num prototype. - Data model: enum third associated value on .text/.blocks; Postbox + a new FlatBuffers `checkState` tri-state field (0=nil,1=unchecked,2=checked, backward-compatible); Equatable. - Transmission: read/write the native API `checkbox`=flags.0 / `checked`=flags.1 bits on all four PageListItem / PageListOrderedItem constructors, so checkbox state survives the server round-trip for sender and recipients. - Markdown: forward parser routes detected `[ ]`/`[x]` into `checked` (keeping the real number for ordered items); reverse converter emits `- [ ]`/`- [x]` so editing a rich message round-trips. - Display: V1 + V2 layout detect via `item.checked`; the V2 renderer draws real CheckNode artwork (was a placeholder); preview text shows a checkbox glyph instead of leaking the old sentinel. Spec: docs/superpowers/specs/2026-05-27-instantpage-list-checkbox-design.md Plan: docs/superpowers/plans/2026-05-27-instantpage-list-checkbox.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ebe37cc714
commit
81e3be1923
12 changed files with 198 additions and 140 deletions
|
|
@ -629,7 +629,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
collect(blocks: blocks)
|
||||
case let .list(items, _):
|
||||
for item in items {
|
||||
if case let .blocks(blocks, _) = item {
|
||||
if case let .blocks(blocks, _, _) = item {
|
||||
collect(blocks: blocks)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ private let markdownInlineHTMLInlineIntent = InlinePresentationIntent(rawValue:
|
|||
private let markdownDefaultBlockImageDimensions = PixelDimensions(width: 1200, height: 900)
|
||||
private let markdownDefaultInlineImageDimensions = PixelDimensions(width: 18, height: 18)
|
||||
private let markdownImageParsingEnabled = false
|
||||
private let markdownTaskListUncheckedNumber = "\u{001f}tg-md-task:unchecked"
|
||||
private let markdownTaskListCheckedNumber = "\u{001f}tg-md-task:checked"
|
||||
private let markdownRawHTMLTagRegex = try! NSRegularExpression(pattern: #"</?([A-Za-z][A-Za-z0-9:-]*)\b[^>]*?>"#)
|
||||
private let markdownFormulaPlaceholderRegex = try! NSRegularExpression(pattern: #"TGMDMATH\d+TGMD"#)
|
||||
private let markdownVoidHTMLTags: Set<String> = [
|
||||
|
|
@ -200,7 +198,7 @@ private final class MarkdownConversionBudget {
|
|||
guard self.registerBlockDepth(depth) else {
|
||||
return false
|
||||
}
|
||||
if case let .blocks(blocks, _) = listItem {
|
||||
if case let .blocks(blocks, _, _) = listItem {
|
||||
return self.validate(blocks: blocks, depth: depth + 1, count: &count)
|
||||
} else {
|
||||
return true
|
||||
|
|
@ -1447,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
|
||||
|
|
@ -2285,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -610,17 +610,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
|
||||
|
|
|
|||
|
|
@ -166,14 +166,24 @@ private func markdownList(items: [InstantPageListItem], ordered: Bool, indent: I
|
|||
var lines: [String] = []
|
||||
var index = 1
|
||||
for item in items {
|
||||
// The stored per-item marker string (`InstantPageListItem`'s `String?`) is
|
||||
// intentionally ignored: ordered markers are regenerated from the running
|
||||
// index (CommonMark renumbers anyway) and the unordered marker is fixed.
|
||||
let marker = ordered ? "\(index). " : "- "
|
||||
// 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, _):
|
||||
case let .text(text, _, _):
|
||||
lines.append("\(indentString)\(marker)\(markdownInline(from: text))")
|
||||
case let .blocks(blocks, _):
|
||||
case let .blocks(blocks, _, _):
|
||||
var remainder = blocks
|
||||
var markerLineText = ""
|
||||
if case let .paragraph(text)? = remainder.first {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -372,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
|
||||
|
|
@ -380,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
|
||||
}
|
||||
|
|
@ -389,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)
|
||||
|
|
@ -432,11 +419,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||
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, _):
|
||||
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
|
||||
|
|
@ -466,7 +453,7 @@ 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?
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import CheckNode
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
|
|
@ -1359,22 +1360,21 @@ final class InstantPageV2ListMarkerView: UIView, InstantPageItemView {
|
|||
label.textAlignment = .right
|
||||
label.frame = CGRect(origin: .zero, size: item.frame.size)
|
||||
self.addSubview(label)
|
||||
case let .checklist(checked):
|
||||
// V0 placeholder: simple square outline (unchecked) or filled square (checked).
|
||||
// The existing V1 InstantPageChecklistMarkerItem artwork can be ported later if needed.
|
||||
let outer = CALayer()
|
||||
outer.borderColor = item.color.cgColor
|
||||
outer.borderWidth = 1.0
|
||||
outer.cornerRadius = 3.0
|
||||
outer.frame = CGRect(origin: .zero, size: item.frame.size)
|
||||
self.layer.addSublayer(outer)
|
||||
if checked {
|
||||
let fill = CALayer()
|
||||
fill.backgroundColor = item.color.cgColor
|
||||
fill.cornerRadius = 3.0
|
||||
fill.frame = CGRect(origin: .zero, size: item.frame.size).insetBy(dx: 2.0, dy: 2.0)
|
||||
self.layer.addSublayer(fill)
|
||||
}
|
||||
case let .checklist(checked, colors):
|
||||
let checkNodeTheme = CheckNodeTheme(
|
||||
backgroundColor: colors.background,
|
||||
strokeColor: colors.stroke,
|
||||
borderColor: colors.border,
|
||||
overlayBorder: false,
|
||||
hasInset: false,
|
||||
hasShadow: false
|
||||
)
|
||||
let checkNode = CheckNode(theme: checkNodeTheme, content: .check(isRectangle: true))
|
||||
checkNode.displaysAsynchronously = false
|
||||
checkNode.isUserInteractionEnabled = false
|
||||
checkNode.frame = CGRect(origin: .zero, size: item.frame.size)
|
||||
checkNode.setSelected(checked, animated: false)
|
||||
self.addSubview(checkNode.view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,10 +136,22 @@ public struct InstantPageV2DividerItem {
|
|||
public let color: UIColor
|
||||
}
|
||||
|
||||
public struct InstantPageV2CheckboxColors {
|
||||
public let background: UIColor
|
||||
public let stroke: UIColor
|
||||
public let border: UIColor
|
||||
|
||||
public init(background: UIColor, stroke: UIColor, border: UIColor) {
|
||||
self.background = background
|
||||
self.stroke = stroke
|
||||
self.border = border
|
||||
}
|
||||
}
|
||||
|
||||
public enum InstantPageV2ListMarkerKind {
|
||||
case bullet
|
||||
case number(String)
|
||||
case checklist(checked: Bool)
|
||||
case checklist(checked: Bool, colors: InstantPageV2CheckboxColors)
|
||||
}
|
||||
|
||||
public struct InstantPageV2ListMarkerItem {
|
||||
|
|
@ -1994,22 +2006,6 @@ private func layoutBlockQuote(
|
|||
return result
|
||||
}
|
||||
|
||||
// MARK: - Task-list marker helpers (copied verbatim from V1 InstantPageLayout.swift:134–146)
|
||||
|
||||
private let instantPageTaskListUncheckedNumber = "\u{001f}tg-md-task:unchecked"
|
||||
private let instantPageTaskListCheckedNumber = "\u{001f}tg-md-task:checked"
|
||||
|
||||
private func instantPageTaskListMarkerState(_ number: String?) -> Bool? {
|
||||
switch number {
|
||||
case instantPageTaskListUncheckedNumber:
|
||||
return false
|
||||
case instantPageTaskListCheckedNumber:
|
||||
return true
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - List layout (ported from V1 InstantPageLayout.swift lines 365–516)
|
||||
|
||||
private func layoutList(
|
||||
|
|
@ -2026,7 +2022,7 @@ private func layoutList(
|
|||
|
||||
if ordered {
|
||||
for item in listItems {
|
||||
if instantPageTaskListMarkerState(item.num) != nil {
|
||||
if item.checked != nil {
|
||||
hasTaskMarkers = true
|
||||
} else if let num = item.num, !num.isEmpty {
|
||||
hasNums = true
|
||||
|
|
@ -2034,7 +2030,7 @@ private func layoutList(
|
|||
}
|
||||
} else {
|
||||
for item in listItems {
|
||||
if instantPageTaskListMarkerState(item.num) != nil {
|
||||
if item.checked != nil {
|
||||
hasTaskMarkers = true
|
||||
break
|
||||
}
|
||||
|
|
@ -2048,13 +2044,18 @@ private func layoutList(
|
|||
}
|
||||
let checklistMarkerSize = CGSize(width: 18.0, height: 18.0)
|
||||
|
||||
let checkboxColors = InstantPageV2CheckboxColors(
|
||||
background: context.theme.panelAccentColor,
|
||||
stroke: context.theme.pageBackgroundColor,
|
||||
border: context.theme.controlColor
|
||||
)
|
||||
var markerInfos: [MarkerInfo] = []
|
||||
for (i, item) in listItems.enumerated() {
|
||||
if let checked = instantPageTaskListMarkerState(item.num) {
|
||||
if let checked = item.checked {
|
||||
if ordered {
|
||||
maxIndexWidth = max(maxIndexWidth, checklistMarkerSize.width)
|
||||
}
|
||||
markerInfos.append(MarkerInfo(kind: .checklist(checked: checked), naturalWidth: checklistMarkerSize.width))
|
||||
markerInfos.append(MarkerInfo(kind: .checklist(checked: checked, colors: checkboxColors), naturalWidth: checklistMarkerSize.width))
|
||||
} else if ordered {
|
||||
let value: String
|
||||
if hasNums {
|
||||
|
|
@ -2109,12 +2110,12 @@ private func layoutList(
|
|||
|
||||
// Effective item: if a .blocks item is empty, treat as a single space.
|
||||
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, _):
|
||||
case let .text(text, _, _):
|
||||
// Layout text content.
|
||||
let styleStack = InstantPageTextStyleStack()
|
||||
setupStyleStack(styleStack, theme: context.theme, category: .paragraph, link: false)
|
||||
|
|
@ -2163,7 +2164,7 @@ private func layoutList(
|
|||
result.append(contentsOf: textLaidOutItems)
|
||||
contentHeight += textSize.height
|
||||
|
||||
case let .blocks(blocks, _):
|
||||
case let .blocks(blocks, _, _):
|
||||
// Nested block content (e.g. sub-list, paragraphs).
|
||||
var previousBlock: InstantPageBlock?
|
||||
let originY = contentHeight
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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,19 +71,19 @@ 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, _):
|
||||
return .pageListItemText(Api.PageListItem.Cons_pageListItemText(flags: 0, text: value.apiRichText()))
|
||||
case let .blocks(blocks, _):
|
||||
return .pageListItemBlocks(Api.PageListItem.Cons_pageListItemBlocks(flags: 0, blocks: blocks.compactMap { $0.apiInputBlock() }))
|
||||
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: ""))))
|
||||
}
|
||||
|
|
@ -62,25 +91,25 @@ extension InstantPageListItem {
|
|||
|
||||
func apiInputPageOrderedListItem() -> Api.InputPageListOrderedItem {
|
||||
switch self {
|
||||
case let .text(value, num):
|
||||
var flags: Int32 = 0
|
||||
|
||||
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):
|
||||
var flags: Int32 = 0
|
||||
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -57,13 +57,16 @@ extension InstantPageListItem {
|
|||
switch self {
|
||||
case .unknown:
|
||||
return ""
|
||||
case let .text(text, num):
|
||||
if let num, !num.isEmpty {
|
||||
return "\(num). \(text.previewText())"
|
||||
case let .text(text, num, checked):
|
||||
let body = text.previewText()
|
||||
if let checked {
|
||||
return "\(checked ? "☑︎" : "☐") \(body)"
|
||||
} else if let num, !num.isEmpty {
|
||||
return "\(num). \(body)"
|
||||
} else {
|
||||
return text.previewText()
|
||||
return body
|
||||
}
|
||||
case let .blocks(blocks, num):
|
||||
case let .blocks(blocks, num, checked):
|
||||
var blocksText = ""
|
||||
for block in blocks {
|
||||
if !blocksText.isEmpty {
|
||||
|
|
@ -71,7 +74,9 @@ extension InstantPageListItem {
|
|||
}
|
||||
blocksText.append(block.previewText())
|
||||
}
|
||||
if let num {
|
||||
if let checked {
|
||||
return "\(checked ? "☑︎" : "☐") \(blocksText)"
|
||||
} else if let num {
|
||||
return "\(num). \(blocksText)"
|
||||
} else {
|
||||
return blocksText
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue