UnigramDev/Unigram/record-session • 951c903 • 9 files, +1363/-1157
Give a recording an owner
ChatRecordSession owns the state and the engine, one per button rather than a
thread-static singleton shared by every chat and story composer on the thread.
Seven booleans and an interface state become three named states, and the tri-state
stop that neither sent nor discarded becomes Cancel and Complete. Permissions go
through MediaDevicePermissions, so granting access no longer costs the first press.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Give a recording an owner
ChatRecordSession owns the state and the engine, one per button rather than a
thread-static singleton shared by every chat and story composer on the thread.
Seven booleans and an interface state become three named states, and the tri-state
stop that neither sent nor discarded becomes Cancel and Complete. Permissions go
through MediaDevicePermissions, so granting access no longer costs the first press.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
TGX-Android/Telegram-X/main • 70c21ad • 4 files, +56/-3
Add flavor-specific `registerReceiver`
TGX-Android/Telegram-X/main • 8bbfcaa • 1 files, +1/-1
Version bump to `1804`
#tgxandroid
Add flavor-specific `registerReceiver`
TGX-Android/Telegram-X/main • 8bbfcaa • 1 files, +1/-1
Version bump to `1804`
#tgxandroid
🫡3
UnigramDev/Unigram/develop • 14060e3 • 9 files, +1363/-1157
Give a recording an owner
ChatRecordSession owns the state and the engine, one per button rather than a
thread-static singleton shared by every chat and story composer on the thread.
Seven booleans and an interface state become three named states, and the tri-state
stop that neither sent nor discarded becomes Cancel and Complete. Permissions go
through MediaDevicePermissions, so granting access no longer costs the first press.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Give a recording an owner
ChatRecordSession owns the state and the engine, one per button rather than a
thread-static singleton shared by every chat and story composer on the thread.
Seven booleans and an interface state become three named states, and the tri-state
stop that neither sent nor discarded becomes Cancel and Complete. Permissions go
through MediaDevicePermissions, so granting access no longer costs the first press.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
UnigramDev/Unigram/record-video-size • 32f72e4 • 4 files, +80/-9
Stop a video message at sixty seconds
The bar drew a sixty second ring and nothing ended the recording when it filled.
The limit is one constant now, read by both the ring and the check, and measured
off Elapsed so pausing pauses it.
UnigramDev/Unigram/record-video-size • 7d3c29a • 4 files, +81/-21
Record a video message at the size it is sent at
A sixty second video message was written at whatever the camera gives, 1080p on
most, and then re-encoded to 384 square when sent. The encoding profile now takes
the camera's own aspect scaled so the short side is the video note length, which
leaves the send-time pass a crop.
The camera's format is untouched, so the sharing mode still allows it.
#unigram
Stop a video message at sixty seconds
The bar drew a sixty second ring and nothing ended the recording when it filled.
The limit is one constant now, read by both the ring and the check, and measured
off Elapsed so pausing pauses it.
UnigramDev/Unigram/record-video-size • 7d3c29a • 4 files, +81/-21
Record a video message at the size it is sent at
A sixty second video message was written at whatever the camera gives, 1080p on
most, and then re-encoded to 384 square when sent. The encoding profile now takes
the camera's own aspect scaled so the short side is the video note length, which
leaves the send-time pass a crop.
The camera's format is untouched, so the sharing mode still allows it.
#unigram
UnigramDev/Unigram/develop • 2a261bf • 1 files, +14/-26
Simplify text block pointer logic
UnigramDev/Unigram/develop • 29db484 • 1 files, +19/-4
Improve format reset
UnigramDev/Unigram/develop • 064b3a1 • 3 files, +39/-16
Fix modern debug build
UnigramDev/Unigram/develop • b230601 • 1 files, +1/-1
Fix wrong field reference
UnigramDev/Unigram/develop • 67a5090 • 3 files, +271/-26
Hook fail fast methods for better error reporting
#unigram
Simplify text block pointer logic
UnigramDev/Unigram/develop • 29db484 • 1 files, +19/-4
Improve format reset
UnigramDev/Unigram/develop • 064b3a1 • 3 files, +39/-16
Fix modern debug build
UnigramDev/Unigram/develop • b230601 • 1 files, +1/-1
Fix wrong field reference
UnigramDev/Unigram/develop • 67a5090 • 3 files, +271/-26
Hook fail fast methods for better error reporting
#unigram
UnigramDev/Unigram/develop • 98ccd91 • 4 files, +80/-9
Stop a video message at sixty seconds
The bar drew a sixty second ring and nothing ended the recording when it filled.
The limit is one constant now, read by both the ring and the check, and measured
off Elapsed so pausing pauses it.
UnigramDev/Unigram/develop • f2ed087 • 4 files, +81/-21
Record a video message at the size it is sent at
A sixty second video message was written at whatever the camera gives, 1080p on
most, and then re-encoded to 384 square when sent. The encoding profile now takes
the camera's own aspect scaled so the short side is the video note length, which
leaves the send-time pass a crop.
The camera's format is untouched, so the sharing mode still allows it.
#unigram
Stop a video message at sixty seconds
The bar drew a sixty second ring and nothing ended the recording when it filled.
The limit is one constant now, read by both the ring and the check, and measured
off Elapsed so pausing pauses it.
UnigramDev/Unigram/develop • f2ed087 • 4 files, +81/-21
Record a video message at the size it is sent at
A sixty second video message was written at whatever the camera gives, 1080p on
most, and then re-encoded to 384 square when sent. The encoding profile now takes
the camera's own aspect scaled so the short side is the video note length, which
leaves the send-time pass a crop.
The camera's format is untouched, so the sharing mode still allows it.
#unigram
UnigramDev/Unigram/record-start-on-press • 131d28b • 1 files, +19/-0
Note the sixty-second pause and message drafts as todo
The official apps stop at sixty seconds and show the pause UI rather than
sending, which 4.3 does not, and they keep an unsent recording as a draft,
which Unigram has no notion of.
UnigramDev/Unigram/record-start-on-press • b160874 • 4 files, +185/-38
Open the recording device on the press, not after it
The 300 ms timer that tells a hold from a tap to switch mode used to run
before device init rather than during it. A session now starts unseen and is
committed when the timer ticks, or thrown away when it doesn't, so nothing
the user can see or hear happens before that. A press only warms up where
access has already been granted, and so can never raise a consent prompt.
#unigram
Note the sixty-second pause and message drafts as todo
The official apps stop at sixty seconds and show the pause UI rather than
sending, which 4.3 does not, and they keep an unsent recording as a draft,
which Unigram has no notion of.
UnigramDev/Unigram/record-start-on-press • b160874 • 4 files, +185/-38
Open the recording device on the press, not after it
The 300 ms timer that tells a hold from a tap to switch mode used to run
before device init rather than during it. A session now starts unseen and is
committed when the timer ticks, or thrown away when it doesn't, so nothing
the user can see or hear happens before that. A press only warms up where
access has already been granted, and so can never raise a consent prompt.
#unigram
UnigramDev/Unigram/record-start-on-press • 45da018 • 1 files, +4/-0
Note that the sixty-second stop cannot be resumed from
The limit reuses the pause UI, but a video message gets no more time, so the
pause button has to go rather than offer its resume glyph.
#unigram
Note that the sixty-second stop cannot be resumed from
The limit reuses the pause UI, but a video message gets no more time, so the
pause button has to go rather than offer its resume glyph.
#unigram
Ajaxy/telegram-tt/master • 0e76735 • 3 files, +4/-4
12.0.39
Ajaxy/telegram-tt/master • 15d3e95 • 10 files, +726/-62
Layer 228 (#7109)
Ajaxy/telegram-tt/master • 8c997fe • 66 files, +1698/-303
Support Ephemeral Messages (#7110)
Co-authored-by: Nikolay Karavaev <47010047+maxdestor@users.noreply.github.com>
Co-authored-by: Dmitry Kabanov <153344039+dmitrykabanovdev@users.noreply.github.com>
Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
Ajaxy/telegram-tt/master • f5032b3 • 47 files, +861/-40
Support global Auto Delete (#7118)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Ajaxy/telegram-tt/master • 575bf0a • 14 files, +272/-196
Story Composer: Redesign (#7126)
Ajaxy/telegram-tt/master • 96a9411 • 10 files, +1095/-1069
Profile Settings: Add new icons (#7136)
Ajaxy/telegram-tt/master • 280e006 • 37 files, +574/-771
Media: Use responsive styling (#7132)
Ajaxy/telegram-tt/master • f222d66 • 44 files, +1751/-110
Support Communities (#7101)
Ajaxy/telegram-tt/master • d1e1618 • 41 files, +1437/-510
Rich Content: Support copy (#7127)
Ajaxy/telegram-tt/master • 91ad760 • 1 files, +9/-10
CI: Skip build on failed version check (#7211)
Ajaxy/telegram-tt/master • 5bb07f5 • 1 files, +1/-2
Chat Report Pane: Fix history deletion in private chats (#7212)
Ajaxy/telegram-tt/master • cbd8f1f • 1 files, +1/-1
Lovely Chart: Fix shifted hover dot (#7214)
Ajaxy/telegram-tt/master • 8c63d46 • 1 files, +7/-2
Stories: Fix long names in stories header (#7215)
Ajaxy/telegram-tt/master • d616b2b • 2 files, +8/-4
Chat: Fix infinite loading on new user conversation(#7213)
#webz
12.0.39
Ajaxy/telegram-tt/master • 15d3e95 • 10 files, +726/-62
Layer 228 (#7109)
Ajaxy/telegram-tt/master • 8c997fe • 66 files, +1698/-303
Support Ephemeral Messages (#7110)
Co-authored-by: Nikolay Karavaev <47010047+maxdestor@users.noreply.github.com>
Co-authored-by: Dmitry Kabanov <153344039+dmitrykabanovdev@users.noreply.github.com>
Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
Ajaxy/telegram-tt/master • f5032b3 • 47 files, +861/-40
Support global Auto Delete (#7118)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Ajaxy/telegram-tt/master • 575bf0a • 14 files, +272/-196
Story Composer: Redesign (#7126)
Ajaxy/telegram-tt/master • 96a9411 • 10 files, +1095/-1069
Profile Settings: Add new icons (#7136)
Ajaxy/telegram-tt/master • 280e006 • 37 files, +574/-771
Media: Use responsive styling (#7132)
Ajaxy/telegram-tt/master • f222d66 • 44 files, +1751/-110
Support Communities (#7101)
Ajaxy/telegram-tt/master • d1e1618 • 41 files, +1437/-510
Rich Content: Support copy (#7127)
Ajaxy/telegram-tt/master • 91ad760 • 1 files, +9/-10
CI: Skip build on failed version check (#7211)
Ajaxy/telegram-tt/master • 5bb07f5 • 1 files, +1/-2
Chat Report Pane: Fix history deletion in private chats (#7212)
Ajaxy/telegram-tt/master • cbd8f1f • 1 files, +1/-1
Lovely Chart: Fix shifted hover dot (#7214)
Ajaxy/telegram-tt/master • 8c63d46 • 1 files, +7/-2
Stories: Fix long names in stories header (#7215)
Ajaxy/telegram-tt/master • d616b2b • 2 files, +8/-4
Chat: Fix infinite loading on new user conversation(#7213)
#webz
Ajaxy/telegram-tt/master • 03fb5b8 • 13 files, +98/-30
Message: Add special copy options (#7219)
Ajaxy/telegram-tt/master • 665d928 • 5 files, +38/-27
Localization: Regular updates (#7221)
Ajaxy/telegram-tt/master • f564a63 • 3 files, +127/-90
Community: Follow-up (#7220)
Ajaxy/telegram-tt/master • ba0a4a5 • 8 files, +30/-4
Premium Modal: Add missing premium features to the list (#7217)
Ajaxy/telegram-tt/master • e474e0c • 1 files, +2/-0
Settings: Fix star style (#7222)
Ajaxy/telegram-tt/master • d7d43d8 • 2 files, +28/-10
Settings: Fix blinking in Auto-Delete settings (#7224)
Ajaxy/telegram-tt/master • 1858d63 • 24 files, +1612/-1290
Audio Player, Message Context Menu: Support "Add to Profile" for audio (#7225)
Ajaxy/telegram-tt/master • da86d68 • 167 files, +2808/-2694
[Build]
#webz
Message: Add special copy options (#7219)
Ajaxy/telegram-tt/master • 665d928 • 5 files, +38/-27
Localization: Regular updates (#7221)
Ajaxy/telegram-tt/master • f564a63 • 3 files, +127/-90
Community: Follow-up (#7220)
Ajaxy/telegram-tt/master • ba0a4a5 • 8 files, +30/-4
Premium Modal: Add missing premium features to the list (#7217)
Ajaxy/telegram-tt/master • e474e0c • 1 files, +2/-0
Settings: Fix star style (#7222)
Ajaxy/telegram-tt/master • d7d43d8 • 2 files, +28/-10
Settings: Fix blinking in Auto-Delete settings (#7224)
Ajaxy/telegram-tt/master • 1858d63 • 24 files, +1612/-1290
Audio Player, Message Context Menu: Support "Add to Profile" for audio (#7225)
Ajaxy/telegram-tt/master • da86d68 • 167 files, +2808/-2694
[Build]
#webz
UnigramDev/Unigram/playback-rework • 1e3ece3 • 1 files, +176/-27
Show album covers in the system media transport controls
The cover is the album cover thumbnail TDLib provides, or the external
variant closest to the size the transport renders when the audio file
carries none. Voice and video notes have no cover and show nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/playback-rework • 920cc42 • 2 files, +103/-49
Fix threading around the transport controls and the player
The transport belongs to the view it was acquired for, so every write to
it now goes back to that view's thread; it can otherwise be driven from a
second window's thread once a Clear has recreated the player there.
Player creation takes the same lock the rest of the player access does,
and repeat mode and shuffle are persisted and mirrored to the transport.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/playback-rework • af08bff • 3 files, +487/-57
Grow the playback playlist as it is played
The playlist was one search of up to a hundred messages around the one
that started it, and the profile audio one was the first hundred audios
with the played one moved to the front. Both now page in as the current
track nears either end, through a source that owns its cursors.
A caller holding a profile source and the audios it loaded can hand both
over, so the playlist keeps the profile order instead of starting again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/playback-rework • d09471c • 2 files, +19/-4
Start a handed-over profile playlist past the items it came with
Paging profile audio is by position, so a caller that hands over items
without the source that loaded them left the cursor at zero and the first
page repeated them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Show album covers in the system media transport controls
The cover is the album cover thumbnail TDLib provides, or the external
variant closest to the size the transport renders when the audio file
carries none. Voice and video notes have no cover and show nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/playback-rework • 920cc42 • 2 files, +103/-49
Fix threading around the transport controls and the player
The transport belongs to the view it was acquired for, so every write to
it now goes back to that view's thread; it can otherwise be driven from a
second window's thread once a Clear has recreated the player there.
Player creation takes the same lock the rest of the player access does,
and repeat mode and shuffle are persisted and mirrored to the transport.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/playback-rework • af08bff • 3 files, +487/-57
Grow the playback playlist as it is played
The playlist was one search of up to a hundred messages around the one
that started it, and the profile audio one was the first hundred audios
with the played one moved to the front. Both now page in as the current
track nears either end, through a source that owns its cursors.
A caller holding a profile source and the audios it loaded can hand both
over, so the playlist keeps the profile order instead of starting again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/playback-rework • d09471c • 2 files, +19/-4
Start a handed-over profile playlist past the items it came with
Paging profile audio is by position, so a caller that hands over items
without the source that loaded them left the cursor at zero and the first
page repeated them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
morethanwords/tweb/master • 79d6a8f • 4 files, +276/-24
Stop replaying dice in a chat you have already read
A group/channel message loaded without its dialog carries no `unread`
flag, so renderMessage falls back to the peer's read cursor. It asked
getReadMaxIdIfUnread for it — but that one answers 0 once the chat has
nothing unread, and `0 < mid` holds for every bubble. Every message of a
fully read chat therefore looked unread: dice and the other interactive
emoji rolled again on every chat open, sticker and message effects
re-played, and every bubble got a pointless read observer.
Extract the cursor itself as getInboxReadMaxId and read that from the
renderer. Own messages are excluded — the inbox cursor tracks incoming
ones and stays below them, so a dice we sent looked unread too. An
unknown cursor (no dialog loaded yet) keeps the old conservative answer:
the read observer is the only thing that marks a history as read, so it
must never be skipped on a guess.
getReadMaxIdIfUnread keeps its contract for the unread delimiter, the
jump target and the sponsored cursor; a 648-case matrix pins it to the
previous implementation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • a2662b6 • 3 files, +249/-4
Validate the URL an Instant View link decodes to
wrapUrl's `iv` branch pulled the target out of `tg://iv?url=...` and handed it
straight to the anchor href, skipping the protocol filter every other branch in
the same function applies. The branch is gated on `safe`, which
wrapTelegramRichText sets from `textUrl.webpage_id != 0` — so the href was only
ever as trustworthy as that one wire field, with no client-side backstop.
Run the decoded URL through matchUrlProtocol the same way the incoming url is
handled a few lines above. Real IV targets are absolute http/https and come out
unchanged; schemeless ones stop resolving relative to our own origin.
Also stop reading a stringified '0' webpage_id as a real webpage: that only
worked because fetchLong keeps longs numeric while they fit in the safe-integer
range, which is not something this call site should depend on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Stop replaying dice in a chat you have already read
A group/channel message loaded without its dialog carries no `unread`
flag, so renderMessage falls back to the peer's read cursor. It asked
getReadMaxIdIfUnread for it — but that one answers 0 once the chat has
nothing unread, and `0 < mid` holds for every bubble. Every message of a
fully read chat therefore looked unread: dice and the other interactive
emoji rolled again on every chat open, sticker and message effects
re-played, and every bubble got a pointless read observer.
Extract the cursor itself as getInboxReadMaxId and read that from the
renderer. Own messages are excluded — the inbox cursor tracks incoming
ones and stays below them, so a dice we sent looked unread too. An
unknown cursor (no dialog loaded yet) keeps the old conservative answer:
the read observer is the only thing that marks a history as read, so it
must never be skipped on a guess.
getReadMaxIdIfUnread keeps its contract for the unread delimiter, the
jump target and the sponsored cursor; a 648-case matrix pins it to the
previous implementation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • a2662b6 • 3 files, +249/-4
Validate the URL an Instant View link decodes to
wrapUrl's `iv` branch pulled the target out of `tg://iv?url=...` and handed it
straight to the anchor href, skipping the protocol filter every other branch in
the same function applies. The branch is gated on `safe`, which
wrapTelegramRichText sets from `textUrl.webpage_id != 0` — so the href was only
ever as trustworthy as that one wire field, with no client-side backstop.
Run the decoded URL through matchUrlProtocol the same way the incoming url is
handled a few lines above. Real IV targets are absolute http/https and come out
unchanged; schemeless ones stop resolving relative to our own origin.
Also stop reading a stringified '0' webpage_id as a real webpage: that only
worked because fetchLong keeps longs numeric while they fit in the safe-integer
range, which is not something this call site should depend on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡2
morethanwords/tweb/master • b59a023 • 3 files, +146/-13
Bind the Mini App bridge to the origin it was opened at
The inbound listener authorised a bridge message by the frame's WindowProxy
alone. That identity survives a cross-origin navigation, so once a Mini App
frame ended up on another origin — an open redirect in the app, a script it
embeds, a callback on a partner domain — the document that replaced it kept
the bridge, and with it the privileges of the bot the frame was opened for:
that bot's device storage, invokeWebViewCustomMethod, and the location the
user had already granted, handed over with no further prompt.
Replies leaked the same way without anyone sending anything: they went out
with target origin '*', so an answer to a request the Mini App itself made —
its location, the clipboard, a custom method result — reached whatever
document occupied the frame by the time it arrived.
The server has been setting webViewResultUrl.same_origin by default since
July 25; Desktop, iOS and Web A all enforce it, and here it was parsed and
never read. Bind on that flag alone, so apps the server does not flag behave
exactly as before, and let postMessage do the outbound half: an event
addressed to the origin is dropped by the browser once the frame has left it.
The check is per message, not a kill switch. An app that walks through a
payment or OAuth origin loses only what it sends from there and works again
on return, which is how the other clients already behave. loadExternal
recomputes the binding because joinChatBotResultWebView replaces the URL.
Games, Instant View embeds and the 3DS verification frame pass no origin and
are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • b0c2052 • 2 files, +5/-5
Stop a group title from running as script in the search chips
The suggestion chips the left search renders under the input take their label
from `community?.title` — the raw `Chat.title` string — and renderEntity assigned
a string label with `innerHTML`. A group named `<img src=x onerror=...>` therefore
executed on web.telegram.org as soon as anyone holding that chat typed enough of
its name for the chip to appear. No click, no opening the chat, no interaction
past the search box.
Only groups and channels reached it: `getCommunityByPeerId` is gated on
`isAnyChat()`, so private chats fell through to `PeerTitle` and built text nodes
like the rest of the app. The string branch went live with "Implement Telegram
Communities" (2d2f188e1); before that the only strings arriving were the
generated date tips, and the sink sat unreachable.
Wrap the string through `wrapEmojiText` + `setInnerHTML` — the path every other
title in the app already takes — rather than escaping at the call site, so the
next caller to pass a string cannot reopen it. Emoji in a chip label now render
as emoji, which the raw assignment never did.
`wrapMessageForReply` carried the identical `innerHTML` line. Nothing hostile
reaches it today, because every rich part is built as a node and only a literal
space ever takes the string branch, so it appends now too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Bind the Mini App bridge to the origin it was opened at
The inbound listener authorised a bridge message by the frame's WindowProxy
alone. That identity survives a cross-origin navigation, so once a Mini App
frame ended up on another origin — an open redirect in the app, a script it
embeds, a callback on a partner domain — the document that replaced it kept
the bridge, and with it the privileges of the bot the frame was opened for:
that bot's device storage, invokeWebViewCustomMethod, and the location the
user had already granted, handed over with no further prompt.
Replies leaked the same way without anyone sending anything: they went out
with target origin '*', so an answer to a request the Mini App itself made —
its location, the clipboard, a custom method result — reached whatever
document occupied the frame by the time it arrived.
The server has been setting webViewResultUrl.same_origin by default since
July 25; Desktop, iOS and Web A all enforce it, and here it was parsed and
never read. Bind on that flag alone, so apps the server does not flag behave
exactly as before, and let postMessage do the outbound half: an event
addressed to the origin is dropped by the browser once the frame has left it.
The check is per message, not a kill switch. An app that walks through a
payment or OAuth origin loses only what it sends from there and works again
on return, which is how the other clients already behave. loadExternal
recomputes the binding because joinChatBotResultWebView replaces the URL.
Games, Instant View embeds and the 3DS verification frame pass no origin and
are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • b0c2052 • 2 files, +5/-5
Stop a group title from running as script in the search chips
The suggestion chips the left search renders under the input take their label
from `community?.title` — the raw `Chat.title` string — and renderEntity assigned
a string label with `innerHTML`. A group named `<img src=x onerror=...>` therefore
executed on web.telegram.org as soon as anyone holding that chat typed enough of
its name for the chip to appear. No click, no opening the chat, no interaction
past the search box.
Only groups and channels reached it: `getCommunityByPeerId` is gated on
`isAnyChat()`, so private chats fell through to `PeerTitle` and built text nodes
like the rest of the app. The string branch went live with "Implement Telegram
Communities" (2d2f188e1); before that the only strings arriving were the
generated date tips, and the sink sat unreachable.
Wrap the string through `wrapEmojiText` + `setInnerHTML` — the path every other
title in the app already takes — rather than escaping at the call site, so the
next caller to pass a string cannot reopen it. Emoji in a chip label now render
as emoji, which the raw assignment never did.
`wrapMessageForReply` carried the identical `innerHTML` line. Nothing hostile
reaches it today, because every rich part is built as a node and only a literal
space ever takes the string branch, so it appends now too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡2
morethanwords/tweb/master • e532dd4 • 8 files, +282/-81
Notify when someone reacts to your story
Settings → Notifications has carried a Reactions → Stories row since the
reactions settings landed: off, contacts, everyone. Nothing read it. The
`updateNewStoryReaction` the server sends when someone reacts to one of our
stories had no handler at all — it existed in the schema and was dropped on
arrival — so the row promised notifications the client never produced.
The foreground half has to be built locally, for the same reason posting
notifications are: the service worker suppresses a server push while any window
is alive, so with the tab open nothing else fires. Android does exactly this,
turning the update into a local notification; iOS and Desktop lean entirely on
the server push, and tdesktop does not even parse the update.
Gate on `stories_notify_from` from account.getReactionsNotifySettings — absent
means off — and drop reactions from non-contacts when it is
reactionNotificationsFromContacts. Android checks only the on/off half locally
and leaves the contacts filter to the server; applying it here costs one cached
read and keeps the row honest whatever the server decides to deliver. Reactions
that were removed, our own, and restricted peers never reach the notification.
`show_previews` off, or an emoticon we cannot resolve, falls back to the wording
the mobile clients use when they hide a sender.
Title and body come from PUSH_REACT_STORY verbatim: the peer's name, then
"❤️ to your story". Clicking opens the story of ours that was reacted to, not a
chat with whoever reacted — `openStoriesForPeer` takes a story id now, and the
cross-account URL carries it as `story_id` so the existing `story=1` marker
keeps meaning "the peer's whole ring".
That click had a second path worth fixing. A background REACT_STORY push sends
the story id as `msg_id` and `from_id` as the peer who reacted, so the story
branch in the click handler never matched: it fell through to setInnerPeer and
opened a chat with the reactor, scrolled to a message id that was really a story
id. Route on the loc_key instead, which also covers REACT_STORY_HIDDEN pushes
that carry no `from_id` and used to be dropped outright, and stamp the same
loc_key on the notification we build so the in-page and service-worker paths
land in one place.
The three builders had grown ~30 identical lines each. The other-account title
suffix, the peer avatar, the passcode-lock override, the push payload
boilerplate and the notify call move into finishNotification; peerString and
peerTitle into getNotificationPeer; the reaction emoticon into
getReactionEmoticon, which no longer walks into getCustomEmojiDocument with an
undefined id for a paid reaction. Each builder is left with the title, body, key
and click target that actually differ.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Notify when someone reacts to your story
Settings → Notifications has carried a Reactions → Stories row since the
reactions settings landed: off, contacts, everyone. Nothing read it. The
`updateNewStoryReaction` the server sends when someone reacts to one of our
stories had no handler at all — it existed in the schema and was dropped on
arrival — so the row promised notifications the client never produced.
The foreground half has to be built locally, for the same reason posting
notifications are: the service worker suppresses a server push while any window
is alive, so with the tab open nothing else fires. Android does exactly this,
turning the update into a local notification; iOS and Desktop lean entirely on
the server push, and tdesktop does not even parse the update.
Gate on `stories_notify_from` from account.getReactionsNotifySettings — absent
means off — and drop reactions from non-contacts when it is
reactionNotificationsFromContacts. Android checks only the on/off half locally
and leaves the contacts filter to the server; applying it here costs one cached
read and keeps the row honest whatever the server decides to deliver. Reactions
that were removed, our own, and restricted peers never reach the notification.
`show_previews` off, or an emoticon we cannot resolve, falls back to the wording
the mobile clients use when they hide a sender.
Title and body come from PUSH_REACT_STORY verbatim: the peer's name, then
"❤️ to your story". Clicking opens the story of ours that was reacted to, not a
chat with whoever reacted — `openStoriesForPeer` takes a story id now, and the
cross-account URL carries it as `story_id` so the existing `story=1` marker
keeps meaning "the peer's whole ring".
That click had a second path worth fixing. A background REACT_STORY push sends
the story id as `msg_id` and `from_id` as the peer who reacted, so the story
branch in the click handler never matched: it fell through to setInnerPeer and
opened a chat with the reactor, scrolled to a message id that was really a story
id. Route on the loc_key instead, which also covers REACT_STORY_HIDDEN pushes
that carry no `from_id` and used to be dropped outright, and stamp the same
loc_key on the notification we build so the in-page and service-worker paths
land in one place.
The three builders had grown ~30 identical lines each. The other-account title
suffix, the peer avatar, the passcode-lock override, the push payload
boilerplate and the notify call move into finishNotification; peerString and
peerTitle into getNotificationPeer; the reaction emoticon into
getReactionEmoticon, which no longer walks into getCustomEmojiDocument with an
undefined id for a paid reaction. Each builder is left with the title, body, key
and click target that actually differ.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • 16bf5ed • 6 files, +244/-74
Warn before opening a link that fakes a familiar domain
Auto-detected links were checked for a latin/non-latin mix only, so a
host built from two non-latin scripts opened without a word: `пօчта.рф`
is cyrillic with an armenian `օ` standing in for `о`. The same check
cried wolf on `例え.jp`, where the only latin in sight is the TLD.
Scripts are now resolved per label following UTS #39 — a label mixing
scripts is a spoof unless it is one of the japanese/korean/chinese
combinations, a letter from a script no registry sells is a spoof on its
own, and a cyrillic/greek/armenian label next to a latin one still is
one, which is what keeps `аррӏе.com` caught.
The check moved into safeWindowOpen as well, so the urls the app opens
without an anchor of its own — inline keyboard buttons, story links, web
app events — no longer skip it, and a host-shaped login such as
`почта.рф@evil.com` is now caught too. Hence the rename: it is no longer
only about scripts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • b2df097 • 4 files, +54/-10
Recolor chat list spoilers when a row becomes active
The bluff spoiler a dialog preview draws over spoilered text picks its
particle color once, from `getComputedStyle(element).color`, and only
recomputes it on a resize, a theme change or a new chat background.
Opening a chat repaints the row's subtitle to white through CSS alone, so
the particles kept the grey of an inactive row — and stayed white once the
row lost the highlight.
The color is already known at that point: renderDialogSubtitleParts threads
`getTextColor(isActive)` into wrapRichText for the custom emoji sitting next
to the spoiler. Pass it down to the renderer too, resolve it through
customProperties, and recolor it from setDialogActiveStatus exactly where
`setTextColor` recolors those custom emoji. Sampling the computed color
stays as the fallback for callers with no property to name — a reply preview
inside a bubble takes its color from the peer, not from a fixed variable.
The color lives in a WeakMap rather than beside the update callback because
it has to outlive the render target: the element can be detached and
reconnected (BluffSpoilerController), and that re-attach must repaint with
the color the row has now, not with the one captured when it first rendered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Warn before opening a link that fakes a familiar domain
Auto-detected links were checked for a latin/non-latin mix only, so a
host built from two non-latin scripts opened without a word: `пօчта.рф`
is cyrillic with an armenian `օ` standing in for `о`. The same check
cried wolf on `例え.jp`, where the only latin in sight is the TLD.
Scripts are now resolved per label following UTS #39 — a label mixing
scripts is a spoof unless it is one of the japanese/korean/chinese
combinations, a letter from a script no registry sells is a spoof on its
own, and a cyrillic/greek/armenian label next to a latin one still is
one, which is what keeps `аррӏе.com` caught.
The check moved into safeWindowOpen as well, so the urls the app opens
without an anchor of its own — inline keyboard buttons, story links, web
app events — no longer skip it, and a host-shaped login such as
`почта.рф@evil.com` is now caught too. Hence the rename: it is no longer
only about scripts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • b2df097 • 4 files, +54/-10
Recolor chat list spoilers when a row becomes active
The bluff spoiler a dialog preview draws over spoilered text picks its
particle color once, from `getComputedStyle(element).color`, and only
recomputes it on a resize, a theme change or a new chat background.
Opening a chat repaints the row's subtitle to white through CSS alone, so
the particles kept the grey of an inactive row — and stayed white once the
row lost the highlight.
The color is already known at that point: renderDialogSubtitleParts threads
`getTextColor(isActive)` into wrapRichText for the custom emoji sitting next
to the spoiler. Pass it down to the renderer too, resolve it through
customProperties, and recolor it from setDialogActiveStatus exactly where
`setTextColor` recolors those custom emoji. Sampling the computed color
stays as the fallback for callers with no property to name — a reply preview
inside a bubble takes its color from the peer, not from a fixed variable.
The color lives in a WeakMap rather than beside the update callback because
it has to outlive the render target: the element can be detached and
reconnected (BluffSpoilerController), and that re-attach must repaint with
the color the row has now, not with the one captured when it first rendered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • f57dbce • 15 files, +1248/-61
Highlight the found text in bubbles and search rows, and the quoted part on quote click
There was one highlight in the whole app — search rows made the matched
word bold by faking a `messageEntityHighlight` entity in
wrapMessageForReply — and nothing at all in the chat: jumping to a search
result or clicking a quote only flashed the bubble.
One helper now does it everywhere: `highlightText()` in
helpers/dom/textHighlight.ts describes the matched text as live Ranges over
the container's text nodes (entities, custom emoji, spoilers, code and
translations untouched; a multi-line match is a single range), re-finds it
when the container mutates or re-flows and collects itself once the
container leaves the document. Two paints:
- `::highlight()` (CSS Custom Highlight API) for chat-list rows — accent
text like tdesktop/Android, white on the active row;
- `lineBoxes` for bubbles: an absolutely positioned overlay with one box
per line stretched to the full line height. Browsers paint `::highlight`
only glyph-high, which leaves gaps between wrapped lines; the overlay is
a contiguous block like a selection and works without the API.
Matching follows the clients: a search query as a phrase, else every word
as a word prefix extended to the word's end with a shorter-prefix fallback
(tdesktop's FindSearchQueryHighlight); a quote is the exact text closest
to quote_offset with a whitespace-insensitive retry (findTwoWays).
Wiring: setLastMessage lights the query up in the message part of a row
and stamps it on the row, the list click carries it as
ChatSetPeerOptions.highlight, topbar search passes its query, the reply
click passes the manual quote — bubbles.highlightBubble paints it after
the scroll to the message settles (iOS): the whole text fades in selected,
holds, narrows down onto the match, stays four seconds and fades. Link
previews, time, reactions and code headers are not the text.
Also: start-preview.sh disables pnpm's verify-deps-before-run — in a
worktree it tried to purge the symlinked node_modules.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#webk
Highlight the found text in bubbles and search rows, and the quoted part on quote click
There was one highlight in the whole app — search rows made the matched
word bold by faking a `messageEntityHighlight` entity in
wrapMessageForReply — and nothing at all in the chat: jumping to a search
result or clicking a quote only flashed the bubble.
One helper now does it everywhere: `highlightText()` in
helpers/dom/textHighlight.ts describes the matched text as live Ranges over
the container's text nodes (entities, custom emoji, spoilers, code and
translations untouched; a multi-line match is a single range), re-finds it
when the container mutates or re-flows and collects itself once the
container leaves the document. Two paints:
- `::highlight()` (CSS Custom Highlight API) for chat-list rows — accent
text like tdesktop/Android, white on the active row;
- `lineBoxes` for bubbles: an absolutely positioned overlay with one box
per line stretched to the full line height. Browsers paint `::highlight`
only glyph-high, which leaves gaps between wrapped lines; the overlay is
a contiguous block like a selection and works without the API.
Matching follows the clients: a search query as a phrase, else every word
as a word prefix extended to the word's end with a shorter-prefix fallback
(tdesktop's FindSearchQueryHighlight); a quote is the exact text closest
to quote_offset with a whitespace-insensitive retry (findTwoWays).
Wiring: setLastMessage lights the query up in the message part of a row
and stamps it on the row, the list click carries it as
ChatSetPeerOptions.highlight, topbar search passes its query, the reply
click passes the manual quote — bubbles.highlightBubble paints it after
the scroll to the message settles (iOS): the whole text fades in selected,
holds, narrows down onto the match, stays four seconds and fades. Link
previews, time, reactions and code headers are not the text.
Also: start-preview.sh disables pnpm's verify-deps-before-run — in a
worktree it tried to purge the symlinked node_modules.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • 1ca7cb9 • 30 files, +1200/-73
Show the group's sticker set and emoji pack in the panel, and let admins set them
A supergroup can offer a sticker set and a custom emoji pack to everyone
chatting in it. tweb showed neither and had no way to set either: the packs
existed in channelFull as `stickerset` / `emojiset` and were simply ignored.
Panel side. GroupSetController (emoticonsDropdown/groupSet.ts) resolves what
the open chat offers and hands the stickers / emoji tab a section for it, so
the set shows up even when the user has not installed it. Resolving reads the
chat's full info, so it is kept off the chat-open path: it runs when the
dropdown opens and afterwards only on that chat's `chat_full_update`. The
section is filed under a synthetic category id rather than the set's own —
the same set may also sit in the user's list, and the install / delete /
reorder events address those by id; when the user installs it themselves the
group copy is dropped as a duplicate, and uninstalling brings it back.
Who sees what follows tdesktop: a sticker set is the server's call
(`can_set_stickers`, i.e. the group is big enough), an emoji pack is
`amCreator() || (adminRights & ChangeInfo)` in a megagroup. An admin gets a
gear to the setup screen; everyone else gets tdesktop's Hidden place — the
section collapses below their own sets and the choice persists per chat,
keyed by the set id so a group swapping packs brings it back.
Setup screen. AppGroupStickersTab serves both variants: search by
t.me/addstickers link, short name or @name, the user's own sets with a radio,
apply and remove, and it follows the chat while open (another admin or
session replacing the pack, or the group being boosted under it). The emoji
pack additionally needs a boost level (`group_emoji_stickers_level_min`) —
the entry in Edit Chat stays visible below it with a level badge, and
applying offers boosting instead of failing.
appChatsManager.setGroupStickerSet wraps channels.setStickers /
channels.setEmojiStickers and patches the cached full chat.
openBoosts is now the single entry into boosting — the chat topbar's menu
item and both level gates — sending an admin to the Boosts tab and everyone
else to the boost popup, and no longer stacking a second Boosts tab on a
repeated tap. PopupBoost takes a reason so the copy says which feature the
level is for instead of talking about stories.
Reused rather than copied: Badge gained the rectangle variant that was living
as `.personal-channel-counter` (which now uses it), SearchLoading /
SearchEmpty moved out of chat automation into a shared component, Row.Media
made its size optional.
Two guards for crashes met on the way: selectTarget's indicator lookup runs a
frame later, by which point the row may be rebuilt or the previous item gone,
and the category-visibility observer may hold an entry for a category deleted
since it queued.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Show the group's sticker set and emoji pack in the panel, and let admins set them
A supergroup can offer a sticker set and a custom emoji pack to everyone
chatting in it. tweb showed neither and had no way to set either: the packs
existed in channelFull as `stickerset` / `emojiset` and were simply ignored.
Panel side. GroupSetController (emoticonsDropdown/groupSet.ts) resolves what
the open chat offers and hands the stickers / emoji tab a section for it, so
the set shows up even when the user has not installed it. Resolving reads the
chat's full info, so it is kept off the chat-open path: it runs when the
dropdown opens and afterwards only on that chat's `chat_full_update`. The
section is filed under a synthetic category id rather than the set's own —
the same set may also sit in the user's list, and the install / delete /
reorder events address those by id; when the user installs it themselves the
group copy is dropped as a duplicate, and uninstalling brings it back.
Who sees what follows tdesktop: a sticker set is the server's call
(`can_set_stickers`, i.e. the group is big enough), an emoji pack is
`amCreator() || (adminRights & ChangeInfo)` in a megagroup. An admin gets a
gear to the setup screen; everyone else gets tdesktop's Hidden place — the
section collapses below their own sets and the choice persists per chat,
keyed by the set id so a group swapping packs brings it back.
Setup screen. AppGroupStickersTab serves both variants: search by
t.me/addstickers link, short name or @name, the user's own sets with a radio,
apply and remove, and it follows the chat while open (another admin or
session replacing the pack, or the group being boosted under it). The emoji
pack additionally needs a boost level (`group_emoji_stickers_level_min`) —
the entry in Edit Chat stays visible below it with a level badge, and
applying offers boosting instead of failing.
appChatsManager.setGroupStickerSet wraps channels.setStickers /
channels.setEmojiStickers and patches the cached full chat.
openBoosts is now the single entry into boosting — the chat topbar's menu
item and both level gates — sending an admin to the Boosts tab and everyone
else to the boost popup, and no longer stacking a second Boosts tab on a
repeated tap. PopupBoost takes a reason so the copy says which feature the
level is for instead of talking about stories.
Reused rather than copied: Badge gained the rectangle variant that was living
as `.personal-channel-counter` (which now uses it), SearchLoading /
SearchEmpty moved out of chat automation into a shared component, Row.Media
made its size optional.
Two guards for crashes met on the way: selectTarget's indicator lookup runs a
frame later, by which point the row may be rebuilt or the previous item gone,
and the category-visibility observer may hold an entry for a category deleted
since it queued.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • 2b00c4d • 6 files, +289/-14
Release chat-list rows the virtual list drops
A tab left open for two days held 597 custom-emoji renderers for a chat list
showing 38, and 1167 animation items against 2763 DOM nodes - 4.7 GB of process
memory, 1.5 GB of it GPU.
The chat list is the only owner of its DialogElements: deferredSortedVirtualList
stores them and nothing else holds a reference. But it dropped them silently -
removeItem, the tail trimmed by checkShrink on every scroll, clear, dispose, and
the value replaced when an id is re-added. A dropped DialogElement owns a
middlewareHelper, and the emoji status, its lottie/video players and the
subtitle's custom emoji are released only by that helper's destroy, so every
trimmed row leaked its renderer and the compositor worker kept its
OffscreenCanvas for the lifetime of the tab.
Give the list an onItemDiscard hook, fired on every path that drops a value for
good, and destroy the DialogElement behind it. onItemUnmount is deliberately not
that signal: it fires when a row merely leaves the rendered window and is kept
for re-mounting on scroll back. Only 'dialog' items are destroyed - the custom
ones are the caller's own objects, handed in as the key, and may be re-added.
CustomEmojiRendererElement also resurrected itself. Custom element reactions are
read off the prototype once, at customElements.define, so `this.connectedCallback
= undefined` never stopped the browser from calling it again: a destroyed
renderer that got re-inserted re-registered in emojiRenderers with its destroy()
already nulled out, and nothing could ever reclaim it. Replace the nulling
sentinel with an explicit `destroyed` flag, keep destroyed renderers out of
connectedCallback, and make destroy() idempotent - which also cuts the recursion
where clean() re-enters through the owner's onDestroy.
No time-based reclaim: a renderer can be legitimately detached and kept for
re-insertion, and there is no way from inside it to tell that apart from an owner
dropped without cleaning.
Adds window.memoryReport() - the JS heap accounts for none of the ImageBitmap and
OffscreenCanvas memory that dominates here, so the app has to count its own
registries. The detached figures are the ones to watch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Release chat-list rows the virtual list drops
A tab left open for two days held 597 custom-emoji renderers for a chat list
showing 38, and 1167 animation items against 2763 DOM nodes - 4.7 GB of process
memory, 1.5 GB of it GPU.
The chat list is the only owner of its DialogElements: deferredSortedVirtualList
stores them and nothing else holds a reference. But it dropped them silently -
removeItem, the tail trimmed by checkShrink on every scroll, clear, dispose, and
the value replaced when an id is re-added. A dropped DialogElement owns a
middlewareHelper, and the emoji status, its lottie/video players and the
subtitle's custom emoji are released only by that helper's destroy, so every
trimmed row leaked its renderer and the compositor worker kept its
OffscreenCanvas for the lifetime of the tab.
Give the list an onItemDiscard hook, fired on every path that drops a value for
good, and destroy the DialogElement behind it. onItemUnmount is deliberately not
that signal: it fires when a row merely leaves the rendered window and is kept
for re-mounting on scroll back. Only 'dialog' items are destroyed - the custom
ones are the caller's own objects, handed in as the key, and may be re-added.
CustomEmojiRendererElement also resurrected itself. Custom element reactions are
read off the prototype once, at customElements.define, so `this.connectedCallback
= undefined` never stopped the browser from calling it again: a destroyed
renderer that got re-inserted re-registered in emojiRenderers with its destroy()
already nulled out, and nothing could ever reclaim it. Replace the nulling
sentinel with an explicit `destroyed` flag, keep destroyed renderers out of
connectedCallback, and make destroy() idempotent - which also cuts the recursion
where clean() re-enters through the owner's onDestroy.
No time-based reclaim: a renderer can be legitimately detached and kept for
re-insertion, and there is no way from inside it to tell that apart from an owner
dropped without cleaning.
Adds window.memoryReport() - the JS heap accounts for none of the ImageBitmap and
OffscreenCanvas memory that dominates here, so the app has to count its own
registries. The detached figures are the ones to watch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • a381f83 • 3 files, +45/-5
Make the dialog list's delete cover both collections it reads from
The list keeps pinned entries in their own collection, and every read API merges
the two - has(), getDialogElement() and getAllDialogElementsMap() all see pinned
entries - while delete() only ever removed from the unpinned one. On top of that
removeItem answered from the merged map, so it reported success for a row it had
left in place, which would take adjustTotalCount down for a dialog that never
went away and skip onItemDiscard, leaving the DialogElement holding its
middlewareHelper.
No caller can reach that today. Real DialogElements are pinned only by addPinned,
used by the monoforum and bot-forum tabs to pin the parent peer above the topic
list, and those tabs build on AutonomousDialogListBase - not AutonomousDialogList,
where the deletes that walk the merged view (validateListForFilter,
updateDialog) live. The keys their own deletes carry - monoforum thread peers,
topic ids - never equal the pinned parent. So this closes an asymmetry between
what the list reports and what it does rather than fixing an observed bug.
Make each remover report what it actually removed, bail before writing when the
id is not theirs (the setter hands back a fresh array either way, invalidating
the memos that rebuild the rendered list), and have delete() cover both sides.
Should a pinned key ever reach delete(), its row now goes away with it instead of
staying behind as a header for a dialog that no longer exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 8314c57 • 8 files, +114/-16
Hold the panel's reordering until it hides, and reorder a gif that was used
Sending a sticker from the panel reshuffled the recent row on screen. Only one
event was ever postponed - stickers_top - and even that one slipped through:
postponing checked isActive(), the class that sendDocId drops the moment the
send resolves, while the panel keeps fading out for another 200ms. Anything
arriving in that window was applied on a panel still in plain view.
Postponing now waits for the panel to be off screen (isDisplayed) and covers
every event that changes order: the recent and faved stickers, the set order,
the recent emoji and the saved gifs. What the user does in the open panel -
faving, deleting from the recent ones, clearing them, un-saving a gif - still
lands at once; a list that got shorter is the tell.
The emoji tab subscribed to emoji_recent twice, postponed and immediate, so the
recent row always reshuffled live and the delete branch had to cancel its own
queued copy. One subscription now, and that guard turned into the one that
matters: deleting an emoji drops a queued use of it, so closing the panel
cannot bring it back.
Gifs were not reordered at all - only the sticker half of it was ever wired up.
Every official client moves a gif that was just sent to the front of the saved
ones (tdesktop Stickers::addSavedGif, Android MediaDataController.addRecentGif,
iOS ApplyUpdateMessage), locally and without a request of its own: the server
reorders the list on send and updateSavedGifs brings the result over.
appGifsManager.addRecentGif does the same, both for a gif sent from the panel
and for one picked out of an inline bot's results, and the gifs tab got the
super.init() it never had - without it nothing flushed its postponed events.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Make the dialog list's delete cover both collections it reads from
The list keeps pinned entries in their own collection, and every read API merges
the two - has(), getDialogElement() and getAllDialogElementsMap() all see pinned
entries - while delete() only ever removed from the unpinned one. On top of that
removeItem answered from the merged map, so it reported success for a row it had
left in place, which would take adjustTotalCount down for a dialog that never
went away and skip onItemDiscard, leaving the DialogElement holding its
middlewareHelper.
No caller can reach that today. Real DialogElements are pinned only by addPinned,
used by the monoforum and bot-forum tabs to pin the parent peer above the topic
list, and those tabs build on AutonomousDialogListBase - not AutonomousDialogList,
where the deletes that walk the merged view (validateListForFilter,
updateDialog) live. The keys their own deletes carry - monoforum thread peers,
topic ids - never equal the pinned parent. So this closes an asymmetry between
what the list reports and what it does rather than fixing an observed bug.
Make each remover report what it actually removed, bail before writing when the
id is not theirs (the setter hands back a fresh array either way, invalidating
the memos that rebuild the rendered list), and have delete() cover both sides.
Should a pinned key ever reach delete(), its row now goes away with it instead of
staying behind as a header for a dialog that no longer exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 8314c57 • 8 files, +114/-16
Hold the panel's reordering until it hides, and reorder a gif that was used
Sending a sticker from the panel reshuffled the recent row on screen. Only one
event was ever postponed - stickers_top - and even that one slipped through:
postponing checked isActive(), the class that sendDocId drops the moment the
send resolves, while the panel keeps fading out for another 200ms. Anything
arriving in that window was applied on a panel still in plain view.
Postponing now waits for the panel to be off screen (isDisplayed) and covers
every event that changes order: the recent and faved stickers, the set order,
the recent emoji and the saved gifs. What the user does in the open panel -
faving, deleting from the recent ones, clearing them, un-saving a gif - still
lands at once; a list that got shorter is the tell.
The emoji tab subscribed to emoji_recent twice, postponed and immediate, so the
recent row always reshuffled live and the delete branch had to cancel its own
queued copy. One subscription now, and that guard turned into the one that
matters: deleting an emoji drops a queued use of it, so closing the panel
cannot bring it back.
Gifs were not reordered at all - only the sticker half of it was ever wired up.
Every official client moves a gif that was just sent to the front of the saved
ones (tdesktop Stickers::addSavedGif, Android MediaDataController.addRecentGif,
iOS ApplyUpdateMessage), locally and without a request of its own: the server
reorders the list on send and updateSavedGifs brings the result over.
appGifsManager.addRecentGif does the same, both for a gif sent from the panel
and for one picked out of an inline bot's results, and the gifs tab got the
super.init() it never had - without it nothing flushed its postponed events.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • 5db7cfb • 3 files, +75/-11
Center a jump on the highlighted text when the message does not fit the screen
A bubble taller than the screen cannot be centered, so fastSmoothScroll aligns
its start instead - and the search result or the quote we jumped to stays
wherever it is, several screens below: highlighted, painted and out of sight.
The scroll knows about the highlight now. When the bubble does not fit and its
start does not bring the text into view either, the text itself is centered:
findTextRect measures the match (the very ranges the highlight paints, without
painting anything), and scrollToBubble hands its offset inside the bubble to
getElementPosition and its height to fastSmoothScroll's new getElementSize -
"the part of the element that has to end up in the middle". The date group
fallback is dropped for such a scroll, it would land on the group instead of
the text, so a match taller than the screen aligns at its start, the way
tdesktop's AdjustScrollForRange does it.
Nothing moves differently when the message fits the screen, when the match is
already within its first screenful, or when there is no match to find.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • b21491c • 300 files, +811/-800
Build
#webk
Center a jump on the highlighted text when the message does not fit the screen
A bubble taller than the screen cannot be centered, so fastSmoothScroll aligns
its start instead - and the search result or the quote we jumped to stays
wherever it is, several screens below: highlighted, painted and out of sight.
The scroll knows about the highlight now. When the bubble does not fit and its
start does not bring the text into view either, the text itself is centered:
findTextRect measures the match (the very ranges the highlight paints, without
painting anything), and scrollToBubble hands its offset inside the bubble to
getElementPosition and its height to fastSmoothScroll's new getElementSize -
"the part of the element that has to end up in the middle". The date group
fallback is dropped for such a scroll, it would land on the group instead of
the text, so a match taller than the screen aligns at its start, the way
tdesktop's AdjustScrollForRange does it.
Nothing moves differently when the message fits the screen, when the match is
already within its first screenful, or when there is no match to find.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • b21491c • 300 files, +811/-800
Build
#webk
🫡3
UnigramDev/Unigram/playback-reconcile • 3b08a55 • 3 files, +323/-2
Keep the playlist in step with the chat and the profile
Chat playlists follow UpdateNewMessage, UpdateDeleteMessages and
UpdateMessageSendSucceeded; deleting the playing message skips to the
next one rather than letting it play out.
Profile audio has no such updates and only ever changes from the playback
popup, so the popup reports it, and an audio removed there plays on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Keep the playlist in step with the chat and the profile
Chat playlists follow UpdateNewMessage, UpdateDeleteMessages and
UpdateMessageSendSucceeded; deleting the playing message skips to the
next one rather than letting it play out.
Profile audio has no such updates and only ever changes from the playback
popup, so the popup reports it, and an audio removed there plays on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram