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
UnigramDev/Unigram/develop • 39ce27d • 1 files, +13/-3
Never lay out the incoming page from the navigated handler
Writing a scroll mode on a scroller that is already laid out reconfigures its
manipulation, and that runs a layout pass over the whole tree. The incoming page
is the frame content by then but NavigateToAsync has not activated it, so
ChatView was arranged without a view model: it threw, and the exception unwound
the navigation before anything could be activated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 4878bfb • 2 files, +371/-52
Match Chrome's back gesture, in distance and in affordance
The pan commits at 30% of the display's larger edge rather than at a fixed 120,
which is what makes the distance feel the same whatever the window size, and the
first 60 DIPs only recognise the gesture. The chip is Chrome's: a 20-radius
circle over a ripple that grows to 40 and bursts to 48 on commit, travelling 146
with 72 of rubber band past it, inverting to the accent as it activates, and
retreating over a duration proportional to how far it came.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 4a16484 • 2 files, +63/-4
Size and load the chat search autocomplete from its own collection
The list is handed an empty collection that fills asynchronously, so a null
check on the source never saw a row: it now follows CollectionChanged too, and
starts the first page itself, since a collapsed list is never measured and its
panel never asks. Member rows are 44, not the 64 a ChatCell declares.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • a156a68 • 3 files, +101/-23
Snap a word or paragraph to the line the pointer is on
A paragraph break takes no rendered unit, so the end of a line and the start of
the next share an index: expanding from it always resolved to the following
paragraph, and a double tap on an empty line selected the next line's first
word. Hit-testing now reports which paragraph the point landed in, and reports
none for an empty line, where there is nothing to expand to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 9147943 • 1 files, +61/-9
Show a hand cursor over an inline button
The I-beam was driven for anything that isn't a hyperlink, buttons included,
and a disabled button takes no pointer input at all, so the pointer fell
through to the text behind it: its wrapper now carries a transparent
background, and the negative margin with it, to cover what the button paints.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Never lay out the incoming page from the navigated handler
Writing a scroll mode on a scroller that is already laid out reconfigures its
manipulation, and that runs a layout pass over the whole tree. The incoming page
is the frame content by then but NavigateToAsync has not activated it, so
ChatView was arranged without a view model: it threw, and the exception unwound
the navigation before anything could be activated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 4878bfb • 2 files, +371/-52
Match Chrome's back gesture, in distance and in affordance
The pan commits at 30% of the display's larger edge rather than at a fixed 120,
which is what makes the distance feel the same whatever the window size, and the
first 60 DIPs only recognise the gesture. The chip is Chrome's: a 20-radius
circle over a ripple that grows to 40 and bursts to 48 on commit, travelling 146
with 72 of rubber band past it, inverting to the accent as it activates, and
retreating over a duration proportional to how far it came.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 4a16484 • 2 files, +63/-4
Size and load the chat search autocomplete from its own collection
The list is handed an empty collection that fills asynchronously, so a null
check on the source never saw a row: it now follows CollectionChanged too, and
starts the first page itself, since a collapsed list is never measured and its
panel never asks. Member rows are 44, not the 64 a ChatCell declares.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • a156a68 • 3 files, +101/-23
Snap a word or paragraph to the line the pointer is on
A paragraph break takes no rendered unit, so the end of a line and the start of
the next share an index: expanding from it always resolved to the following
paragraph, and a double tap on an empty line selected the next line's first
word. Hit-testing now reports which paragraph the point landed in, and reports
none for an empty line, where there is nothing to expand to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 9147943 • 1 files, +61/-9
Show a hand cursor over an inline button
The I-beam was driven for anything that isn't a hyperlink, buttons included,
and a disabled button takes no pointer input at all, so the pointer fell
through to the text behind it: its wrapper now carries a transparent
background, and the negative margin with it, to cover what the button paints.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
UnigramDev/Unigram/develop • 03f39d7 • 1 files, +19/-4
Flatten an inline button's label as plain text
The label's own entities competed with the button entity over the same range,
and a run carrying both reports the inner type: a label of custom emoji
rendered as emoji, with no button around them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 1ea35f4 • 1 files, +23/-2
Stop waiting for TDLib to transform a frame once the call is closing
Transform blocks a media thread until TDLib answers, and Dispose blocks TDLib's
update thread until tgcalls has stopped — and that is the thread the answer would
come from. A transform still waiting when Dispose runs holds the two against each
other, with the call window frozen behind the manager lock.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 1a017ae • 1 files, +45/-32
Stop the group call managers outside the manager lock
Stop waits for the tgcalls threads to wind down, and the UI thread sits on the
lock for as long as that takes, which is what freezes the call window while a
call is torn down. The lock only has to cover the fields: once they are null the
UI thread has nothing left to reach, and the instance the caller took out is its
own to stop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • fbb0e7f • 1 files, +51/-23
Tear the group call managers down off TDLib's update thread
Stopping a manager waits for the tgcalls threads behind it, and one of those can
be waiting on a request whose answer arrives on the very thread Dispose runs on.
EndScreenSharing has the mirror problem: called from the UI thread, it held the
window for the whole teardown.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 3f8a338 • 2 files, +9/-2
Fix the order and the icons of the group call participant list
A participant moving down the list was inserted one place too low, since the
index was measured before the removal shifted everything after it. Sorting only
looked right for participants moving up, and the list drifts as people speak.
The icon for someone sharing a screen but no camera was behind a condition that
repeated the one above it, so the row kept whatever the recycled container had.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Flatten an inline button's label as plain text
The label's own entities competed with the button entity over the same range,
and a run carrying both reports the inner type: a label of custom emoji
rendered as emoji, with no button around them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 1ea35f4 • 1 files, +23/-2
Stop waiting for TDLib to transform a frame once the call is closing
Transform blocks a media thread until TDLib answers, and Dispose blocks TDLib's
update thread until tgcalls has stopped — and that is the thread the answer would
come from. A transform still waiting when Dispose runs holds the two against each
other, with the call window frozen behind the manager lock.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 1a017ae • 1 files, +45/-32
Stop the group call managers outside the manager lock
Stop waits for the tgcalls threads to wind down, and the UI thread sits on the
lock for as long as that takes, which is what freezes the call window while a
call is torn down. The lock only has to cover the fields: once they are null the
UI thread has nothing left to reach, and the instance the caller took out is its
own to stop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • fbb0e7f • 1 files, +51/-23
Tear the group call managers down off TDLib's update thread
Stopping a manager waits for the tgcalls threads behind it, and one of those can
be waiting on a request whose answer arrives on the very thread Dispose runs on.
EndScreenSharing has the mirror problem: called from the UI thread, it held the
window for the whole teardown.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 3f8a338 • 2 files, +9/-2
Fix the order and the icons of the group call participant list
A participant moving down the list was inserted one place too low, since the
index was measured before the removal shifted everything after it. Sorting only
looked right for participants moving up, and the list drifts as people speak.
The icon for someone sharing a screen but no camera was behind a condition that
repeated the one above it, so the row kept whatever the recycled container had.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
UnigramDev/Unigram/develop • 53f18ac • 1 files, +13/-2
Take the video list down with the pinned cell it belongs to
The list exists to hold the videos the pinned cell covers, so when that cell goes
away the grid lays them out again while the list still holds them, and every
remaining participant shows up twice. Clearing the pin was not enough: nothing
tore the list down, and the participant rows stayed hidden with it.
LoadVideoInfo replays every participant, which could put the pinned endpoint in
the list the same way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • bf72417 • 1 files, +67/-14
Pin a shared screen without being asked to
A screen share takes the pinned slot as it arrives, and again if the pinned cell
goes away while another one is running. One automatic pin replaces another, but a
cell the user pinned themselves is never taken over.
Selecting a cell moves out of the click handler, since both paths need it. The
only difference is that an automatic pin does not scroll the compact list to the
top, which would move it under whoever is reading it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 49d97ae • 1 files, +8/-0
Let a new back gesture take the chip from a burst still playing
The burst animates the chip's opacity to zero, and a property under a running
animation ignores direct writes, so the next gesture's reset was dropped and the
burst left the chip hidden for the whole of it - while the tracker carried the
gesture through to the navigation regardless.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Take the video list down with the pinned cell it belongs to
The list exists to hold the videos the pinned cell covers, so when that cell goes
away the grid lays them out again while the list still holds them, and every
remaining participant shows up twice. Clearing the pin was not enough: nothing
tore the list down, and the participant rows stayed hidden with it.
LoadVideoInfo replays every participant, which could put the pinned endpoint in
the list the same way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • bf72417 • 1 files, +67/-14
Pin a shared screen without being asked to
A screen share takes the pinned slot as it arrives, and again if the pinned cell
goes away while another one is running. One automatic pin replaces another, but a
cell the user pinned themselves is never taken over.
Selecting a cell moves out of the click handler, since both paths need it. The
only difference is that an automatic pin does not scroll the compact list to the
top, which would move it under whoever is reading it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 49d97ae • 1 files, +8/-0
Let a new back gesture take the chip from a burst still playing
The burst animates the chip's opacity to zero, and a property under a running
animation ignores direct writes, so the next gesture's reset was dropped and the
burst left the chip hidden for the whole of it - while the tracker carried the
gesture through to the navigation regardless.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
UnigramDev/Unigram/playback-shuffle • e6346dd • 3 files, +215/-36
Implement shuffle
The button existed in two places and set IsReversed, since shuffle itself
was never written. It now walks a fixed shuffled order, so Previous goes
back to what was actually heard, and pages loaded later are folded into
the part not yet played.
Paging is measured against the playlist rather than the order being
walked, which shuffle reaches the ends of in no particular order.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Implement shuffle
The button existed in two places and set IsReversed, since shuffle itself
was never written. It now walks a fixed shuffled order, so Previous goes
back to what was actually heard, and pages loaded later are folded into
the part not yet played.
Paging is measured against the playlist rather than the order being
walked, which shuffle reaches the ends of in no particular order.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
telegramdesktop/tdesktop/nightly • 565c076 • 5 files, +2/-67
Removed protobuf dependency from cld3 and build recipes.
#tdesktop
Removed protobuf dependency from cld3 and build recipes.
#tdesktop
🫡3
telegramdesktop/tdesktop/nightly • af5bb1b • 1 files, +2/-0
Enabled LTO in Release configuration on Windows CI.
#tdesktop
Enabled LTO in Release configuration on Windows CI.
#tdesktop
🫡3
telegramdesktop/tdesktop/nightly • 37b4ed4 • 1 files, +24/-11
Switched macOS CI to Release build with LTO on nightly.
#tdesktop
Switched macOS CI to Release build with LTO on nightly.
#tdesktop
🫡3
UnigramDev/Unigram/develop • ffdc081 • 5 files, +31/-23
Update emoji to Unicode 17.0
UnigramDev/Unigram/develop • 37b0fc4 • 1 files, +0/-0
Trim emoji font size
UnigramDev/Unigram/develop • 6b4b9cd • 1 files, +3/-3
Fix shift when sending a message after many unread messages
UnigramDev/Unigram/develop • 761ab7b • 1 files, +1/-0
Clear children before add
#unigram
Update emoji to Unicode 17.0
UnigramDev/Unigram/develop • 37b0fc4 • 1 files, +0/-0
Trim emoji font size
UnigramDev/Unigram/develop • 6b4b9cd • 1 files, +3/-3
Fix shift when sending a message after many unread messages
UnigramDev/Unigram/develop • 761ab7b • 1 files, +1/-0
Clear children before add
#unigram
UnigramDev/Unigram/record-start-on-press • 51b52ea • 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 • 0889840 • 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.
UnigramDev/Unigram/record-start-on-press • e892215 • 1 files, +3/-10
Drop two flags the timer and the session already answer for
_recordAudioVideoRunnableStarted said what _timer.IsEnabled says, and
_calledRecordRunnable was only ever read where a recording exists, which the
session knows.
UnigramDev/Unigram/record-start-on-press • 4da4557 • 1 files, +8/-1
Don't record for a press that was let go while rights were checked
The check is awaited between the press and the timer, so a release during it
left the press running: the timer started with no pointer down, and a recording
began three hundred milliseconds later that nobody was holding.
UnigramDev/Unigram/record-start-on-press • 3069c66 • 1 files, +21/-4
Close off opening the device on the press
Warming the device up under the hold timer works, but it marks the microphone
and the camera as accessed for a tap that only switches mode, and lights the
privacy LED on hardware that has one. Written down so it isn't tried again.
#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 • 0889840 • 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.
UnigramDev/Unigram/record-start-on-press • e892215 • 1 files, +3/-10
Drop two flags the timer and the session already answer for
_recordAudioVideoRunnableStarted said what _timer.IsEnabled says, and
_calledRecordRunnable was only ever read where a recording exists, which the
session knows.
UnigramDev/Unigram/record-start-on-press • 4da4557 • 1 files, +8/-1
Don't record for a press that was let go while rights were checked
The check is awaited between the press and the timer, so a release during it
left the press running: the timer started with no pointer down, and a recording
began three hundred milliseconds later that nobody was holding.
UnigramDev/Unigram/record-start-on-press • 3069c66 • 1 files, +21/-4
Close off opening the device on the press
Warming the device up under the hold timer works, but it marks the microphone
and the camera as accessed for a tap that only switches mode, and lights the
privacy LED on hardware that has one. Written down so it isn't tried again.
#unigram
UnigramDev/Unigram/develop • cf37ec8 • 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/develop • 134180d • 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.
UnigramDev/Unigram/develop • e71214d • 1 files, +3/-10
Drop two flags the timer and the session already answer for
_recordAudioVideoRunnableStarted said what _timer.IsEnabled says, and
_calledRecordRunnable was only ever read where a recording exists, which the
session knows.
UnigramDev/Unigram/develop • 57630df • 1 files, +8/-1
Don't record for a press that was let go while rights were checked
The check is awaited between the press and the timer, so a release during it
left the press running: the timer started with no pointer down, and a recording
began three hundred milliseconds later that nobody was holding.
UnigramDev/Unigram/develop • 86ad43f • 1 files, +21/-4
Close off opening the device on the press
Warming the device up under the hold timer works, but it marks the microphone
and the camera as accessed for a tap that only switches mode, and lights the
privacy LED on hardware that has one. Written down so it isn't tried again.
#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/develop • 134180d • 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.
UnigramDev/Unigram/develop • e71214d • 1 files, +3/-10
Drop two flags the timer and the session already answer for
_recordAudioVideoRunnableStarted said what _timer.IsEnabled says, and
_calledRecordRunnable was only ever read where a recording exists, which the
session knows.
UnigramDev/Unigram/develop • 57630df • 1 files, +8/-1
Don't record for a press that was let go while rights were checked
The check is awaited between the press and the timer, so a release during it
left the press running: the timer started with no pointer down, and a recording
began three hundred milliseconds later that nobody was holding.
UnigramDev/Unigram/develop • 86ad43f • 1 files, +21/-4
Close off opening the device on the press
Warming the device up under the hold timer works, but it marks the microphone
and the camera as accessed for a tap that only switches mode, and lights the
privacy LED on hardware that has one. Written down so it isn't tried again.
#unigram
telegramdesktop/tdesktop/nightly • 74da0c3 • 1 files, +30/-1
Enabled LTO in Release configuration on Windows CI.
telegramdesktop/tdesktop/nightly • f7c7386 • 1 files, +24/-11
Switched macOS CI to Release build with LTO on nightly.
#tdesktop
Enabled LTO in Release configuration on Windows CI.
telegramdesktop/tdesktop/nightly • f7c7386 • 1 files, +24/-11
Switched macOS CI to Release build with LTO on nightly.
#tdesktop
🫡3
telegramdesktop/tdesktop/nightly • 9ac4970 • 1 files, +35/-1
Enabled LTO in Release configuration on Windows CI.
telegramdesktop/tdesktop/nightly • eb2a3dc • 1 files, +24/-11
Switched macOS CI to Release build with LTO on nightly.
#tdesktop
Enabled LTO in Release configuration on Windows CI.
telegramdesktop/tdesktop/nightly • eb2a3dc • 1 files, +24/-11
Switched macOS CI to Release build with LTO on nightly.
#tdesktop
🫡3