Telegram github commits and releases
4.55K subscribers
601 files
20.3K links
Broadcast from the most important Telegram clients' repositories
Download Telegram
morethanwords/tweb/master2b00c4d6 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
🫡3
morethanwords/tweb/mastera381f833 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/master8314c578 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/master5db7cfb3 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/masterb21491c300 files, +811/-800
Build

#webk
🫡3
UnigramDev/Unigram/playback-reconcile3b08a553 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
UnigramDev/Unigram/develop39ce27d1 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/develop4878bfb2 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/develop4a164842 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/developa156a683 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/develop91479431 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/develop03f39d71 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/develop1ea35f41 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/develop1a017ae1 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/developfbb0e7f1 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/develop3f8a3382 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/develop53f18ac1 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/developbf724171 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/develop49d97ae1 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-shufflee6346dd3 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
telegramdesktop/tdesktop/nightly565c0765 files, +2/-67
Removed protobuf dependency from cld3 and build recipes.

#tdesktop
🫡3
telegramdesktop/tdesktop/nightlyaf5bb1b1 files, +2/-0
Enabled LTO in Release configuration on Windows CI.

#tdesktop
🫡3
telegramdesktop/tdesktop/nightly37b4ed41 files, +24/-11
Switched macOS CI to Release build with LTO on nightly.

#tdesktop
🫡3
UnigramDev/Unigram/developffdc0815 files, +31/-23
Update emoji to Unicode 17.0

UnigramDev/Unigram/develop37b0fc41 files, +0/-0
Trim emoji font size

UnigramDev/Unigram/develop6b4b9cd1 files, +3/-3
Fix shift when sending a message after many unread messages

UnigramDev/Unigram/develop761ab7b1 files, +1/-0
Clear children before add

#unigram
UnigramDev/Unigram/record-start-on-press51b52ea1 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-press08898401 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-presse8922151 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-press4da45571 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-press3069c661 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/developcf37ec81 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/develop134180d1 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/develope71214d1 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/develop57630df1 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/develop86ad43f1 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/nightly74da0c31 files, +30/-1
Enabled LTO in Release configuration on Windows CI.

telegramdesktop/tdesktop/nightlyf7c73861 files, +24/-11
Switched macOS CI to Release build with LTO on nightly.

#tdesktop
🫡3
telegramdesktop/tdesktop/nightly9ac49701 files, +35/-1
Enabled LTO in Release configuration on Windows CI.

telegramdesktop/tdesktop/nightlyeb2a3dc1 files, +24/-11
Switched macOS CI to Release build with LTO on nightly.

#tdesktop
🫡3
telegramdesktop/tdesktop/nightly396ada11 files, +38/-1
Enabled LTO in Release configuration on Windows CI.

telegramdesktop/tdesktop/nightly6cda4dd1 files, +24/-11
Switched macOS CI to Release build with LTO on nightly.

#tdesktop
🫡3
telegramdesktop/tdesktop/nightly4f1f0f53 files, +138/-1
Added packing of Lottie animations into tgs at build time.

telegramdesktop/tdesktop/nightly567462e226 files, +1071/-112
Replaced animation tgs blobs with plain json sources.

#tdesktop
🫡2
UnigramDev/Unigram/developec0529b1 files, +0/-0
Fix space char in emoji font

UnigramDev/Unigram/developb9ed1af1 files, +4/-1
Normalize log tail

UnigramDev/Unigram/develop19143711 files, +18/-2
Log focus manager exceptions

UnigramDev/Unigram/develop7189e2b1 files, +16/-0
Try to park window focus before swapping content

UnigramDev/Unigram/develop9d605821 files, +4/-1
Enable bubble tails only in high performance mode

UnigramDev/Unigram/developc3cdae31 files, +1/-1
Don't ignore source ImageBrush

UnigramDev/Unigram/developa99cbaf31 files, +222/-87
Update TDLib

UnigramDev/Unigram/develop09074193 files, +11/-12
Update webrtc to the 2026-08-19 archives, built with Visual Studio 18

UnigramDev/Unigram/develop6fcff0b6 files, +29/-0
Split generators into two projects

UnigramDev/Unigram/develop6fb1c6210 files, +230/-64
Support message ephemeral content

UnigramDev/Unigram/developa2e5b966 files, +63/-6
Handle unsupported rich message blocks

UnigramDev/Unigram/developc2b02391 files, +24/-0
Store community full info

UnigramDev/Unigram/developa16508c1 files, +3/-0
Unsubscribe events on unlock

UnigramDev/Unigram/developdafbee51 files, +7/-3
Fix VideoAnimation getting stuck on read loop

UnigramDev/Unigram/develop0769a061 files, +6/-1
Capture RPC_E_WRONG_THREAD too

#unigram
UnigramDev/Unigram/developba5dbcb13 files, +1220/-43
Animate deleted messages coming apart

A frozen visual surface captures at the compositor commit that follows the
freeze, so the view's mirror holds a removal back for one frame to give it one.
The view model applies updateDeleteMessages unchanged.

IsInitial becomes AnimationState, which is also how the view tells a deletion
from a row leaving the list for any other reason. The renderer is a diagnostics
setting, off by default in the Store build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

UnigramDev/Unigram/developc290b371 files, +8/-1
Try to fix null ref

UnigramDev/Unigram/develop24927ab1 files, +73/-9
Park focus asynchronously when swapping window content

Control.Focus answers before the focus pipeline has run, and reports success
even when a LosingFocus handler cancels the move underneath it. Focus stayed
on the passcode field, which was then detached with it, leaving the focus
manager pointing at an element XAML can no longer resolve a peer for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/developc3bf9374 files, +522/-14
Update notes, add changelog skill

#unigram