morethanwords/tweb/master • bcd2792 • 3 files, +67/-1
fix: detect group-call media-transport stall (ICE connected, DTLS hung)
GroupCallInstance.connectionState (and thus the call UI) reports only the
ICE connection state, so a call where ICE reaches `connected` but the
RTCPeerConnection never does — the DTLS handshake stalls, seen on
restrictive networks / some VPNs — looked "connected" while no media ever
flowed: black video tiles, silence, the SFU data channel never opening,
no error anywhere.
Watch the real RTCPeerConnection.connectionState in groupCallsController:
arm a 10s watchdog when ICE connects, and if connectionState hasn't
reached `connected` by then (or goes `failed` — which the existing ICE
`failed` branch never caught, since ICE stays `connected` while DTLS
fails), surface it via the existing conference-bug breadcrumb and hang up
the dead call instead of leaving the user on a silent black call.
Also log the DtlsTransport state on connectionstatechange so an exported
log pins a DTLS stall directly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
morethanwords/tweb/master • 007cf7c • 13 files, +821/-100
fix: harden E2E call crypto — DH param validation + block authorization
The server is the explicit adversary of Telegram's E2E calls, but WebK trusted
server-supplied data in two places, and the tde2e port had skipped several
validations the reference (tdlib/tdesktop) enforces.
1-on-1 calls (src/lib/crypto):
- Validate the DH config {p,g} from messages.getDhConfig: pin the known 2048-bit
safe prime (safe-prime + generator-congruence fallback) and range-check both
our g_a and the peer's g_b (1<x<p-1, 2^(2048-64) bounds) — mirrors tdesktop
mtproto_dh_utils. Defeats a malicious server's smooth prime (passive key
recovery, invisible to the emoji SAS) and degenerate peer values. Hang up on
any failure.
E2E group calls (src/lib/calls/e2e/blockchain.ts):
- Port tde2e State::apply per-change authorization: derive the signer's
permissions from the prior group state and reject changes it isn't entitled
to make (may_add_users / may_remove_users, no granting flags the signer lacks,
no widening external_permissions, may_change_shared_key requires membership).
- Make state-proof validation faithful to validate_state (group_state/shared_key
omitted-when-changed, present-when-not) and require >=1 SetValue/SetGroupState.
tde2e port hardening:
- TLReader.vector: reject a length larger than the remaining buffer (DoS).
- Reject trailing data after decodeBlock / decodeGroupBroadcast (fetch_end).
- serverToLocal: only strip the +1 magic when magic-1 is a known constructor.
- hydrateStateFromBlock: reject negative block height.
- Buffer future-height verification broadcasts and replay them when the chain
advances (delayed_broadcasts_), so SFU reordering can't suppress the emoji SAS.
- Re-check our pubkey<->user_id on every epoch; co-locate the SELF_PACKET guard.
Adds unit + adversarial tests: DH validation, block authorization, state-proof
shape, and broadcast reordering.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#webk
fix: detect group-call media-transport stall (ICE connected, DTLS hung)
GroupCallInstance.connectionState (and thus the call UI) reports only the
ICE connection state, so a call where ICE reaches `connected` but the
RTCPeerConnection never does — the DTLS handshake stalls, seen on
restrictive networks / some VPNs — looked "connected" while no media ever
flowed: black video tiles, silence, the SFU data channel never opening,
no error anywhere.
Watch the real RTCPeerConnection.connectionState in groupCallsController:
arm a 10s watchdog when ICE connects, and if connectionState hasn't
reached `connected` by then (or goes `failed` — which the existing ICE
`failed` branch never caught, since ICE stays `connected` while DTLS
fails), surface it via the existing conference-bug breadcrumb and hang up
the dead call instead of leaving the user on a silent black call.
Also log the DtlsTransport state on connectionstatechange so an exported
log pins a DTLS stall directly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
morethanwords/tweb/master • 007cf7c • 13 files, +821/-100
fix: harden E2E call crypto — DH param validation + block authorization
The server is the explicit adversary of Telegram's E2E calls, but WebK trusted
server-supplied data in two places, and the tde2e port had skipped several
validations the reference (tdlib/tdesktop) enforces.
1-on-1 calls (src/lib/crypto):
- Validate the DH config {p,g} from messages.getDhConfig: pin the known 2048-bit
safe prime (safe-prime + generator-congruence fallback) and range-check both
our g_a and the peer's g_b (1<x<p-1, 2^(2048-64) bounds) — mirrors tdesktop
mtproto_dh_utils. Defeats a malicious server's smooth prime (passive key
recovery, invisible to the emoji SAS) and degenerate peer values. Hang up on
any failure.
E2E group calls (src/lib/calls/e2e/blockchain.ts):
- Port tde2e State::apply per-change authorization: derive the signer's
permissions from the prior group state and reject changes it isn't entitled
to make (may_add_users / may_remove_users, no granting flags the signer lacks,
no widening external_permissions, may_change_shared_key requires membership).
- Make state-proof validation faithful to validate_state (group_state/shared_key
omitted-when-changed, present-when-not) and require >=1 SetValue/SetGroupState.
tde2e port hardening:
- TLReader.vector: reject a length larger than the remaining buffer (DoS).
- Reject trailing data after decodeBlock / decodeGroupBroadcast (fetch_end).
- serverToLocal: only strip the +1 magic when magic-1 is a known constructor.
- hydrateStateFromBlock: reject negative block height.
- Buffer future-height verification broadcasts and replay them when the chain
advances (delayed_broadcasts_), so SFU reordering can't suppress the emoji SAS.
- Re-check our pubkey<->user_id on every epoch; co-locate the SELF_PACKET guard.
Adds unit + adversarial tests: DH validation, block authorization, state-proof
shape, and broadcast reordering.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#webk
👍3🫡1
morethanwords/tweb/master • 9d58fe1 • 117 files, +1371/-403
feat: Document Picture-in-Picture — pop the whole client into an always-on-top window
Move the live app DOM into a Document PiP window (same instance, instant) via the left-sidebar More submenu "Picture-in-Picture" entry, gated on Chromium support and toggling to "Exit" while popped out.
Core: appWindow.ts metrics/overlay rebind layer (getAppWindow / getOverlayRoot / bindActiveWindowListener / onAppWindowChange); clientPip move/restore + mirrorDocumentStyles + ClientPipPlaceholder for the vacated tab.
Plus a broad realm-coupling sweep so overlays, context menus, gestures, keyboard, paste/drop, IntersectionObserver, scroll preservation and cross-realm instanceof all keep working when the app lives in the PiP window.
Fullscreen from the media viewer is unavailable in PiP (forbidden by spec and activation cannot cross windows); the button shows a hint to return to the tab instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
morethanwords/tweb/master • 0fa2741 • 2 files, +53/-1
fix: deepEqual no longer leaks ignoreKeys into nested objects
Bug (discovery scan, src/helpers/object/deepEqual.ts)
-----------------------------------------------------
`deepEqual(x, y, ignoreKeys)` is meant to skip the listed keys at the
level being compared (e.g. drafts ignore `date`/`reply_to`; the chat
pattern renderer ignores `element`). But the recursive call passed the
SAME `ignoreKeys` down into every nested object:
ok(x).every((key) => deepEqual(x[key], y[key], ignoreKeys))
so any nested property whose name matched an ignored key was silently
skipped at every depth. Two objects that differed only inside a nested
object — under a same-named key — were wrongly reported equal.
Root cause (current code, line 12)
----------------------------------
`ignoreKeys` was forwarded unconditionally in the recursion, turning a
top-level filter into a global, all-depths filter.
Fix
---
Drop `ignoreKeys` from the recursive call so it applies only at the
comparison (top) level — the documented intent. One-token change plus a
doc note. Both existing callers (drafts `['date','reply_to']`, pattern
renderer `['element']`) compare flat shapes with no nested same-named
keys, so their behavior is unchanged; the fix is strictly more correct
and removes a footgun for any future caller.
Evidence
--------
New src/tests/deepEqual.test.ts (6 tests): flat compare, undefined-prop
skipping, nested recursion, top-level ignore, and the regression case
"does NOT ignore the keys inside nested objects". The regression test
FAILS on master (returns true, expected false) and PASSES after the fix;
the other 5 lock existing behavior. Full suite: 215 passed (the lone
unrelated failure, appGroupCallsManagerJoin.test.ts, is pre-existing on
master and in an off-limits area).
Gates: tsc --noEmit 0 · eslint 0 (both files) · vitest 6/6 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
feat: Document Picture-in-Picture — pop the whole client into an always-on-top window
Move the live app DOM into a Document PiP window (same instance, instant) via the left-sidebar More submenu "Picture-in-Picture" entry, gated on Chromium support and toggling to "Exit" while popped out.
Core: appWindow.ts metrics/overlay rebind layer (getAppWindow / getOverlayRoot / bindActiveWindowListener / onAppWindowChange); clientPip move/restore + mirrorDocumentStyles + ClientPipPlaceholder for the vacated tab.
Plus a broad realm-coupling sweep so overlays, context menus, gestures, keyboard, paste/drop, IntersectionObserver, scroll preservation and cross-realm instanceof all keep working when the app lives in the PiP window.
Fullscreen from the media viewer is unavailable in PiP (forbidden by spec and activation cannot cross windows); the button shows a hint to return to the tab instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
morethanwords/tweb/master • 0fa2741 • 2 files, +53/-1
fix: deepEqual no longer leaks ignoreKeys into nested objects
Bug (discovery scan, src/helpers/object/deepEqual.ts)
-----------------------------------------------------
`deepEqual(x, y, ignoreKeys)` is meant to skip the listed keys at the
level being compared (e.g. drafts ignore `date`/`reply_to`; the chat
pattern renderer ignores `element`). But the recursive call passed the
SAME `ignoreKeys` down into every nested object:
ok(x).every((key) => deepEqual(x[key], y[key], ignoreKeys))
so any nested property whose name matched an ignored key was silently
skipped at every depth. Two objects that differed only inside a nested
object — under a same-named key — were wrongly reported equal.
Root cause (current code, line 12)
----------------------------------
`ignoreKeys` was forwarded unconditionally in the recursion, turning a
top-level filter into a global, all-depths filter.
Fix
---
Drop `ignoreKeys` from the recursive call so it applies only at the
comparison (top) level — the documented intent. One-token change plus a
doc note. Both existing callers (drafts `['date','reply_to']`, pattern
renderer `['element']`) compare flat shapes with no nested same-named
keys, so their behavior is unchanged; the fix is strictly more correct
and removes a footgun for any future caller.
Evidence
--------
New src/tests/deepEqual.test.ts (6 tests): flat compare, undefined-prop
skipping, nested recursion, top-level ignore, and the regression case
"does NOT ignore the keys inside nested objects". The regression test
FAILS on master (returns true, expected false) and PASSES after the fix;
the other 5 lock existing behavior. Full suite: 215 passed (the lone
unrelated failure, appGroupCallsManagerJoin.test.ts, is pre-existing on
master and in an off-limits area).
Gates: tsc --noEmit 0 · eslint 0 (both files) · vitest 6/6 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
👍3🫡2
morethanwords/tweb/master • 690ab5f • 4 files, +50/-4
fix: format file sizes >= 1 TB instead of emitting a bare "1.000 "
Bug (class: bugs.telegram.org #5273-adjacent number/size formatting +
direct code defect): formatBytesPure / formatBytes drop the unit for
sizes >= 1 TB.
Root cause: both helpers index a length-4 sizes array
(['B','KB','MB','GB']) with i = floor(log(bytes)/log(1024)). For
bytes >= 1024^4 the index becomes 4 (or more), so sizes[i] is
undefined. formatBytesPure(1024**4) returned "1.000 " (value, a space,
NO unit, trailing space); 5 TB -> "5.000 ". formatBytes was worse: it
passed the undefined LangPackKey straight into i18n(), garbling the
unit. There was also no FileSize.TB lang key.
Reachable from the Data & Storage UI: storageQuota.tsx formats
navigator.storage.estimate() quota, which on a modern multi-TB disk
routinely exceeds 1 TB, plus cumulative cache totals.
Fix: extend the sizes arrays with TB and PB and clamp i to the array
bounds (Math.min(..., sizes.length - 1)) so the index can never select
an undefined unit; add the matching FileSize.TB / FileSize.PB lang keys.
Test (src/tests/formatBytesPure.test.ts): pure-unit Vitest.
Before the fix it FAILED — formatBytesPure(1024**4) === "1.000 " and
5*1024**4 === "5.000 " (bare value, no unit). After the fix it returns
"1.000 TB" / "5.000 TB", and a regression guard asserts the output
always ends in a real unit. Boundary cases (0, 1023, 1024, 1 GB)
unchanged.
Gates: npx tsc --noEmit (0), npx eslint on changed files (0),
npx vitest run src/tests/formatBytesPure.test.ts (7 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
fix: format file sizes >= 1 TB instead of emitting a bare "1.000 "
Bug (class: bugs.telegram.org #5273-adjacent number/size formatting +
direct code defect): formatBytesPure / formatBytes drop the unit for
sizes >= 1 TB.
Root cause: both helpers index a length-4 sizes array
(['B','KB','MB','GB']) with i = floor(log(bytes)/log(1024)). For
bytes >= 1024^4 the index becomes 4 (or more), so sizes[i] is
undefined. formatBytesPure(1024**4) returned "1.000 " (value, a space,
NO unit, trailing space); 5 TB -> "5.000 ". formatBytes was worse: it
passed the undefined LangPackKey straight into i18n(), garbling the
unit. There was also no FileSize.TB lang key.
Reachable from the Data & Storage UI: storageQuota.tsx formats
navigator.storage.estimate() quota, which on a modern multi-TB disk
routinely exceeds 1 TB, plus cumulative cache totals.
Fix: extend the sizes arrays with TB and PB and clamp i to the array
bounds (Math.min(..., sizes.length - 1)) so the index can never select
an undefined unit; add the matching FileSize.TB / FileSize.PB lang keys.
Test (src/tests/formatBytesPure.test.ts): pure-unit Vitest.
Before the fix it FAILED — formatBytesPure(1024**4) === "1.000 " and
5*1024**4 === "5.000 " (bare value, no unit). After the fix it returns
"1.000 TB" / "5.000 TB", and a regression guard asserts the output
always ends in a real unit. Boundary cases (0, 1023, 1024, 1 GB)
unchanged.
Gates: npx tsc --noEmit (0), npx eslint on changed files (0),
npx vitest run src/tests/formatBytesPure.test.ts (7 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • 07d14fb • 7 files, +176/-69
feat: peer-color background-emoji pattern behind web-page previews (tdesktop geometry, shared with replies)
Web-page previews now render the peer-color background-emoji pattern (the sender's
name-color `background_emoji_id`) that tweb already draws behind replies and quotes.
Previously the webpage box got only the quote background + left peer-color border.
Reference (tdesktop): `FillBackgroundEmoji` (history_view_reply.cpp:267) is defined
once and reused by replies (857), contacts (394) AND web pages
(history_view_web_page.cpp:1036) — the same geometry for all; web pages and normal
replies call it with quote=false.
This pulls tdesktop's EXACT pattern geometry into one shared module and points both
replies and web pages at it:
- New `src/components/wrappers/peerColorPattern.ts` owns the pattern (reads the peer's
`color.background_emoji_id`, computes the tint, calls `wrapEmojiPattern`, optional
collectible gift) AND the geometry ported 1:1 from tdesktop — the three cached frame
sizes 12/16/20 (reply.cpp:237-239) across 9 right-anchored positions with per-emoji
opacity (reply.cpp:296-310). tdesktop's right-anchored x maps to tweb's left-origin
canvas as `width - distanceFromRight` (the canvas is CSS-pinned to the box's right
edge); canvas 130x76, emojiSize 24 (source raster >= the largest 20px draw). The
quote-like box's overflow:hidden clips it to the real box height, mirroring tdesktop's
`if(y >= rect.height()) return`.
- `reply.ts` and `bubbles.ts` (webPage box ref) both call `wrapPeerColorPattern` with no
geometry of their own — single source of truth, identical look, exactly as tdesktop
shares one function. Webpage pattern gated on `!isOut && !isSponsored`, peer =
`fwdFromId || fromId` (same as the bubble border, so border + pattern share one color).
- `_chatPinned.scss` `.reply-background-canvas` unified to flush top-right
(top:0; inset-inline-end:0; pointer-events:none) — matching `.webpage-background-canvas`
and tdesktop's right anchor (was top:-6; inset-inline-end:9px).
Note: this is tdesktop's non-quote layout (webpage / contact / normal reply). The
quote-reply variant (+2 emoji, +12px offset, quote=_hasQuoteIcon) is not yet
distinguished in tweb's reply path — a possible follow-up.
Pure logic extracted + unit-tested: `pickPatternEmojiColor.ts` (collectible accent vs
gradient[0], night/dark fallback). Tests: `peerColorPattern.test.ts` 4/4.
Gates: tsc --noEmit clean (touched files); eslint clean; vitest 4/4. Visual verified in
an authed Chrome preview (replies + web-page previews render the identical tdesktop cluster).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
feat: peer-color background-emoji pattern behind web-page previews (tdesktop geometry, shared with replies)
Web-page previews now render the peer-color background-emoji pattern (the sender's
name-color `background_emoji_id`) that tweb already draws behind replies and quotes.
Previously the webpage box got only the quote background + left peer-color border.
Reference (tdesktop): `FillBackgroundEmoji` (history_view_reply.cpp:267) is defined
once and reused by replies (857), contacts (394) AND web pages
(history_view_web_page.cpp:1036) — the same geometry for all; web pages and normal
replies call it with quote=false.
This pulls tdesktop's EXACT pattern geometry into one shared module and points both
replies and web pages at it:
- New `src/components/wrappers/peerColorPattern.ts` owns the pattern (reads the peer's
`color.background_emoji_id`, computes the tint, calls `wrapEmojiPattern`, optional
collectible gift) AND the geometry ported 1:1 from tdesktop — the three cached frame
sizes 12/16/20 (reply.cpp:237-239) across 9 right-anchored positions with per-emoji
opacity (reply.cpp:296-310). tdesktop's right-anchored x maps to tweb's left-origin
canvas as `width - distanceFromRight` (the canvas is CSS-pinned to the box's right
edge); canvas 130x76, emojiSize 24 (source raster >= the largest 20px draw). The
quote-like box's overflow:hidden clips it to the real box height, mirroring tdesktop's
`if(y >= rect.height()) return`.
- `reply.ts` and `bubbles.ts` (webPage box ref) both call `wrapPeerColorPattern` with no
geometry of their own — single source of truth, identical look, exactly as tdesktop
shares one function. Webpage pattern gated on `!isOut && !isSponsored`, peer =
`fwdFromId || fromId` (same as the bubble border, so border + pattern share one color).
- `_chatPinned.scss` `.reply-background-canvas` unified to flush top-right
(top:0; inset-inline-end:0; pointer-events:none) — matching `.webpage-background-canvas`
and tdesktop's right anchor (was top:-6; inset-inline-end:9px).
Note: this is tdesktop's non-quote layout (webpage / contact / normal reply). The
quote-reply variant (+2 emoji, +12px offset, quote=_hasQuoteIcon) is not yet
distinguished in tweb's reply path — a possible follow-up.
Pure logic extracted + unit-tested: `pickPatternEmojiColor.ts` (collectible accent vs
gradient[0], night/dark fallback). Tests: `peerColorPattern.test.ts` 4/4.
Gates: tsc --noEmit clean (touched files); eslint clean; vitest 4/4. Visual verified in
an authed Chrome preview (replies + web-page previews render the identical tdesktop cluster).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • a274135 • 6 files, +155/-3
feat: render sticker-set / custom-emoji webpage preview as a sticker grid
A webPage linking to a sticker set or custom-emoji set (type
telegram_stickerset) carries a webPageAttributeStickerSet with the set's
preview stickers. tweb mapped the footer button + click (open the set popup)
but never rendered the stickers, so the bubble showed only text + button. All
three official clients draw a small right-aligned square NxN grid in the
square-photo slot (4 -> 2x2, 9 -> 3x3, side = ceil(sqrt(count))). This ports it.
- appWebPagesManager.saveWebPage: normalize the attribute's preview docs through
appDocsManager.saveDoc (MyDocument + file_reference context, like the
webPageAttributeStory precedent) so wrapSticker has media context. Same fix
applied to webPageAttributeStarGiftCollection icons.
- bubbles.ts (messageMediaWebPage): render the square grid into the existing
square-photo preview slot via the existing wrapSticker; custom-emoji + text
color tint gated on pFlags.emojis && pFlags.text_color. Previews loop.
- footer button: a custom-emoji set (pFlags.emojis) says "VIEW EMOJI"
(OpenEmojiSet), a sticker set keeps "VIEW STICKERS" (OpenStickers).
- _chatBubble.scss: grid layout + let the square resizer size to the inline box.
- New pure helper helpers/stickerSetPreviewGrid.ts isolates the grid geometry,
with src/tests/stickerSetPreviewGrid.test.ts (8 cases).
Reference: tdesktop history_view_web_page.cpp:1063-1084; Android (DrKLO)
ChatMessageCell.java:7499-7512.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
feat: render sticker-set / custom-emoji webpage preview as a sticker grid
A webPage linking to a sticker set or custom-emoji set (type
telegram_stickerset) carries a webPageAttributeStickerSet with the set's
preview stickers. tweb mapped the footer button + click (open the set popup)
but never rendered the stickers, so the bubble showed only text + button. All
three official clients draw a small right-aligned square NxN grid in the
square-photo slot (4 -> 2x2, 9 -> 3x3, side = ceil(sqrt(count))). This ports it.
- appWebPagesManager.saveWebPage: normalize the attribute's preview docs through
appDocsManager.saveDoc (MyDocument + file_reference context, like the
webPageAttributeStory precedent) so wrapSticker has media context. Same fix
applied to webPageAttributeStarGiftCollection icons.
- bubbles.ts (messageMediaWebPage): render the square grid into the existing
square-photo preview slot via the existing wrapSticker; custom-emoji + text
color tint gated on pFlags.emojis && pFlags.text_color. Previews loop.
- footer button: a custom-emoji set (pFlags.emojis) says "VIEW EMOJI"
(OpenEmojiSet), a sticker set keeps "VIEW STICKERS" (OpenStickers).
- _chatBubble.scss: grid layout + let the square resizer size to the inline box.
- New pure helper helpers/stickerSetPreviewGrid.ts isolates the grid geometry,
with src/tests/stickerSetPreviewGrid.test.ts (8 cases).
Reference: tdesktop history_view_web_page.cpp:1063-1084; Android (DrKLO)
ChatMessageCell.java:7499-7512.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
🫡2👍1
morethanwords/tweb/master • df939e8 • 4 files, +238/-21
feat(reactions): enforce reactions_uniq_max in the reaction picker
The per-user reaction-count cap (reactions_user_max_default / _premium,
"how many distinct reactions ONE user may place on ONE message") is already
fully implemented in tweb — getLimit('reactions') in apiManagerMethods.ts
(maps to those two appConfig keys, premium-aware) is enforced in
appReactionsManager.sendReaction(), which silently drops the user's OLDEST
chosen reaction beyond the cap. This matches tdesktop
(data/data_message_reactions.cpp:1937-2002) and iOS
(MessageReactions.swift:159-164) exactly — no blocking, no toast. Nothing
to build there.
The one reaction cap tweb did NOT implement is reactions_uniq_max
(appConfig.d.ts:27, default 11) — the total number of DISTINCT reactions
allowed to EXIST on a single message. Reference: tdesktop
data/data_peer_values.cpp:86-87 (UniqueReactionsLimit) consumed by
LookupPossibleReactions (data/data_message_reactions.cpp:211-261): once a
message already carries 11 distinct reactions, the picker is narrowed to the
reactions already present on the message, so a user can pile onto an existing
kind but can't introduce a 12th distinct one.
All three official clients enforce this the SAME way — a silent UX gate, no
toast/error — and crucially they gate the custom-emoji search/picker too, not
just the static reaction strip:
- tdesktop: LookupPossibleReactions leaves customAllowed=false at the cap, so
the selector opens in EmojiListMode::RecentReactions and the footer (search +
custom-emoji packs) is never created (history_view_reactions_selector.cpp:1462,
emoji_list_widget.cpp:1627).
- iOS: peerMessageAllowedReactions() returns .set of the present reactions, so
the context menu shows areCustomEmojiEnabled:false and the custom picker is
not offered (TopMessageReactions.swift:48, ChatControllerOpenMessageContextMenu).
- Android: hitLimit → showCustomEmojiReaction()=false hides the (+) expand button
(ReactionsContainerLayout.java:1058,1119,539), and MessageObject.selectReaction()
also returns false for a novel reaction at the cap, counting distinct reactions
EXCLUDING TL_reactionPaid (MessageObject.java:11918-11929).
Implementation:
- Pure helpers in @appManagers/utils/reactions/filterReactionsAtUniqCap:
countDistinctReactions() (excludes reactionPaid, the ⭐ star counter — matching
Android's chosenCount), isMessageAtUniqReactionCap(), and filterReactionsAtUniqCap
which narrows the offered list to the kinds already present once at the cap
(reactionPaid and already-present reactions kept). Default 11 mirrors tdesktop.
- appReactionsManager.getAvailableReactionsByMessage resolves the warm-cached
appConfig alongside the per-peer reactions (apiManager.getAppConfig, the same
accessor getLimit uses), narrows peerAvailableReactions.reactions, and flags
peerAvailableReactions.atUniqCap from the same uniqMax — single source of truth,
on the picker-open path (not chat-open) so the cached await is fine.
- reactionsMenu hides the custom-emoji search + packs (noSearch = noPacks = true)
when atUniqCap, so a novel reaction can't be introduced via the EmojiTab search
onFinish path — matching the three clients above. The narrowed grid still lets
the user pile onto existing kinds.
Tests: src/tests/filterReactionsAtUniqCap.test.ts — 14 cases covering default
fallback, below-cap pass-through (same reference), at-cap narrowing, reactionPaid
always kept + never counted toward the cap, present custom-emoji kept, custom
uniqMax bounds, empty/undefined inputs, over-cap, and the countDistinctReactions /
isMessageAtUniqReactionCap predicates. All green.
Gates: tsc --noEmit 0 errors; eslint 0 on all 4 files; vitest 14/14 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
feat(reactions): enforce reactions_uniq_max in the reaction picker
The per-user reaction-count cap (reactions_user_max_default / _premium,
"how many distinct reactions ONE user may place on ONE message") is already
fully implemented in tweb — getLimit('reactions') in apiManagerMethods.ts
(maps to those two appConfig keys, premium-aware) is enforced in
appReactionsManager.sendReaction(), which silently drops the user's OLDEST
chosen reaction beyond the cap. This matches tdesktop
(data/data_message_reactions.cpp:1937-2002) and iOS
(MessageReactions.swift:159-164) exactly — no blocking, no toast. Nothing
to build there.
The one reaction cap tweb did NOT implement is reactions_uniq_max
(appConfig.d.ts:27, default 11) — the total number of DISTINCT reactions
allowed to EXIST on a single message. Reference: tdesktop
data/data_peer_values.cpp:86-87 (UniqueReactionsLimit) consumed by
LookupPossibleReactions (data/data_message_reactions.cpp:211-261): once a
message already carries 11 distinct reactions, the picker is narrowed to the
reactions already present on the message, so a user can pile onto an existing
kind but can't introduce a 12th distinct one.
All three official clients enforce this the SAME way — a silent UX gate, no
toast/error — and crucially they gate the custom-emoji search/picker too, not
just the static reaction strip:
- tdesktop: LookupPossibleReactions leaves customAllowed=false at the cap, so
the selector opens in EmojiListMode::RecentReactions and the footer (search +
custom-emoji packs) is never created (history_view_reactions_selector.cpp:1462,
emoji_list_widget.cpp:1627).
- iOS: peerMessageAllowedReactions() returns .set of the present reactions, so
the context menu shows areCustomEmojiEnabled:false and the custom picker is
not offered (TopMessageReactions.swift:48, ChatControllerOpenMessageContextMenu).
- Android: hitLimit → showCustomEmojiReaction()=false hides the (+) expand button
(ReactionsContainerLayout.java:1058,1119,539), and MessageObject.selectReaction()
also returns false for a novel reaction at the cap, counting distinct reactions
EXCLUDING TL_reactionPaid (MessageObject.java:11918-11929).
Implementation:
- Pure helpers in @appManagers/utils/reactions/filterReactionsAtUniqCap:
countDistinctReactions() (excludes reactionPaid, the ⭐ star counter — matching
Android's chosenCount), isMessageAtUniqReactionCap(), and filterReactionsAtUniqCap
which narrows the offered list to the kinds already present once at the cap
(reactionPaid and already-present reactions kept). Default 11 mirrors tdesktop.
- appReactionsManager.getAvailableReactionsByMessage resolves the warm-cached
appConfig alongside the per-peer reactions (apiManager.getAppConfig, the same
accessor getLimit uses), narrows peerAvailableReactions.reactions, and flags
peerAvailableReactions.atUniqCap from the same uniqMax — single source of truth,
on the picker-open path (not chat-open) so the cached await is fine.
- reactionsMenu hides the custom-emoji search + packs (noSearch = noPacks = true)
when atUniqCap, so a novel reaction can't be introduced via the EmojiTab search
onFinish path — matching the three clients above. The narrowed grid still lets
the user pile onto existing kinds.
Tests: src/tests/filterReactionsAtUniqCap.test.ts — 14 cases covering default
fallback, below-cap pass-through (same reference), at-cap narrowing, reactionPaid
always kept + never counted toward the cap, present custom-emoji kept, custom
uniqMax bounds, empty/undefined inputs, over-cap, and the countDistinctReactions /
isMessageAtUniqReactionCap predicates. All green.
Gates: tsc --noEmit 0 errors; eslint 0 on all 4 files; vitest 14/14 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
🫡3👍1
morethanwords/tweb/master • f259511 • 7 files, +145/-5
feat: giveaway end-date validation + live, second-accurate date-picker minimum
Giveaway creation (Boosts via Gifts) allowed a past or out-of-window end date,
and the shared date/time picker's "Minimum send time" froze at popup-open so it
drifted into the past. Bound the giveaway end date to [now, now +
giveaway_period_max] like the official clients, and make the picker's minimum
track the clock to the second.
Giveaway end-date validation (boostsViaGifts):
- The expiration picker now passes minDate (today) + minTimeDate (now), so past
days/times are unselectable, plus a defense-in-depth submit guard that
re-validates via isGiveawayUntilDateValid before building the payload
(toast + shake if giveaway_period_max drifted after the date was picked).
- isGiveawayUntilDateValid (new pure helper): untilDate > now && untilDate <=
now + periodMax (seconds). Mirrors tdesktop ChooseDateTimeBox bounds
(.min = now, .max = now + giveawayPeriodMax) and its collect() guard.
- lang: BoostsViaGifts.InvalidEndDate.
Date-picker live minimum (datePicker, benefits every caller — giveaway,
suggested-post, poll end):
- minTimeDate was captured once at open: the caption showed a stale minimum, and
because the displayed floor is minute-granular while the hard guard compared
seconds, the shown minute itself became unselectable once its :00 had elapsed.
- earliestSelectableMinuteMs(minTime, now) (new, @helpers/date): ceil(max(minTime,
now)) up to the next full minute — never behind the clock, never offers a
minute already partly in the past, keeps the caption and Confirm-enable
consistent.
- A 1s clock ticks the minute bucket; the caption, the confirm guard and the
initial input seed all derive from it via the new helper.
Tests: giveawayUntilDate (5 cases) + earliestSelectableMinute (5 cases).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
feat: giveaway end-date validation + live, second-accurate date-picker minimum
Giveaway creation (Boosts via Gifts) allowed a past or out-of-window end date,
and the shared date/time picker's "Minimum send time" froze at popup-open so it
drifted into the past. Bound the giveaway end date to [now, now +
giveaway_period_max] like the official clients, and make the picker's minimum
track the clock to the second.
Giveaway end-date validation (boostsViaGifts):
- The expiration picker now passes minDate (today) + minTimeDate (now), so past
days/times are unselectable, plus a defense-in-depth submit guard that
re-validates via isGiveawayUntilDateValid before building the payload
(toast + shake if giveaway_period_max drifted after the date was picked).
- isGiveawayUntilDateValid (new pure helper): untilDate > now && untilDate <=
now + periodMax (seconds). Mirrors tdesktop ChooseDateTimeBox bounds
(.min = now, .max = now + giveawayPeriodMax) and its collect() guard.
- lang: BoostsViaGifts.InvalidEndDate.
Date-picker live minimum (datePicker, benefits every caller — giveaway,
suggested-post, poll end):
- minTimeDate was captured once at open: the caption showed a stale minimum, and
because the displayed floor is minute-granular while the hard guard compared
seconds, the shown minute itself became unselectable once its :00 had elapsed.
- earliestSelectableMinuteMs(minTime, now) (new, @helpers/date): ceil(max(minTime,
now)) up to the next full minute — never behind the clock, never offers a
minute already partly in the past, keeps the caption and Confirm-enable
consistent.
- A 1s clock ticks the minute bucket; the caption, the confirm guard and the
initial input seed all derive from it via the new helper.
Tests: giveawayUntilDate (5 cases) + earliestSelectableMinute (5 cases).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
🫡4
🫡2
telegramdesktop/tdesktop/dev • 20542a1 • 3 files, +20/-14
Added logs of source path when an unknown deleted message is re-added.
Related commit: 384f6d9a31.
telegramdesktop/tdesktop/dev • 0cf37dd • 4 files, +28/-1
Replaced gift button with call button for channel owner in info top bar.
telegramdesktop/tdesktop/dev • 550f623 • 1 files, +10/-1
Slightly improved trackpad scroll handling in profile flexible top bar.
telegramdesktop/tdesktop/dev • 16d9706 • 5 files, +111/-0
Added playback wave animation around voice message play button.
telegramdesktop/tdesktop/dev • 92e9644 • 1 files, +1/-2
Allowed swipe-to-reply gesture in channels.
telegramdesktop/tdesktop/dev • 9a38ab7 • 2 files, +4/-1
Fixed invisible small group call videos.
telegramdesktop/tdesktop/dev • c56fd45 • 1 files, +2/-2
Fixed repaint region for third column section animation.
telegramdesktop/tdesktop/dev • 969e84f • 1 files, +1/-0
Fixed clipped overlay buttons in group call QRhi viewport.
telegramdesktop/tdesktop/dev • 407d2cf • 4 files, +10/-0
Fixed new account intro opening in wrong window.
telegramdesktop/tdesktop/dev • df64efd • 6 files, +37/-9
Fixed back from new account intro focusing wrong window.
telegramdesktop/tdesktop/dev • 5d52a28 • 6 files, +66/-2
Added experimental option to keep unmuted unread dialogs on top.
telegramdesktop/tdesktop/dev • a74ee60 • 8 files, +297/-0
Added initial support of swipe-next-channel.
telegramdesktop/tdesktop/dev • 49396c4 • 4 files, +565/-12
Added growing-capsule swipe-next-channel pull indicator with userpic.
#tdesktop
Added logs of source path when an unknown deleted message is re-added.
Related commit: 384f6d9a31.
telegramdesktop/tdesktop/dev • 0cf37dd • 4 files, +28/-1
Replaced gift button with call button for channel owner in info top bar.
telegramdesktop/tdesktop/dev • 550f623 • 1 files, +10/-1
Slightly improved trackpad scroll handling in profile flexible top bar.
telegramdesktop/tdesktop/dev • 16d9706 • 5 files, +111/-0
Added playback wave animation around voice message play button.
telegramdesktop/tdesktop/dev • 92e9644 • 1 files, +1/-2
Allowed swipe-to-reply gesture in channels.
telegramdesktop/tdesktop/dev • 9a38ab7 • 2 files, +4/-1
Fixed invisible small group call videos.
telegramdesktop/tdesktop/dev • c56fd45 • 1 files, +2/-2
Fixed repaint region for third column section animation.
telegramdesktop/tdesktop/dev • 969e84f • 1 files, +1/-0
Fixed clipped overlay buttons in group call QRhi viewport.
telegramdesktop/tdesktop/dev • 407d2cf • 4 files, +10/-0
Fixed new account intro opening in wrong window.
telegramdesktop/tdesktop/dev • df64efd • 6 files, +37/-9
Fixed back from new account intro focusing wrong window.
telegramdesktop/tdesktop/dev • 5d52a28 • 6 files, +66/-2
Added experimental option to keep unmuted unread dialogs on top.
telegramdesktop/tdesktop/dev • a74ee60 • 8 files, +297/-0
Added initial support of swipe-next-channel.
telegramdesktop/tdesktop/dev • 49396c4 • 4 files, +565/-12
Added growing-capsule swipe-next-channel pull indicator with userpic.
#tdesktop
🫡3
telegramdesktop/tdesktop/dev • 7549185 • 7 files, +111/-14
Added bottom-up section slide for swipe-next-channel jump.
telegramdesktop/tdesktop/dev • 1b88612 • 2 files, +22/-10
Fixed stale truncated history view top bar title on peer name change.
telegramdesktop/tdesktop/dev • 792cc5c • 1 files, +18/-4
Fixed text recognition overlay rotation in QRhi media viewer.
telegramdesktop/tdesktop/dev • 2da46cb • 2 files, +52/-16
Added text recognition for image documents in media viewer.
telegramdesktop/tdesktop/dev • b9d6b07 • 9 files, +628/-17
Added recognized text selection and copying in overlay media viewer.
telegramdesktop/tdesktop/dev • 05cb1e4 • 3 files, +41/-4
Added blockquote and code block rendering to media viewer captions.
telegramdesktop/tdesktop/dev • d0dba2e • 7 files, +119/-23
Added text transfer between message field and gif caption box.
telegramdesktop/tdesktop/dev • d10e9ce • 8 files, +271/-17
Added alphabetical section headers to contacts list sorted by name.
telegramdesktop/tdesktop/dev • 076de2d • 6 files, +498/-0
Added alphabetical fast-scroll index strip to contacts box.
telegramdesktop/tdesktop/dev • b8e82e3 • 1 files, +8/-5
Fixed opening sections from the settings search results.
telegramdesktop/tdesktop/dev • 165d20a • 5 files, +106/-15
Added support of deep link copying to settings search results.
telegramdesktop/tdesktop/dev • c40446b • 2 files, +13/-4
[thanos] Fixed lost auto-scroll to bottom during animation.
telegramdesktop/tdesktop/dev • 0ef307f • 2 files, +29/-17
Added right-button clicks support to pinned bar.
#tdesktop
Added bottom-up section slide for swipe-next-channel jump.
telegramdesktop/tdesktop/dev • 1b88612 • 2 files, +22/-10
Fixed stale truncated history view top bar title on peer name change.
telegramdesktop/tdesktop/dev • 792cc5c • 1 files, +18/-4
Fixed text recognition overlay rotation in QRhi media viewer.
telegramdesktop/tdesktop/dev • 2da46cb • 2 files, +52/-16
Added text recognition for image documents in media viewer.
telegramdesktop/tdesktop/dev • b9d6b07 • 9 files, +628/-17
Added recognized text selection and copying in overlay media viewer.
telegramdesktop/tdesktop/dev • 05cb1e4 • 3 files, +41/-4
Added blockquote and code block rendering to media viewer captions.
telegramdesktop/tdesktop/dev • d0dba2e • 7 files, +119/-23
Added text transfer between message field and gif caption box.
telegramdesktop/tdesktop/dev • d10e9ce • 8 files, +271/-17
Added alphabetical section headers to contacts list sorted by name.
telegramdesktop/tdesktop/dev • 076de2d • 6 files, +498/-0
Added alphabetical fast-scroll index strip to contacts box.
telegramdesktop/tdesktop/dev • b8e82e3 • 1 files, +8/-5
Fixed opening sections from the settings search results.
telegramdesktop/tdesktop/dev • 165d20a • 5 files, +106/-15
Added support of deep link copying to settings search results.
telegramdesktop/tdesktop/dev • c40446b • 2 files, +13/-4
[thanos] Fixed lost auto-scroll to bottom during animation.
telegramdesktop/tdesktop/dev • 0ef307f • 2 files, +29/-17
Added right-button clicks support to pinned bar.
#tdesktop
🫡4👍1
telegramdesktop/tdesktop/dev • 1ab8597 • 6 files, +106/-0
Added reverse iteration over pinned messages on right click.
telegramdesktop/tdesktop/dev • a1f3ee5 • 15 files, +193/-5
Added tooltip above link for hidden forwarded sender.
telegramdesktop/tdesktop/dev • e0504dd • 7 files, +763/-1121
[ai] Attempt to automate testing changes.
telegramdesktop/tdesktop/dev • 9ca694f • 1 files, +51/-51
Rename currency-balance and post-suggestion strings to Grams.
telegramdesktop/tdesktop/dev • 4089646 • 6 files, +41/-32
Refresh and unify the TON diamond vector icons.
telegramdesktop/tdesktop/dev • c9ed5ef • 1 files, +1/-1
Update lib_base.
telegramdesktop/tdesktop/dev • ee39aa3 • 4 files, +4/-23
Switch to Qt's queued invoker
#tdesktop
Added reverse iteration over pinned messages on right click.
telegramdesktop/tdesktop/dev • a1f3ee5 • 15 files, +193/-5
Added tooltip above link for hidden forwarded sender.
telegramdesktop/tdesktop/dev • e0504dd • 7 files, +763/-1121
[ai] Attempt to automate testing changes.
telegramdesktop/tdesktop/dev • 9ca694f • 1 files, +51/-51
Rename currency-balance and post-suggestion strings to Grams.
telegramdesktop/tdesktop/dev • 4089646 • 6 files, +41/-32
Refresh and unify the TON diamond vector icons.
telegramdesktop/tdesktop/dev • c9ed5ef • 1 files, +1/-1
Update lib_base.
telegramdesktop/tdesktop/dev • ee39aa3 • 4 files, +4/-23
Switch to Qt's queued invoker
#tdesktop
🫡4
telegramdesktop/tdesktop/dev • 156c8e7 • 4 files, +21/-11
Use new base::option::changes in a few old options
telegramdesktop/tdesktop/dev • 1520a81 • 7 files, +7/-7
Bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-version: '7'
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
#tdesktop
Use new base::option::changes in a few old options
telegramdesktop/tdesktop/dev • 1520a81 • 7 files, +7/-7
Bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-version: '7'
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
#tdesktop
🫡3💅1
telegramdesktop/tdesktop/dev • 1d17852 • 1 files, +1/-1
Return hinting for math formula fonts.
telegramdesktop/tdesktop/dev • c8ef31a • 4 files, +18/-26
Fix rich pages in scheduled / shorcuts sending.
telegramdesktop/tdesktop/dev • 54a7fd0 • 9 files, +101/-24
Use correct table entities limits.
telegramdesktop/tdesktop/dev • b01d375 • 1 files, +1/-1
Add some more math formula guardrails.
telegramdesktop/tdesktop/dev • fa9b926 • 5 files, +44/-48
Make InputField menu extension with hooks.
telegramdesktop/tdesktop/dev • 8997785 • 5 files, +81/-31
Enable jump-to-date in forum search.
telegramdesktop/tdesktop/dev • 2e54c33 • 1 files, +0/-2
Fix build with MSVC.
#tdesktop
Return hinting for math formula fonts.
telegramdesktop/tdesktop/dev • c8ef31a • 4 files, +18/-26
Fix rich pages in scheduled / shorcuts sending.
telegramdesktop/tdesktop/dev • 54a7fd0 • 9 files, +101/-24
Use correct table entities limits.
telegramdesktop/tdesktop/dev • b01d375 • 1 files, +1/-1
Add some more math formula guardrails.
telegramdesktop/tdesktop/dev • fa9b926 • 5 files, +44/-48
Make InputField menu extension with hooks.
telegramdesktop/tdesktop/dev • 8997785 • 5 files, +81/-31
Enable jump-to-date in forum search.
telegramdesktop/tdesktop/dev • 2e54c33 • 1 files, +0/-2
Fix build with MSVC.
#tdesktop
🫡3
Ajaxy/telegram-tt/master • f45c51b • 3 files, +4/-4
12.0.30
Ajaxy/telegram-tt/master • cf4b734 • 1 files, +2/-2
Chat Extra: Fix unofficial security risk block (#6977)
Ajaxy/telegram-tt/master • 0f2a87c • 1 files, +5/-2
Poll: Disable sending quiz with 1 option (#6980)
Ajaxy/telegram-tt/master • 7ddb46e • 3 files, +20/-1
Bot Forum: Change botoforum info button string (#6979)
Ajaxy/telegram-tt/master • 33ef4fb • 1 files, +14/-2
Chat List: Fix messege appearance after chat deletion (#6978)
Ajaxy/telegram-tt/master • 98e7644 • 115 files, +1925/-5812
Migrate to Vite (#6973)
Ajaxy/telegram-tt/master • 6aa405f • 6 files, +1654/-156
[CI] Bundle Stats: Add Vite PR reporting (#6976)
Ajaxy/telegram-tt/master • f50eefa • 7 files, +4305/-6728
Tests: Migrate Jest suite to Vitest (#6981)
Ajaxy/telegram-tt/master • bac8e43 • 2 files, +73/-58
PR Stats: Change format (#6983)
Ajaxy/telegram-tt/master • af5008b • 26 files, +544/-64
Reaction: Support reaction deletion (#6982)
Ajaxy/telegram-tt/master • 754be91 • 4 files, +90/-23
Vite: Show workers in stats (#7010)
Ajaxy/telegram-tt/master • 194f199 • 60 files, +836/-745
Remove Buffer dependency (#7009)
Ajaxy/telegram-tt/master • 49f82ed • 1 files, +1/-0
Sync: Fix outdated global error (#7014)
Ajaxy/telegram-tt/master • 38e4b77 • 5 files, +15/-29
Safari: Fix several problems (#7015)
Ajaxy/telegram-tt/master • 9f01ca8 • 11 files, +210/-195
GifButton: Move to SCSS Modules (#7013)
#webz
12.0.30
Ajaxy/telegram-tt/master • cf4b734 • 1 files, +2/-2
Chat Extra: Fix unofficial security risk block (#6977)
Ajaxy/telegram-tt/master • 0f2a87c • 1 files, +5/-2
Poll: Disable sending quiz with 1 option (#6980)
Ajaxy/telegram-tt/master • 7ddb46e • 3 files, +20/-1
Bot Forum: Change botoforum info button string (#6979)
Ajaxy/telegram-tt/master • 33ef4fb • 1 files, +14/-2
Chat List: Fix messege appearance after chat deletion (#6978)
Ajaxy/telegram-tt/master • 98e7644 • 115 files, +1925/-5812
Migrate to Vite (#6973)
Ajaxy/telegram-tt/master • 6aa405f • 6 files, +1654/-156
[CI] Bundle Stats: Add Vite PR reporting (#6976)
Ajaxy/telegram-tt/master • f50eefa • 7 files, +4305/-6728
Tests: Migrate Jest suite to Vitest (#6981)
Ajaxy/telegram-tt/master • bac8e43 • 2 files, +73/-58
PR Stats: Change format (#6983)
Ajaxy/telegram-tt/master • af5008b • 26 files, +544/-64
Reaction: Support reaction deletion (#6982)
Ajaxy/telegram-tt/master • 754be91 • 4 files, +90/-23
Vite: Show workers in stats (#7010)
Ajaxy/telegram-tt/master • 194f199 • 60 files, +836/-745
Remove Buffer dependency (#7009)
Ajaxy/telegram-tt/master • 49f82ed • 1 files, +1/-0
Sync: Fix outdated global error (#7014)
Ajaxy/telegram-tt/master • 38e4b77 • 5 files, +15/-29
Safari: Fix several problems (#7015)
Ajaxy/telegram-tt/master • 9f01ca8 • 11 files, +210/-195
GifButton: Move to SCSS Modules (#7013)
#webz
🫡2
Ajaxy/telegram-tt/master • 48bf95b • 1 files, +30/-15
Forum: Load topics when listed ids are missing (#7016)
Ajaxy/telegram-tt/master • bee4c3a • 1 files, +2/-2
Left Main: Fix chat list with tabs layout (#7017)
Ajaxy/telegram-tt/master • 34474e7 • 6 files, +54/-8
Profile: Implement round corners in profile media (#7019)
Ajaxy/telegram-tt/master • f23a4ba • 3 files, +594/-597
Agents: Move CLAUDE.md to AGENTS.md (#7012)
Ajaxy/telegram-tt/master • d419be9 • 11 files, +31/-43
Sticker: Fix blinking on send (#7011)
Ajaxy/telegram-tt/master • 0b47bec • 2 files, +2/-2
Audio: Fix audio message transcribing button color (#7021)
Ajaxy/telegram-tt/master • b52deb1 • 3 files, +169/-12
Checklists: Fix emoji display for checklists in chat list (#7018)
Ajaxy/telegram-tt/master • 173280d • 4 files, +14/-6
Summary: Prevent hiding on error (#7023)
Ajaxy/telegram-tt/master • 764aaa4 • 2 files, +0/-508
Remove unused dependencies (#7024)
Ajaxy/telegram-tt/master • ccd4bdc • 11 files, +0/-88
Remove unused functions (#7025)
Ajaxy/telegram-tt/master • 9c285e7 • 41 files, +156/-4438
Statistics: Upgrade to lovely-chart v2 npm package (#7027)
Ajaxy/telegram-tt/master • 54185fd • 7 files, +168/-139
Statistics: Restore lazy lovely-chart loading (#7028)
Ajaxy/telegram-tt/master • 954f0f1 • 121 files, +7622/-759
Support Rich Messages & Instant View (#6963)
Ajaxy/telegram-tt/master • 0df85b8 • 4 files, +14/-13
Lang: Updates from platform (#7030)
#webz
Forum: Load topics when listed ids are missing (#7016)
Ajaxy/telegram-tt/master • bee4c3a • 1 files, +2/-2
Left Main: Fix chat list with tabs layout (#7017)
Ajaxy/telegram-tt/master • 34474e7 • 6 files, +54/-8
Profile: Implement round corners in profile media (#7019)
Ajaxy/telegram-tt/master • f23a4ba • 3 files, +594/-597
Agents: Move CLAUDE.md to AGENTS.md (#7012)
Ajaxy/telegram-tt/master • d419be9 • 11 files, +31/-43
Sticker: Fix blinking on send (#7011)
Ajaxy/telegram-tt/master • 0b47bec • 2 files, +2/-2
Audio: Fix audio message transcribing button color (#7021)
Ajaxy/telegram-tt/master • b52deb1 • 3 files, +169/-12
Checklists: Fix emoji display for checklists in chat list (#7018)
Ajaxy/telegram-tt/master • 173280d • 4 files, +14/-6
Summary: Prevent hiding on error (#7023)
Ajaxy/telegram-tt/master • 764aaa4 • 2 files, +0/-508
Remove unused dependencies (#7024)
Ajaxy/telegram-tt/master • ccd4bdc • 11 files, +0/-88
Remove unused functions (#7025)
Ajaxy/telegram-tt/master • 9c285e7 • 41 files, +156/-4438
Statistics: Upgrade to lovely-chart v2 npm package (#7027)
Ajaxy/telegram-tt/master • 54185fd • 7 files, +168/-139
Statistics: Restore lazy lovely-chart loading (#7028)
Ajaxy/telegram-tt/master • 954f0f1 • 121 files, +7622/-759
Support Rich Messages & Instant View (#6963)
Ajaxy/telegram-tt/master • 0df85b8 • 4 files, +14/-13
Lang: Updates from platform (#7030)
#webz
🫡2
Ajaxy/telegram-tt/master • 68478f6 • 45 files, +615/-401
Redesign: Redesign Left panel and Middle header (#6971)
Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
Ajaxy/telegram-tt/master • e1f2988 • 1 files, +1/-1
Add .kml / .kmz as potentially IP-revealing. (#7031)
Ajaxy/telegram-tt/master • 73ff150 • 10 files, +76/-63
Rich Messages: Small fixes (#7032)
Ajaxy/telegram-tt/master • 1fafd66 • 1 files, +2/-2
Reactor List Modal: Fix button hover (#7034)
Ajaxy/telegram-tt/master • 1ead092 • 1 files, +2/-1
Topic: Fix error on min constructor (#7022)
Ajaxy/telegram-tt/master • ee72f71 • 10 files, +47/-33
Media Viewer: Fix ghost animation clipping (#7037)
Ajaxy/telegram-tt/master • 4cd5e76 • 8 files, +368/-6
Profile: Show long titles in line (#7020)
Ajaxy/telegram-tt/master • 3d85e97 • 12 files, +96/-99
Middle Column: Support mask gradient (#7036)
Ajaxy/telegram-tt/master • 54f4693 • 27 files, +153/-74
Toncoin: Rebrand token to Gram
Rename the Toncoin token to "Gram" across user-facing strings and
localization keys, and add the new colored coin icon. The TON blockchain,
the `TON` currency code and protocol/API entities are left intact.
- Update display strings to use the name "Gram"
- Rename token localization keys (`*Ton*` -> `*Gram*`)
- Add `GramIcon` (colored SVG inline, PNG in Settings, `currentColor`
monochrome variant for amount-colored transactions)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ajaxy/telegram-tt/master • 6f02c7f • 1 files, +2/-1
Rich Message: Fix more button hover color (#7038)
Ajaxy/telegram-tt/master • bada5e5 • 2 files, +20/-0
Modal: Fix Esc handling (#7043)
Ajaxy/telegram-tt/master • b9a888f • 18 files, +452/-493
Mini Apps: Fix theme color parsing (#7029)
Ajaxy/telegram-tt/master • b7764bc • 17 files, +51/-30
Search: Fix search input position in chat list (#7040)
#webz
Redesign: Redesign Left panel and Middle header (#6971)
Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
Ajaxy/telegram-tt/master • e1f2988 • 1 files, +1/-1
Add .kml / .kmz as potentially IP-revealing. (#7031)
Ajaxy/telegram-tt/master • 73ff150 • 10 files, +76/-63
Rich Messages: Small fixes (#7032)
Ajaxy/telegram-tt/master • 1fafd66 • 1 files, +2/-2
Reactor List Modal: Fix button hover (#7034)
Ajaxy/telegram-tt/master • 1ead092 • 1 files, +2/-1
Topic: Fix error on min constructor (#7022)
Ajaxy/telegram-tt/master • ee72f71 • 10 files, +47/-33
Media Viewer: Fix ghost animation clipping (#7037)
Ajaxy/telegram-tt/master • 4cd5e76 • 8 files, +368/-6
Profile: Show long titles in line (#7020)
Ajaxy/telegram-tt/master • 3d85e97 • 12 files, +96/-99
Middle Column: Support mask gradient (#7036)
Ajaxy/telegram-tt/master • 54f4693 • 27 files, +153/-74
Toncoin: Rebrand token to Gram
Rename the Toncoin token to "Gram" across user-facing strings and
localization keys, and add the new colored coin icon. The TON blockchain,
the `TON` currency code and protocol/API entities are left intact.
- Update display strings to use the name "Gram"
- Rename token localization keys (`*Ton*` -> `*Gram*`)
- Add `GramIcon` (colored SVG inline, PNG in Settings, `currentColor`
monochrome variant for amount-colored transactions)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ajaxy/telegram-tt/master • 6f02c7f • 1 files, +2/-1
Rich Message: Fix more button hover color (#7038)
Ajaxy/telegram-tt/master • bada5e5 • 2 files, +20/-0
Modal: Fix Esc handling (#7043)
Ajaxy/telegram-tt/master • b9a888f • 18 files, +452/-493
Mini Apps: Fix theme color parsing (#7029)
Ajaxy/telegram-tt/master • b7764bc • 17 files, +51/-30
Search: Fix search input position in chat list (#7040)
#webz
🫡2
Ajaxy/telegram-tt/master • 5a78bb0 • 7 files, +275/-76
Calls: Migrate methods to object parameters (#7044)
Ajaxy/telegram-tt/master • 248690e • 1 files, +3/-0
Inline Bots: Support rich content (#7046)
Ajaxy/telegram-tt/master • 7b64d1b • 7 files, +15/-8
Styles: Redesign shadows (#7039)
Ajaxy/telegram-tt/master • 37480bc • 1 files, +1/-1
Folders: Disable left tab folders grey out in inactive state (#7041)
Ajaxy/telegram-tt/master • 7a618df • 2 files, +4/-2
Chat Invite Modal: Redesign (#7047)
Ajaxy/telegram-tt/master • 9602b82 • 11 files, +96/-89
Revert "Middle Column: Support mask gradient (#7036)" (#7049)
Ajaxy/telegram-tt/master • 110eb87 • 1 files, +1/-1
Files: Fix progress spinner height init (#7052)
Ajaxy/telegram-tt/master • bcdf2a4 • 2 files, +1/-5
Contact Modal: Fix top margin for help text in New Contact Modal (#7053)
Ajaxy/telegram-tt/master • 84ee447 • 9 files, +68/-6
Message: Show edited if needed (#7045)
Ajaxy/telegram-tt/master • 3f795db • 31 files, +934/-157
Management: Support guard bots (#7035)
Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
Ajaxy/telegram-tt/master • 0b401d4 • 1 files, +4/-0
Quick Preview Modal: Fix layout (#7054)
Ajaxy/telegram-tt/master • fc777e6 • 300 files, +21565/-1530
[Build]
#webz
Calls: Migrate methods to object parameters (#7044)
Ajaxy/telegram-tt/master • 248690e • 1 files, +3/-0
Inline Bots: Support rich content (#7046)
Ajaxy/telegram-tt/master • 7b64d1b • 7 files, +15/-8
Styles: Redesign shadows (#7039)
Ajaxy/telegram-tt/master • 37480bc • 1 files, +1/-1
Folders: Disable left tab folders grey out in inactive state (#7041)
Ajaxy/telegram-tt/master • 7a618df • 2 files, +4/-2
Chat Invite Modal: Redesign (#7047)
Ajaxy/telegram-tt/master • 9602b82 • 11 files, +96/-89
Revert "Middle Column: Support mask gradient (#7036)" (#7049)
Ajaxy/telegram-tt/master • 110eb87 • 1 files, +1/-1
Files: Fix progress spinner height init (#7052)
Ajaxy/telegram-tt/master • bcdf2a4 • 2 files, +1/-5
Contact Modal: Fix top margin for help text in New Contact Modal (#7053)
Ajaxy/telegram-tt/master • 84ee447 • 9 files, +68/-6
Message: Show edited if needed (#7045)
Ajaxy/telegram-tt/master • 3f795db • 31 files, +934/-157
Management: Support guard bots (#7035)
Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
Ajaxy/telegram-tt/master • 0b401d4 • 1 files, +4/-0
Quick Preview Modal: Fix layout (#7054)
Ajaxy/telegram-tt/master • fc777e6 • 300 files, +21565/-1530
[Build]
#webz
🫡2
morethanwords/tweb/master • 11fde94 • 6 files, +38/-13
Misc non-sticker fixes: folders sidebar notification badge, media spoiler cover, reactions no-select, CLAUDE.md note, langPack sync
- folders sidebar: reactive total notification count on the menu button (Accessor instead of cloning the toolbar count element)
- media: keep sensitive-content cover clickable during reveal so a repeat click is ignored instead of falling through
- reactions: disable text selection
- CLAUDE.md: add commit-policy note
- regenerate langPack.strings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#webk
Misc non-sticker fixes: folders sidebar notification badge, media spoiler cover, reactions no-select, CLAUDE.md note, langPack sync
- folders sidebar: reactive total notification count on the menu button (Accessor instead of cloning the toolbar count element)
- media: keep sensitive-content cover clickable during reveal so a repeat click is ignored instead of falling through
- reactions: disable text selection
- CLAUDE.md: add commit-policy note
- regenerate langPack.strings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • 550b488 • 3 files, +195/-68
fix: format a shared contact's phone under the viewer's country, not a misread foreign code
Bug (bugs.telegram.org #30681): a phone number in an in-chat shared contact
embed is displayed with the wrong country code when the stored vCard number does
not explicitly contain its dialing prefix. The leading national/city digits are
shown as if they were the country code (does not happen on Android).
Root cause: formatPhoneNumber (src/helpers/formatPhoneNumber.ts) strips every
non-digit and then ALWAYS resolves a country by greedily matching the longest
numeric prefix against every known country code. For a number stored without its
dialing prefix (e.g. an Indonesian national mobile "8123456789"), the leading
"81" collide with Japan (+81): "81" is stripped as the country code and the rest
is regrouped under it, so "8123456789" rendered as "+81 23 4567 89" in the
contact bubble (src/components/chat/bubbles.ts, messageMediaContact). Registered
users (user.phone) always carry their country code, so this only surfaces for the
arbitrary vCard phone_number of a shared contact.
Fix: mirror Android's PhoneFormat, which biases an unprefixed number to the
viewer's own country instead of guessing one from the leading digits.
- formatPhoneNumber takes an opt-in `defaultCountryCode` (the viewer's own
calling code). When the input has no explicit '+' AND its digits do not already
start with that code, the number is treated as a national number of the
viewer's country and grouped with that country's national patterns; `code` is
returned undefined so the caller omits the '+'. Otherwise (explicit '+', or
digits already starting with the viewer's code) the existing greedy detection
runs unchanged, so a registered user's fully-qualified phone and an
international "+..." contact keep their grouping. The grouping logic is factored
into a shared applyPattern() helper.
- bubbles.ts reads the viewer's country code once from the warm main-thread user
cache (apiManagerProxy.getUser(rootScope.myId)) — synchronous, no chat-open
round-trip — and prefixes '+' only when a country code is present. Every other
formatPhoneNumber caller (profile, search, payments, telInputField) is
unchanged.
Result: a same-country contact regains correct grouping (e.g. "+7 999 123-45-67"),
the #30681 number is grouped under the viewer's country with no false foreign code
(Indonesian "8123456789" -> "812 3456789", never "+81 ..."), and a number that
does carry its own code still renders "+CC ...".
Test (src/tests/formatPhoneNumber.test.ts): pure-unit Vitest with two countries
whose leading digits overlap (JP +81, ID +62). Asserts the #30681 input is
grouped under the viewer (+62) with code undefined and never attributed to Japan;
plus the own-country international "+CC", explicit-'+' detection, the
foreign-under-viewer no-"+CC" case, and the unchanged default parse. 6/6 green
(one reproduces the bug before the fix).
Gates: tsc --noEmit 0 errors; eslint 0 on all changed files; vitest 6/6 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
fix: format a shared contact's phone under the viewer's country, not a misread foreign code
Bug (bugs.telegram.org #30681): a phone number in an in-chat shared contact
embed is displayed with the wrong country code when the stored vCard number does
not explicitly contain its dialing prefix. The leading national/city digits are
shown as if they were the country code (does not happen on Android).
Root cause: formatPhoneNumber (src/helpers/formatPhoneNumber.ts) strips every
non-digit and then ALWAYS resolves a country by greedily matching the longest
numeric prefix against every known country code. For a number stored without its
dialing prefix (e.g. an Indonesian national mobile "8123456789"), the leading
"81" collide with Japan (+81): "81" is stripped as the country code and the rest
is regrouped under it, so "8123456789" rendered as "+81 23 4567 89" in the
contact bubble (src/components/chat/bubbles.ts, messageMediaContact). Registered
users (user.phone) always carry their country code, so this only surfaces for the
arbitrary vCard phone_number of a shared contact.
Fix: mirror Android's PhoneFormat, which biases an unprefixed number to the
viewer's own country instead of guessing one from the leading digits.
- formatPhoneNumber takes an opt-in `defaultCountryCode` (the viewer's own
calling code). When the input has no explicit '+' AND its digits do not already
start with that code, the number is treated as a national number of the
viewer's country and grouped with that country's national patterns; `code` is
returned undefined so the caller omits the '+'. Otherwise (explicit '+', or
digits already starting with the viewer's code) the existing greedy detection
runs unchanged, so a registered user's fully-qualified phone and an
international "+..." contact keep their grouping. The grouping logic is factored
into a shared applyPattern() helper.
- bubbles.ts reads the viewer's country code once from the warm main-thread user
cache (apiManagerProxy.getUser(rootScope.myId)) — synchronous, no chat-open
round-trip — and prefixes '+' only when a country code is present. Every other
formatPhoneNumber caller (profile, search, payments, telInputField) is
unchanged.
Result: a same-country contact regains correct grouping (e.g. "+7 999 123-45-67"),
the #30681 number is grouped under the viewer's country with no false foreign code
(Indonesian "8123456789" -> "812 3456789", never "+81 ..."), and a number that
does carry its own code still renders "+CC ...".
Test (src/tests/formatPhoneNumber.test.ts): pure-unit Vitest with two countries
whose leading digits overlap (JP +81, ID +62). Asserts the #30681 input is
grouped under the viewer (+62) with code undefined and never attributed to Japan;
plus the own-country international "+CC", explicit-'+' detection, the
foreign-under-viewer no-"+CC" case, and the unchanged default parse. 6/6 green
(one reproduces the bug before the fix).
Gates: tsc --noEmit 0 errors; eslint 0 on all changed files; vitest 6/6 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#webk
🫡3
telegramdesktop/tdesktop/dev • ef76b58 • 6 files, +45/-29
Fix reaction preview crash in stories.
telegramdesktop/tdesktop/dev • 2622f02 • 5 files, +487/-98
Improve context of messages copied to clipboard.
#tdesktop
Fix reaction preview crash in stories.
telegramdesktop/tdesktop/dev • 2622f02 • 5 files, +487/-98
Improve context of messages copied to clipboard.
#tdesktop
🫡3
telegramdesktop/tdesktop/dev • 9f61a9f • 2 files, +4/-2
Work around freeze on Windows on ARM.
Fixes #30867.
#tdesktop
Work around freeze on Windows on ARM.
Fixes #30867.
#tdesktop
🫡3