Revert the universal/dynamic watch bundle id change (commit 1120b9084e)
back to the static 0682 shape, for install testing.
This reverts the PRODUCT_BUNDLE_IDENTIFIER override + $(...:base) companion-id
derivation; the watch app again bakes the hardcoded ph.telegra.Telegraph.watchkitapp
and literal WKCompanionAppBundleIdentifier=ph.telegra.Telegraph.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parse custom emoji into RichText.textCustomEmoji when sending markdown
rich messages, and round-trip them through edit, copy, and paste using a
shared tg://emoji?id=<fileId> markdown-link marker.
- Send: rewrite each customEmoji input attribute into a
[<alt>](tg://emoji?id=<fileId>) marker before the CommonMark parse, then
intercept the marker URL afterward to emit .textCustomEmoji. Only rich
messages are affected; a custom emoji alone stays on the entity path.
- Reverse: InstantPageToMarkdown (whole-message copy + edit reconstruction)
and InstantPageMultiTextAdapter (selection copy) emit the marker;
edit-load and chat paste reattach it as a live customEmoji attribute.
- Marker helpers shared in TextFormat/CustomEmojiMarkdownMarker.swift.
- Rich sends now pass inlineStickers so recipients can fetch the files.
Follow-up to verify at runtime: recipient rendering goes out with
Api.InputRichMessage.documents: nil; if recipients see only the fallback
glyph, populate documents: in apiInputRichMessage().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the thinking block render consistently with a normal text item:
- Size the `.thinking` item frame text-width at the page inset
(`(horizontalInset, 0, textWidth, height)`), identical to a `.text`
item, instead of a full-bleed `(0, 0, boundingWidth, height)` box, by
laying the text out flush and moving the inset onto the block frame.
The shimmer (sized to `item.frame.size`) now hugs the text.
- Size the shimmer + its gradient mask to the clipping-inset-expanded
frame (shifted `-inset`) so the mask no longer crops glyph ascenders,
descenders and the last line's underline, mirroring how a `.text`
view's frame is inset-expanded.
- Visit the thinking block's shimmer-wrapped inner text view in
updateInlineEmoji / updateInlineImages so its inline custom emoji and
images get layers created, positioned and revealed. They were skipped
because the page only iterated top-level InstantPageV2TextView items.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a first-class InstantPageBlock.thinking(RichText) case mirroring .kicker:
- enum case + InstantPageBlockType.thinking = 30; Postbox coding & equality
- FlatBuffers InstantPageBlock_Thinking schema (appended last) + Swift codecs
- API parse at InstantPage.swift; output-only on send (apiInputBlock returns nil)
- no-op stubs in the two exhaustive switches (preview text, V2 layout)
Rendering is out of scope. Verified by a full Bazel build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the user long-presses Send in the main chat and the composed text passes the
same rich-markdown gate used at send time (richMarkdownAttributeIfNeeded), the
send-options preview bubble renders the message as rich InstantPage content,
crossfading from the raw text as the menu animates in. Plain text and custom chat
contents / edit previews are unchanged.
ChatSendMessageActionUI cannot depend on InstantPageUI (it closes a dependency
cycle via LocationUI -> AttachmentUI -> ... -> ChatTextInputActionButtonsNode), so
the rich view is built in TelegramUI (ChatSendMessageRichTextPreview, wrapping
InstantPageV2View + the outgoing message theme) and injected into the screen via a
new ChatSendMessageContextScreenRichTextPreview protocol, mirroring the existing
media-preview pattern. The main-chat send call site classifies the compose text
and injects the preview; MessageItemView lays it out within the send-button-bounded
bubble envelope and crossfades between the raw text and the rich layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rich messages (RichTextMessageAttribute, text == "") are copyable as
markdown two ways: the context-menu Copy action copies the whole
message, and a text selection inside the rich-data bubble copies just
the selected range. Both reconstruct markdown mirroring the edit
round-trip (markdownStringFromInstantPage).
Implements the full RichText entity case set
(mention/hashtag/cashtag/bot-command/bank-card/auto url/email/phone) with
tap interaction, the InstantPage -> markdown inverse converter and edit
round-trip, markdown-context stamping during V2 layout
(InstantPageMarkdownBlockContext: heading level, list/code/table/quote
depth), partial-selection markdown emission
(InstantPageMultiTextAdapter.markdownForRange), and numerous converter
edge-case fixes (tables, links, fenced code, blockquote line coalescing,
compact nested >> markers).
CLAUDE.md documents the feature; the spec/plan scratch docs generated
during development are not committed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Upgrade InstantPageBlock.blockQuote from (text, caption) to
(blocks: [InstantPageBlock], caption). Legacy inbound shapes (API
pageBlockBlockquote, Postbox "t", FlatBuffers text) lift into
[.paragraph(text)]; outbound stays on the legacy wire constructor for
empty/single-paragraph quotes and uses pageBlockBlockquoteBlocks
otherwise. V1/V2 renderers recurse into child blocks with a fixed 10pt
inter-child gap; markdown forward/reverse, entity-expressibility, and
preview text updated. pullQuote is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upgrades the InstantPageBlock.blockQuote case from a text-only
payload to a nested-blocks payload, covering API parse, Postbox +
FlatBuffers serialization, API encode, V1/V2 layout, markdown
forward/reverse, entity-expressibility, and preview text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bubble's bottom inset is supplied by the `statusBottomEdge + 6.0`
max() in the measure closure, but that branch is gated by `!hasDraft`.
During streaming (status hidden, alpha=0) the gate skips the max() and
nothing else supplies the inset — boundingSize.height ends up at only
`revealedContentSize.height + 2` (= bounds.maxY + closingPad + rim),
so descenders of the last revealed line sit cramped against the bubble's
bottom edge and the bubble visibly grows by 6pt the moment streaming
ends and the status node fades in.
Adds an explicit `if hasDraft { boundingSize.height += 6.0 }` after
the streamingHeaderOffset application — same 6pt the status max() would
contribute, so the streaming bubble's bottom breathing room matches its
post-stream height and the grow-pop disappears.
The `hadDraft && !hasDraft` finalize pass already gets the inset via
the status max() (since `!hasDraft` is true), so it's untouched. The
non-streaming path is also untouched.
TextBubble doesn't have this issue because it computes a position-based
`bottomInset` and adds it to boundingSize.height unconditionally (see
ChatMessageTextBubbleContentNode.swift:234-256, :827). RichData absorbed
that value into the `+ 6.0` constant inside its `!hasDraft` status
max(), which made it disappear during streaming. CLAUDE.md gains a
sibling bullet flagging that future refactor-the-constant-into-bottomInset
work must remove both ends in the same commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>