mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
4cb80e456b
14 changed files with 1736 additions and 140 deletions
1317
docs/superpowers/plans/2026-05-27-instantpage-list-checkbox.md
Normal file
1317
docs/superpowers/plans/2026-05-27-instantpage-list-checkbox.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,221 @@
|
|||
# InstantPage list checkboxes (task-list style) — design
|
||||
|
||||
**Date:** 2026-05-27
|
||||
**Status:** Approved for planning
|
||||
|
||||
## Summary
|
||||
|
||||
`InstantPageListItem` gains first-class **task-list checkbox** support — the `- [ ]` / `- [x]`
|
||||
markdown construct — covering parsing, local serialization (Postbox + FlatBuffers),
|
||||
API transmission, display, and the edit round-trip in rich-data message bubbles.
|
||||
|
||||
A prior prototype already wired most of this through a **sentinel string stuffed into the
|
||||
ordered-list `num` field** (`"\u{001f}tg-md-task:checked"` / `:unchecked`). This design
|
||||
**replaces that sentinel with a first-class `checked: Bool?`** on the list item, orthogonal
|
||||
to `num`, because:
|
||||
|
||||
1. The Telegram API represents the checkbox as **flags independent of the list number** on
|
||||
all four list-item types (see "API facts" below). The sentinel-in-`num` cannot represent
|
||||
an item that is *both* numbered *and* a checkbox, which the API allows.
|
||||
2. The sentinel never survived the server round-trip — `apiInputBlock` hardcoded `flags: 0`
|
||||
and dropped it — so checkboxes silently reverted to bullets the moment a message was
|
||||
confirmed, even for the sender.
|
||||
|
||||
The first-class field fixes transmission (real API flags), makes the local model faithful to
|
||||
the API, and removes the sentinel hack from three files.
|
||||
|
||||
## API facts (verified against generated `Api.*` source, not inferred)
|
||||
|
||||
All four list-item constructors carry `checkbox` and `checked` as conditional-`true` flags;
|
||||
`checkbox` = bit 0, `checked` = bit 1. They are **independent** of the ordered-list number.
|
||||
|
||||
```
|
||||
pageListItemText#2f58683c flags:# checkbox:flags.0?true checked:flags.1?true text:RichText
|
||||
pageListItemBlocks#63ca67aa flags:# checkbox:flags.0?true checked:flags.1?true blocks:Vector<PageBlock>
|
||||
pageListOrderedItemText#cd3ea036 flags:# checkbox:flags.0?true checked:flags.1?true num:string text:RichText
|
||||
pageListOrderedItemBlocks#422931d4 flags:# checkbox:flags.0?true checked:flags.1?true num:string blocks:Vector<PageBlock>
|
||||
```
|
||||
|
||||
| iOS `Api.` type | Used for | checkbox | checked | other bits |
|
||||
|---|---|---|---|---|
|
||||
| `Api.PageListItem` (`.pageListItemText` / `.Blocks`) | send + receive (unordered) | flags.0 | flags.1 | — |
|
||||
| `Api.PageListOrderedItem` | receive (ordered) | flags.0 | flags.1 | `num` (unconditional string) |
|
||||
| `Api.InputPageListOrderedItem` | send (ordered) | flags.0 | flags.1 | value=flags.2, type=flags.3 |
|
||||
|
||||
The generated structs expose only `flags: Int32`; checkbox/checked are read/written by
|
||||
masking bits 0 and 1. The current conversion code ignores them.
|
||||
|
||||
## Data model
|
||||
|
||||
`InstantPageListItem` (`SyncCore_InstantPage.swift`) gains a third associated value:
|
||||
|
||||
```swift
|
||||
public indirect enum InstantPageListItem: PostboxCoding, Equatable {
|
||||
case unknown
|
||||
case text(RichText, String?, Bool?) // (text, num, checked)
|
||||
case blocks([InstantPageBlock], String?, Bool?) // (blocks, num, checked)
|
||||
}
|
||||
```
|
||||
|
||||
`checked` semantics (orthogonal to `num`):
|
||||
|
||||
- `nil` — not a checkbox item (ordinary bullet / numbered item)
|
||||
- `false` — checkbox, unchecked
|
||||
- `true` — checkbox, checked
|
||||
|
||||
A new accessor mirrors the existing `var num`:
|
||||
|
||||
```swift
|
||||
public extension InstantPageListItem {
|
||||
var checked: Bool? { … } // returns the third value for .text/.blocks, nil for .unknown
|
||||
}
|
||||
```
|
||||
|
||||
The `var num` accessor is unchanged.
|
||||
|
||||
### Why an associated value (not a new enum case)
|
||||
|
||||
A checkbox item is still a text/blocks item; the only new dimension is the checkbox state.
|
||||
An associated value keeps every existing `switch` at three cases (one extra bound variable
|
||||
each) rather than forcing a fourth case into every exhaustive switch. The change is
|
||||
source-breaking by design — the compiler enumerates every construction/destructure site, so
|
||||
a full build with `--continueOnError` is the completeness check.
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Enum + local serialization — `SyncCore_InstantPage.swift`
|
||||
|
||||
- Add the `Bool?` associated value to `.text` / `.blocks`; add the `checked` accessor.
|
||||
- **Postbox**: encode the checked tri-state under a new key (`"ck"`) using the **same
|
||||
`0=nil, 1=unchecked, 2=checked`** mapping as FlatBuffers (below); encode only when
|
||||
`checked != nil`, and decode with `decodeInt32ForKey("ck", orElse: 0)` → `0→nil, 1→false,
|
||||
2→true`. Old stored data lacks the key → `orElse: 0` → `nil` (backward compatible).
|
||||
- **FlatBuffers**: add `checkState:int32 (id: 2)` to both `InstantPageListItem_Text` and
|
||||
`InstantPageListItem_Blocks` in `InstantPageBlock.fbs` (tri-state: `0=nil, 1=unchecked,
|
||||
2=checked`). int32 defaults to 0, so absent → `nil` (backward compatible) — this mirrors
|
||||
the existing `alignment:int32` / `level:int32` pattern and sidesteps optional-scalar-bool
|
||||
ambiguity. Edit the `.fbs` (source of truth; the Bazel `flatc` genrule regenerates the
|
||||
Swift); update the hand-written codec in `SyncCore_InstantPage.swift` to read/write
|
||||
`checkState`. Encode only when non-nil (keeps the wire compact).
|
||||
- **Equatable**: compare the new value in the `.text` / `.blocks` arms.
|
||||
|
||||
### 2. API transmission — `ApiUtils/InstantPage.swift`
|
||||
|
||||
- **Receive** `init(apiListItem:)` (unordered) and `init(apiListOrderedItem:)` (ordered):
|
||||
read `flags & (1<<0)` (checkbox present) and `flags & (1<<1)` (checked). Set
|
||||
`checked = (flags & 1<<0) != 0 ? ((flags & 1<<1) != 0) : nil`. Keep `num` as today
|
||||
(nil for unordered, the API `num` string for ordered).
|
||||
- **Send** `apiInputPageListItem()` (→ `Api.PageListItem`) and `apiInputPageOrderedListItem()`
|
||||
(→ `Api.InputPageListOrderedItem`): when `checked != nil`, set `flags |= (1<<0)` and add
|
||||
`(1<<1)` when `checked == true`. Preserve the existing `value`/`type` bit handling on the
|
||||
ordered input.
|
||||
- The `var num` accessor is unaffected; add `checked` reads where the items are built.
|
||||
- **Bonus:** incoming Telegra.ph / cross-client task lists now render as checkboxes.
|
||||
|
||||
### 3. Real V2 checkbox artwork — `InstantPageRenderer.swift` + `InstantPageV2Layout.swift`
|
||||
|
||||
The V2 renderer (the live path for rich-message bubbles) currently draws a **placeholder**
|
||||
square. Replace it with the real artwork:
|
||||
|
||||
- In `InstantPageV2ListMarkerView.rebuildContents()`'s `.checklist` case, host a
|
||||
`CheckNode(theme:, content: .check(isRectangle: true))` (add its `.view` as a subview),
|
||||
themed exactly like the V1 `InstantPageChecklistMarkerNode` (`CheckNodeTheme` from
|
||||
`panelAccentColor` / `pageBackgroundColor` / `controlColor`), and call
|
||||
`setSelected(checked, animated: false)`. `import CheckNode` (the module is already a
|
||||
`BUILD` dep of InstantPageUI).
|
||||
- The marker view's `init` does not receive the theme, only `update(item:theme:)` does. Carry
|
||||
the three required colors on the `.checklist` marker payload (the layout has
|
||||
`context.theme`) so the view stays theme-agnostic and the colors are present at init time.
|
||||
- Layout detection switches from `instantPageTaskListMarkerState(item.num)` to `item.checked`;
|
||||
the marker kind becomes `.checklist(checked: item.checked == true)` whenever
|
||||
`item.checked != nil`. Remove the V1/V2 sentinel constants and `instantPageTaskListMarkerState`.
|
||||
|
||||
### 4. Reverse markdown (edit path) — `InstantPageToMarkdown.swift`
|
||||
|
||||
`markdownList` currently ignores the marker, so editing a rich message downgrades checkboxes
|
||||
to bullets. Read `item.checked` and emit `- [ ] ` / `- [x] ` (task markers are an unordered
|
||||
construct). Re-classification on save re-parses it back to a checkbox → API flags.
|
||||
|
||||
### 5. preview-text — `InstantPagePreviewText.swift`
|
||||
|
||||
`previewText()` currently prepends `num` blindly, which under the old sentinel leaked
|
||||
`"\u{001f}tg-md-task:checked. text"` into notifications/reply panels. With the first-class
|
||||
field, `num` is once again only a real number, so the leak is gone; additionally render a
|
||||
checkbox glyph (`"☑︎ "` / `"☐ "`) before the text when `checked != nil`.
|
||||
|
||||
### 6. Markdown forward parser — `BrowserMarkdown.swift`
|
||||
|
||||
`markdownListItems` keeps the existing `[ ]`/`[x]`/`[X]` detection
|
||||
(`markdownTaskListMarker` / `markdownStrippingTaskListMarker` / `markdownApplyTaskListMarker`)
|
||||
but routes the result into the new `checked` field instead of the `num` sentinel:
|
||||
|
||||
- Unordered task item → `.text(text, nil, state)` (number stays nil).
|
||||
- Ordered task item (`1. [ ] x`) → `.text(text, "\(ordinal)", state)` — number **and** checkbox
|
||||
now coexist (previously the sentinel destroyed the number).
|
||||
|
||||
Remove the markdown sentinel constants (`markdownTaskListUncheckedNumber` /
|
||||
`CheckedNumber`) and `markdownTaskListNumber`. Update the `.blocks(...)` / `.text(...)`
|
||||
construction arms and the `validate(listItem:)` destructure to the 3-value shape.
|
||||
|
||||
### 7. Other construction/destructure sites (mechanical, compiler-enforced)
|
||||
|
||||
The enum change touches these files' `.text(...)` / `.blocks(...)` sites; all are in
|
||||
list-handling modules (no external consumer constructs `InstantPageListItem`):
|
||||
|
||||
- `SyncCore_InstantPage.swift` — decodeListItems, Postbox decode/encode, `==`, FlatBuffers codec
|
||||
- `ApiUtils/InstantPage.swift` — `num`/`checked` accessors, `init(apiListItem:)`,
|
||||
`init(apiListOrderedItem:)`, `apiInputPageListItem()`, `apiInputPageOrderedListItem()`
|
||||
- `BrowserReadability.swift` — `.blocks(blocks, nil)` / `.text(...)` builders → add `nil`
|
||||
- `InstantPageV2Layout.swift` / `InstantPageLayout.swift` — `layoutList` empty-blocks
|
||||
substitution (`.text(.plain(" "), num)` must carry `checked` through) and the
|
||||
`.text`/`.blocks` destructures
|
||||
- `InstantPagePreviewText.swift`, `InstantPageToMarkdown.swift` — destructures (see 4 / 5)
|
||||
|
||||
## Round-trip contract
|
||||
|
||||
```
|
||||
compose "- [ ] x"
|
||||
→ markdown parse → .text("x", nil, false)
|
||||
→ render checkbox (V2 CheckNode)
|
||||
→ send: Api.PageListItem.pageListItemText(flags: 1<<0, text:"x")
|
||||
→ server echo → receive: flags bit0 set, bit1 clear → .text("x", nil, false)
|
||||
→ render checkbox on SENDER and RECIPIENT (and native checkbox on other clients)
|
||||
edit
|
||||
→ reverse markdown reads checked=false → "- [ ] x"
|
||||
→ re-classify → .text("x", nil, false) (identical)
|
||||
```
|
||||
|
||||
Postbox/FlatBuffers carry `checked` locally across app restarts; the API flags carry it
|
||||
across the server.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Interactivity (tapping a rendered checkbox to toggle it). These are display-only, matching
|
||||
the rest of InstantPage rendering.
|
||||
- Inline images/videos inside list items (unchanged; pre-existing behavior).
|
||||
|
||||
## Testing / verification
|
||||
|
||||
No unit tests exist in this project. Verify manually after a full build:
|
||||
|
||||
1. **Build** the full `Telegram/Telegram` target (`--continueOnError`) — the compile-breaking
|
||||
enum change surfaces any missed `.text`/`.blocks` site.
|
||||
2. **Send round-trip:** send a rich message `- [ ] a` / `- [x] b` to Saved Messages; confirm
|
||||
the checkboxes persist **after the send confirms** (not just in the pre-send preview), and
|
||||
that checked/unchecked states are correct.
|
||||
3. **Edit round-trip:** edit that message; confirm the editor repopulates `- [ ] a` / `- [x] b`
|
||||
and saving preserves the states.
|
||||
4. **Preview surfaces:** confirm the chat list / notification / reply panel previews show a
|
||||
checkbox glyph, never the raw sentinel or `num`.
|
||||
5. **Regression:** ordinary ordered (`1.`) and unordered (`-`) lists still render with correct
|
||||
numbers/bullets.
|
||||
|
||||
## Risks
|
||||
|
||||
- **FlatBuffers regen:** the checked-in `*_generated.swift` is stale; the build regenerates
|
||||
from the `.fbs`. Follow the flatc casing rules already documented (edit `.fbs`, not the
|
||||
generated Swift).
|
||||
- **Source-breaking enum change:** mitigated by the compiler — every site must be updated to
|
||||
build. The full-build step is the completeness gate.
|
||||
- **Tri-state encoding:** both Postbox (`"ck"` int) and FlatBuffers (`checkState:int32`) treat
|
||||
absent/0 as `nil`, so pre-existing stored pages decode unchanged.
|
||||
|
|
@ -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