You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CLAUDE.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -69,7 +69,7 @@ The sim ignores code signing, so the unsigned `Telegram_archive-root` bundle run
69
69
70
70
## RichTextEditor editor & the `ChatInputContent` composer
71
71
72
-
A from-scratch WYSIWYG rich-text editor (`submodules/TelegramUI/Components/RichTextEditor`) is the default chat composer (the `forceLegacyTextInput` experimental flag opts back out to the legacy input), and `ChatInputContent` (a TelegramCore-native value model) replaced `NSAttributedString` as the composer currency. The app-side integration — the model and its load-bearing invariants, composer ↔ editor wiring, the formatting-menu / custom-emoji-mention-date / code-block / inline-media round-trips, rich-message send / edit / pending-display, the long-press-Send send-options preview, and draft persistence (local, cross-device media sync, re-login restore) — lives in [`docs/richtext-composer.md`](docs/richtext-composer.md). Editor internals (the TextKit seam, layout) are the editor's own `submodules/TelegramUI/Components/RichTextEditor/CLAUDE.md`; message **rendering** is [`docs/instantpage-richtext.md`](docs/instantpage-richtext.md).
72
+
A from-scratch WYSIWYG rich-text editor (`submodules/TelegramUI/Components/RichTextEditor`) is the native chat-composer backend — by default a **dual-field switch** (the composer uses the legacy input and latches to the native editor only when content becomes legacy-non-representable); the `forceNewTextInput` experimental flag (Debug Settings ▸ "Force Text Field v2") forces always-native. (This inverted the earlier default+`forceLegacyTextInput`-opt-out scheme.) `ChatInputContent` (a TelegramCore-native value model) replaced `NSAttributedString` as the composer currency. The app-side integration — the model and its load-bearing invariants, composer ↔ editor wiring, the formatting-menu / custom-emoji-mention-date / code-block / inline-media round-trips, rich-message send / edit / pending-display, the long-press-Send send-options preview, and draft persistence (local, cross-device media sync, re-login restore) — lives in [`docs/richtext-composer.md`](docs/richtext-composer.md). Editor internals (the TextKit seam, layout) are the editor's own `submodules/TelegramUI/Components/RichTextEditor/CLAUDE.md`; message **rendering** is [`docs/instantpage-richtext.md`](docs/instantpage-richtext.md).
"Community.View.ShowAsOneChat" = "Show as One Chat";
@@ -16440,6 +16443,8 @@ Error: %8$@";
16440
16443
"Community.View.ChatsLimitReached" = "Sorry, this community has reached the maximum number of chats.";
16441
16444
"Community.Request.UnknownRequester" = "Someone";
16442
16445
"Community.Request.RequesterSuggestsGroup" = "%@ suggests this group:";
16446
+
"Community.Request.RequesterSuggestsChannel" = "%@ suggests this channel:";
16447
+
"Community.Request.RequesterSuggestsBot" = "%@ suggests this bot:";
16443
16448
"Community.Request.PrivateStatus" = "visible only to its members";
16444
16449
"Community.Request.Decline" = "Decline";
16445
16450
"Community.Request.Add" = "Add";
@@ -16514,13 +16519,23 @@ Error: %8$@";
16514
16519
"Community.Add.VisibilityHiddenInfo" = "Only invited members and community admins will see this chat.";
16515
16520
"Community.Add.ActionAddToCommunity" = "Add to Community";
16516
16521
16522
+
"Community.Add.Confirm.TextGroup" = "If this group is approved by a community admin, its members will be able to join or request to join other chats in the community.";
16523
+
"Community.Add.Confirm.TextChannel" = "If this channel is approved by a community admin, its subscribers will be able to join or request to join other chats in the community.";
16524
+
"Community.Add.Confirm.TextBot" = "If this bot is approved by a community admin, community members will be able to see and use it.";
16525
+
"Community.Add.Confirm.Add" = "Add";
16526
+
16527
+
"Community.AddChat.Title" = "Add a Chat";
16528
+
"Community.AddChat.Description" = "Select a chat to add to this community";
16529
+
16517
16530
"Community.Chat.JoinToView" = "You need to join this group to view its messages.";
16518
16531
"Community.PrivateChat.Info" = "This group is invite-only.\nOnly its members can view it.";
16519
16532
"Community.PrivateChat.MessageOwner" = "Message Group Owner";
16533
+
"Community.CommunityAdded.View" = "View";
16520
16534
16521
16535
"Privacy.GroupsAndChannels.InviteToCommunityError" = "Sorry, you cannot add %@ to the community because of %@'s privacy settings.";
16522
16536
16523
16537
"Notification.CommunityAddedGroup" = "%1$@ added this group to %2$@ community";
16538
+
"Notification.CommunityAddedGroupUnknown" = "This group was added to %1$@ community";
16524
16539
"Notification.CommunityAddedGroupYou" = "You added this group to %1$@ community";
16525
16540
"Notification.CommunityRemovedGroup" = "%1$@ removed this group from a community";
16526
16541
"Notification.CommunityRemovedGroupYou" = "You removed this group from a community";
-**Forward parser keeps `[ ]` detection but routes to `checked`.**`markdownApplyTaskListMarker`/`markdownStrippingTaskListMarker`/`markdownTaskListMarker` still strip the marker from the item text; the state flows into `checked` while ordered items keep their real `"\(ordinal)"` number. The reverse converter emits lowercase `[x]` / `[ ]`, which the forward `hasPrefix` guards re-parse — that is the round-trip contract.
439
448
-**The enum-arity change is compile-enforced.** Adding the third associated value broke every `.text`/`.blocks` construction/destructure; the full build is the completeness gate. Read-only consumers outside the core set exist (`BrowserInstantPageContent.swift`, `CachedFaqInstantPage.swift`) — grep `\.(text|blocks)\(` repo-wide when touching the enum again.
440
449
450
+
### Tap-to-toggle (editable rich messages)
451
+
452
+
Task-list checkboxes in a rendered rich message are **interactive when the message is editable**: tapping one flips it and persists the change by editing the message.
453
+
454
+
Where things live:
455
+
456
+
| File | Responsibility |
457
+
|---|---|
458
+
|`submodules/TelegramCore/Sources/InstantPageCheckboxToggle.swift`| Pure transform `InstantPage.togglingCheckbox(at: [Int], to: Bool) -> InstantPage` — rebuilds the block tree following a structural path and flips the target list item's `checked` (no-op on an unresolvable/non-checkbox path). |
459
+
|`submodules/InstantPageUI/Sources/InstantPageV2Layout.swift`|`InstantPageV2ListMarkerItem.checkboxPath: [Int]?`; a `pathPrefix: [Int]` threaded through `layoutBlockSequence`/`layoutBlock`/`layoutList`/`layoutDetails`/`layoutBlockQuote` stamps each `.checklist` marker with its path. |
460
+
|`submodules/InstantPageUI/Sources/InstantPageRenderer.swift`|`InstantPageV2ListMarkerView` installs a tap gesture when `interactive`, flips its own `CheckNode` optimistically, and fires `onCheckboxTapped(path, newValue)`; `InstantPageV2View.checkboxTapped` routes it up (mirrors `detailsTapped`). |
461
+
|`submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift`|`canEditMessageRichText: (EngineRawMessage) -> Bool` (sync gate, mirrors `canSetupReply`) + `toggleMessageRichTextCheckbox: (EngineMessage.Id, [Int], Bool) -> Void` (the edit action). Both have no-op defaults so only the real chat wires them. |
462
+
|`submodules/TelegramUI/Sources/ChatController.swift`| Implements both closures. The toggle looks up the message, re-checks `canEditMessage`, applies `togglingCheckbox`, and submits via `pendingUpdateMessageManager.add(text: "", richText:)` — the composer's rich-edit path (mirrors the native-todo `requestToggleTodoMessageItem`). |
463
+
|`submodules/TelegramUI/Components/Chat/.../ChatMessageRichDataBubbleContentNode.swift`|`checkboxesInteractive(item:resolved:)` gate + sets `pageView.checkboxTapped` per apply. |
464
+
465
+
Non-obvious invariants:
466
+
467
+
-**Checkbox identity is a structural `[Int]` path**, not an ordinal: each element indexes the current container's children — block-array index at page/`.details`/`.blockQuote`/list-item-`.blocks` levels; item index at `.list` level. The layout stamping (`InstantPageV2Layout`) and the toggle walker (`InstantPageCheckboxToggle`) MUST keep identical semantics — they were built and reviewed as a matched pair. Decoupled from `<details>` expand/collapse state.
468
+
-**The path is absolute-from-root only because every `layoutBlockSequence` that can reach a list is entered with a correct prefix.** The two secondary `layoutBlockSequence` call sites (table cells, hard-coded `[.paragraph]`; and the details title) contain no lists, so no checkbox is produced there; a `kind != .cell` guard in `layoutList` is belt-and-suspenders against future misrouting.
469
+
-**The toggle applies to `attribute.instantPage`, so taps must only ever fire against that page.** The bubble's `checkboxesInteractive` gate enforces this: interactive only when `resolved.key` is `.original` AND `resolved.instantPage === attribute.instantPage` (class identity — excludes the show-more `fullInstantPage` rendering, which shares the `.original` key but is a different `InstantPage` object) AND `canEditMessageRichText(item.message)`. Translations (`.translated`) and in-flight pending edits (`.pendingEdit`) are inert. Non-editable messages (incoming, past the edit window) are inert — and AI-streamed rich messages are incoming, hence never tappable.
470
+
-**Optimistic flip, model supersedes.** The marker view flips its own `CheckNode` on tap; the pending/edited attribute re-render then supersedes (or, on failure, reverts, since the model was unchanged). Known minor edges of this state-free optimism: (1) an unrelated `update()` before the pending edit lands rebuilds the marker from the old `checked`, briefly reverting the visual; (2) a rapid double-tap re-reads the still-stale `checked` and re-sends the same value rather than toggling back — the pending-edit manager coalesces, so the net state is consistent, just not a double-toggle. Both are acceptable given the edit lands promptly.
471
+
-**Gating uses closures because `canEditMessage` is `internal` to the `TelegramUI` module** and the rich-data bubble lives in a separate component module that cannot call it. The two closures bridge the boundary; **their init-parameter order must match the `ChatController` call-site order** (Swift requirement) — they sit between `displayTodoToggleUnavailable` and `openStarsPurchase`.
472
+
441
473
## InstantPageBlock.blockQuote nested blocks
442
474
443
475
`InstantPageBlock.blockQuote` carries `(blocks: [InstantPageBlock], caption: RichText)` — a sequence of nested page blocks (paragraphs, headings, lists, code, even nested quotes), not the legacy text-only payload. `.pullQuote` is unchanged (still `(text: RichText, caption: RichText)`; the TL API has no `pullQuoteBlocks` constructor).
0 commit comments