Telegram github commits and releases
4.54K subscribers
601 files
20.3K links
Broadcast from the most important Telegram clients' repositories
Download Telegram
UnigramDev/Unigram/temp2b6f4ac1 files, +1/-1
Fix crash when sending non-text content with ephemeral bot commands (#3323)

CreateSendMessage maps anything that is not an InputMessageText to a null
caption, then dereferenced it unconditionally. Sending a sticker (or any
other media) in a chat where HasEphemeralBotCommands is set crashed with a
NullReferenceException.

Reported by crash telemetry on 12.9.0.0.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/tempe53bc6c1 files, +17/-0
Translate font and render target error messages (#3324)

DirectWrite's two font-file errors were missing from TranslateText entirely, so
every locale reported them as its own crash group. The render target message was
already translated, but only for a few locales.

Adds the localized variants observed in crash reports:

- font file not found: de, es, fr, it, pt, ru, tr, zh-Hans
- font file access denied: fr, ru
- wrong render target: es, fr, ru

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/temp3b186d21 files, +6/-0
Fix crash when opening the gallery (#3326)

ConvertOf is an x:Bind function binding over ViewModel.SelectedItem, and
Bindings.Update runs from Load before an item has been selected, so the very
first evaluation dereferenced a null GalleryMedia.

ConvertCaption in the same file already guards its input the same way.

Reported by crash telemetry on 12.9.0.0.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/temp63a834d1 files, +3/-1
Fix crash when rendering the wallpaper preview bubbles (#3327)

* Fix crash when rendering the wallpaper preview bubbles

The plain-run fast path wrote whatever font size had changed, including 0, and
XAML rejects a FontSize of 0 with E_INVALIDARG. GetOrCreateRun already treats
non-positive as "inherit" and clears the property instead; the fast path now
does the same.

MessageBubble.Mockup reaches this: it calls SetText without a font size, so the
default of 0 replaces the size the bubble was previously rendered with, and the
transition is exactly what triggers the write.

Reported by crash telemetry on 12.9.0.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Use the computed font size in the fast path

Per review: _fontSize is the raw value, fontSize is what AutoFontSize resolved
it to, and the slow path already passes the resolved one to GetOrCreateRun. The
fast path writing the raw value is the actual defect, so write the resolved one
instead of clearing the property — clearing would inherit the parent's size
rather than applying the theme size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/temp21628b71 files, +5/-1
Never report a negative desired height from SettingsPanel (#3328)

The IsHeader branch subtracts the trailing gap that follows the last child, but
the accumulator starts at 0 when IsFooter is also set, so a panel with nothing
visible reported -16. XAML rejects a negative desired size.

Both flags are set together on ShareGroupCallPopup, ChatInviteLinkInfoPopup and
FolderPage, so the combination is reachable; no crash report is currently tied
to it, this is a latent defect found while reading the layout code.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/temp26e36721 files, +3/-1
Fix the expander never collapsing its content (#3329)

The scoped batch's Completed handler compares _tracker against a value captured
with a post-increment, so the comparison was (n + 1) == n and never held. The
handler is what applies Visibility.Collapsed after the collapse animation, and
PopupRoot.Visibility is set to Visible unconditionally just above it, so
collapsed content stayed visible and kept participating in layout — hidden only
by PopupHost.Height = 0 and the negative margin.

Pre-increment gives the guard its intended meaning: apply the final visibility
unless a newer toggle has started since this batch began.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/tempd675e791 files, +8/-0
Fix crash when copying an empty selection or an absent note (#3330)

CopyText(XamlRoot, FormattedText) dereferenced its argument immediately, but its
callers pass values that are legitimately null: TextSelectionManager's
GetSelectedText returns null when nothing is selected, BlockQuote passes the
result of FormattedTextBlock.GetSelectedText, and ProfileHeader passes
UserFullInfo.Note, which is null when the contact has no note.

The method is async void, so the NullReferenceException was posted to the
dispatcher and terminated the app instead of being catchable.

Reported by crash telemetry on 12.9.0.0.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/temp7d0d7be1 files, +2/-2
Only log the voip ready state when it changes (#3331)

OnStateUpdated logged before the guard that ignores an unchanged state, so a
source repeating the same state filled the whole log window with identical
lines — one crash report held nothing but Failed, every entry stamped in the
same millisecond, which cost us everything that preceded it.

OnSignalBarsUpdated just below already logs from inside its change guard.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/tempea40fcc1 files, +36/-13
Keep the log tail in a ring buffer (#3332)

Three things were wrong with the window that ships in every crash report.

It was a List trimmed with RemoveAt(0), so once full every log call copied all
retained entries down one slot. Logging happens on hot paths; this is now an
overwrite of one slot.

Dump stored its "Bump" marker as an entry, so taking a dump evicted a real line
and a second dump left the first marker behind. It is appended to the output
instead, and Dump no longer mutates anything.

The size was an inline 50, which is under a second of ordinary logging. It is
now a named constant at 200.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/tempda46bee1 files, +1/-0
Add todo note

UnigramDev/Unigram/temp066672a1 files, +10/-4
Optimize hashset pop

UnigramDev/Unigram/temp320dce97 files, +127/-8
Experimental bubble content recycling

UnigramDev/Unigram/tempd642d862 files, +10/-0
Clear text block pool on navigate

UnigramDev/Unigram/tempe7ffacf8 files, +113/-57
Improve recycling for some contents

UnigramDev/Unigram/temp99d1e141 files, +255/-0
Started to work on WebRTC issues

UnigramDev/Unigram/tempc71aa982 files, +0/-3
Code cleanup

UnigramDev/Unigram/temp74469722 files, +0/-0
Update tlottie to latest dev

UnigramDev/Unigram/temp9090b241 files, +4/-4
Update webrtc todo

UnigramDev/Unigram/tempc533f042 files, +2/-2
Bump version to 12.9.1

#unigram
UnigramDev/Unigram/devicelostba693f92 files, +41/-13
Don't recreate the Direct3D device from the device-lost callback

PlaceholderImageHelper watches its D3D device with RegisterDeviceRemovedEvent
and recreated the device straight from the threadpool wait callback. That runs
while the display driver is still resetting, and D3D11CreateDevice at that
moment faults inside the vendor user-mode driver (reported by crash telemetry
on 12.9, on two different Intel UMDs).

The device is now rebuilt lazily instead: PlaceholderHelper already calls
HandleDeviceLost() on every access to the singleton, so the next use recreates
it from the UI thread. HandleDeviceLost tolerates a null device, which is the
state left behind when creation failed.

Two related lifetime fixes in the same path:

- CreateDeviceResources released nothing before calling D3D11CreateDevice.
com_ptr::put() overwrites without releasing, so recreating over the removed
device stranded it and everything the driver held behind it.

- PlaceholderImageHelper::Close() called StopWatchingCurrentDevice(), which
closes the threadpool wait but does not wait for a callback that is already
running -- the callback then reached RaiseDeviceLostEvent on an object that
was being destroyed. The drain that already existed in ~DeviceLostHelper is
now a Shutdown() method that Close() calls, and it was dead code before
because Close() had already cleared the handle it checks.

Not built: no UWP/.NET Native build environment here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/instant-empty-table3b632791 files, +7/-0
Handle an instant view table with no rows

ProcessTable computed its column count with table.Cells.Max(...), which throws
InvalidOperationException: Sequence contains no elements when the table has no
rows at all. It reached OnApplyTemplate through InstantContent.UpdateView, so
the whole message failed to render. Reported by crash telemetry on 12.9.1.

There is no grid to build for an empty table, so render just the caption --
which is what the method already falls back to, and callers of ProcessBlock all
handle a null return.

Not built: no UWP/.NET Native build environment here. Verified the file still
parses with Roslyn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/swapchain-diagnosticsdaaee852 files, +50/-5
Log why the video swap chain failed, and don't crash when it does

AsyncMediaPlayerSwapChain::Create() swallowed every failure and returned false,
so the only trace left was SwapChainOptions() throwing "You must wait for the
VideoView to be loaded before calling GetSwapChainOptions()" -- a message
inherited from LibVLCSharp that describes a lifecycle mistake this code cannot
make. The real cause is that one of the Direct3D calls failed, and nothing
recorded which one. That throw crossed a XAML Loaded handler (gallery video,
stories, the music player all construct AsyncMediaPlayer there) and became an
unhandled exception, reported by crash telemetry on 12.8.1 and 12.9.

Create() now names each stage as it goes and logs the stage, the HRESULT and
the message, so the next report says which driver call failed. The thrown
message says what actually happened.

The player no longer fails construction over it: the Direct3D device can
legitimately be unavailable while the display driver is resetting, and there is
nothing the caller could do about it. When the swap chain is not loaded the
player is built with --no-video instead, so audio still plays and the failure
is in the log rather than in a crash.

Also releases m_d3d11Device/m_deviceContext between adapter attempts --
com_ptr::put() overwrites without releasing, so a failed attempt could strand a
device.

Not built: no UWP/.NET Native build environment here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/settingsbutton-disabled0b6ba981 files, +4/-5
Don't animate a deferred template part from SettingsButton's Disabled state

SettingsButton's Disabled visual state animated the Foreground of three template
parts, one of which -- DescriptionPresenter -- is x:Load="False" and is only
realized by OnApplyTemplate when Description is non-empty. A Storyboard whose
TargetName cannot be resolved fails with E_INVALIDARG, and because the initial
visual state is applied while the template is being applied, that is inside the
control's measure. The ArgumentException therefore surfaces from the measure of
whatever is above it, which is why the reported stacks anchor on
HeaderedControlPanel and SettingsPanel and name nothing closer to the cause.

It needs the button to be disabled before its template is applied, which is what
happens when the disabled state is inherited: on Settings > Power saving the
HeaderedControl carries IsEnabled, so its SettingsExpanders -- whose ActionButton
has no Description -- are born disabled. Settings > Data and storage has the same
disabled expanders but sets Description, so the part is realized and the target
resolves. Reported by crash telemetry on every version from 12.4.1 to 12.9.1.

Dropping the key frame is enough: DescriptionPresenter is already painted with
SystemControlDisabledChromeDisabledLowBrush, so the disabled appearance is
effectively unchanged.

Checked the rest of the app for the same shape: this is the only
Storyboard.TargetName in any template that points at an x:Load="False" element.
The VisualState.Setters that target deferred parts (RecentChoosers in the message
reaction templates) are left alone -- those resolve differently and do not throw,
as reactions demonstrate every day.

Not built: no UWP/.NET Native build environment here. The file was checked for XML
well-formedness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/settingsbutton-disabledb852e7b1 files, +10/-5
Keep the disabled description dimming, realize the part instead

Dropping the key frame left the description on its own brush while the title
dimmed, so on pages that do set Description the two no longer matched. Restore
the key frame and take x:Load off DescriptionPresenter instead, so the target
always resolves.

The disabled appearance is now exactly what it was before this branch; the cost
is one collapsed, unmeasured ContentPresenter per SettingsButton. The other two
optional parts keep their deferral -- nothing animates them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/settingsbutton-disabled4b571511 files, +6/-4
Defer DescriptionPresenter with Lazy instead of dropping the deferral

x:DeferLoadStrategy="Lazy" realizes the element when something references it,
including a visual state, so the Disabled storyboard resolves without the part
being materialized up front. x:Load does not, which is what made the target
unresolvable. The stock TextBox template deferrs HeaderContentPresenter the same
way for the same reason.

Keeps the deferral the previous commit had given up, and the disabled appearance
is still what it was before this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/develop4eda2af3 files, +26/-52
Probe VoipCaptureBase before get_self

VoipManager::Start cast the descriptor's capture straight to VoipVideoCapture
with get_self, which reinterprets the ABI pointer without checking it. The
capture is whichever one is active when the call turns ready, so sharing the
screen before that point handed a VoipScreenCapture to the cast and m_impl was
read at the wrong offset.

The other three call sites already probed with try_as; move that chain into
GetVideoCaptureImpl and use it everywhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/developd2005801 files, +17/-5
Drop group call frames when the E2E delegates are missing

OnE2EEncryptDecrypt returned the input untouched when the delegate was null,
which puts plaintext on the wire while encrypting and feeds ciphertext to the
decoder as if it were plaintext while decrypting. The window is real: the
delegates are installed after the manager is constructed, and cleared again
before Stop.

tgcalls drops the frame when the transform returns empty, which is what the
managed side already does when TDLib fails, so return empty here too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/developfef656e1 files, +2/-5
Fix out-of-bounds write reading VoipDescriptor.PersistentState

The loop indexed into an empty vector. Nothing sets PersistentState today so it
never ran, but it would corrupt the heap the moment something did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/developd4f76122 files, +18/-1
Survive a loopback capture that fails to start

OnStopCapture dereferenced m_AudioClient unconditionally, but StopCaptureAsync
accepts the error state and a failed activation never produces a client, so
stopping after a failed start crashed. Process loopback is unavailable on older
builds, which is exactly when that happens.

The MFStartup was also left dangling on that path, since StopCaptureAsync is
what pairs it. Track it and shut down from the destructor if we never got there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/developc4bbb772 files, +19/-9
Hold CLoopbackCapture by reference, and only for screencast

It is a ref-counted RuntimeClass whose queued MF work items hold references of
their own, so storing it by value freed its storage out from under them. Hold a
ComPtr instead, and create it only on the screencast path that uses it, which
also drops the StopCaptureAsync every other call was making into a manager that
had never captured anything.

Skip the loopback when it fails to start rather than sharing the screen with an
audio capture that is not running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/develop5356eba1 files, +8/-4
Clear the group call E2E delegates after Stop, not before

Both teardown paths cleared them first, leaving a window where tgcalls was still
live and could ask us to transform a frame with no delegate installed. They are
only cleared to break the native to managed cycle that would otherwise leak the
call, and that works just as well once the instance is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/develop668badd6 files, +101/-46
Join requests accessibility

UnigramDev/Unigram/develop30abc2e1 files, +57/-5
Improve toasts accessibility

UnigramDev/Unigram/developb469c6a1 files, +302/-0
Track the VoipManager review findings

Catches the doc up to the P0 fixes already committed; from here it moves in the
commit that closes each item.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/develop806e4793 files, +35/-9
Stop re-registering the same incoming video sink

tgcalls appends sinks and only drops them once the weak_ptr expires, but the
page re-sets the output on every remote media state change where video is
active, always with the same sink. Each one appended another entry, so a single
frame was rendered once per registration.

Remember the last sink and skip a repeat. Weakly, so that dropping the sink
stays the way the output is detached.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/developc9247d63 files, +23/-6
Fire each broadcast task completion exactly once

The deferral is handed to managed code, which can raise it more than once, and
nothing stopped the second call from re-entering tgcalls with a completion it
had already consumed. Latch _done after firing, in all three task impls.

OnMediaChannelDescriptionsRequested was the one doing it, deferring on the
null-participants path and then falling through — where it dereferenced the null
and took down an async void before it ever reached the second deferral.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/develop2d25c442 files, +10/-11
Report an empty broadcast part as NotReady

An empty payload was reported as Success, leaving tgcalls to decode a zero-byte
part instead of asking again. Only a non-null, non-empty part is a success.

While here, drop the second copy of the payload: it was read out through the
IVector iterator and then memcpy'd into another vector.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/develop647b6682 files, +19/-12
Harden the descriptor conversion in VoipManager::Start

Read the encryption key with one GetMany instead of 256 GetAt calls through a
property fetched each time. It also clamps to what is there rather than throwing
hresult_out_of_bounds out of Start, and drops a copy via a stack array.

pushStun skips an empty host but pushTurn did not, so a server without IPv6
contributed a TURN entry pointing at nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/develop1e7c8952 files, +5/-11
Return the debug info without leaking it

The hand-rolled UTF-8 conversion leaked its buffer on every call and returned it
uninitialized if MultiByteToWideChar failed. The correct one-liner was already
there below it, unreachable — the source of the C4702 warning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/develop28996492 files, +36/-23
Guard three loose ends in VoipGroupManager

Screencast unmuted the instance directly, leaving m_isMuted stale so IsMuted
reported the opposite of the truth; route it through the setter. A screencast
with no audio to send stays muted, which is what it means.

The quality switch had no default, so an unrecognised value read an
uninitialised local. AddIncomingVideoOutput took get_self on the sink without
checking it, unlike its VoipManager counterpart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/developa1e3ccf2 files, +93/-73
Limits popup accessibility

UnigramDev/Unigram/develop6393fdb1 files, +1/-1
Fix rich messages translation

UnigramDev/Unigram/develop73473171 files, +3/-3
Fix polls/checklists translation

#unigram
UnigramDev/Unigram/developa9bef419 files, +452/-559
Rewrite the loopback capture as VoipLoopbackCapture

It was a near-verbatim copy of the Microsoft ApplicationLoopbackAudio sample and
the one piece of this project still on WRL and WIL. Now on C++/WinRT: implements
instead of RuntimeClass, which brings agility so FtmBase goes too, and com_ptr,
handle and slim_mutex in place of the wil types.

The METHODASYNCCALLBACK macro and its four offsetof-derived callbacks are gone.
Only the sample pump needs the MMCSS work queue, so start, stop and finish are
direct calls now and one IMFAsyncCallback is left. That also removes the
unbounded wait on m_hCaptureStopped: Stop takes the lock the sample callback
holds, so it waits for a packet read at most.

Three bugs in the packet loop came out with it. The copy length was taken from
GetNextPacketSize but used after GetBuffer, which may report fewer frames.
AUDCLNT_BUFFERFLAGS_SILENT was ignored, and a silent buffer's contents are
undefined, so silence went out as whatever was in memory. The samples handler
was called without checking it was set; it is a constructor argument now.

CLSID_YUV420Effect was being defined by whichever translation unit happened to
reach the initguid.h that LoopbackCapture.h dragged in. VoipVideoOutputSink.cpp
includes it deliberately now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/developea548fe1 files, +12/-5
Fix crash when searching while no tab is selected (#3325)

Keep SelectedTab in range instead of guarding its readers

Per review: nothing deselects a tab, so the question was where -1 comes from.

TopNavView.SelectedIndex is bound TwoWay, and a selector reports -1 whenever it
holds no selection — which it does while its ItemsSource is being attached.
BindableBase.Set stores the value and raises the notification before returning,
so the existing guard, which ran after Set and only checked the upper bound,
had already let -1 into the field. Tabs[value] on the next line was reading at
-1 too; the reported crash simply landed later, in Query, on the next
keystroke.

Validating before the value is committed keeps the field in range for every
reader — Query, ItemsView, IsTopChatsVisible and the setter itself — so the
guard added to Query in the first commit is no longer needed and is reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/developa10164e2 files, +41/-13
Don't recreate the Direct3D device from the device-lost callback (#3333)

PlaceholderImageHelper watches its D3D device with RegisterDeviceRemovedEvent
and recreated the device straight from the threadpool wait callback. That runs
while the display driver is still resetting, and D3D11CreateDevice at that
moment faults inside the vendor user-mode driver (reported by crash telemetry
on 12.9, on two different Intel UMDs).

The device is now rebuilt lazily instead: PlaceholderHelper already calls
HandleDeviceLost() on every access to the singleton, so the next use recreates
it from the UI thread. HandleDeviceLost tolerates a null device, which is the
state left behind when creation failed.

Two related lifetime fixes in the same path:

- CreateDeviceResources released nothing before calling D3D11CreateDevice.
com_ptr::put() overwrites without releasing, so recreating over the removed
device stranded it and everything the driver held behind it.

- PlaceholderImageHelper::Close() called StopWatchingCurrentDevice(), which
closes the threadpool wait but does not wait for a callback that is already
running -- the callback then reached RaiseDeviceLostEvent on an object that
was being destroyed. The drain that already existed in ~DeviceLostHelper is
now a Shutdown() method that Close() calls, and it was dead code before
because Close() had already cleared the handle it checks.

Not built: no UWP/.NET Native build environment here.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/develop8b5b9742 files, +50/-5
Log why the video swap chain failed, and don't crash when it does (#3334)

AsyncMediaPlayerSwapChain::Create() swallowed every failure and returned false,
so the only trace left was SwapChainOptions() throwing "You must wait for the
VideoView to be loaded before calling GetSwapChainOptions()" -- a message
inherited from LibVLCSharp that describes a lifecycle mistake this code cannot
make. The real cause is that one of the Direct3D calls failed, and nothing
recorded which one. That throw crossed a XAML Loaded handler (gallery video,
stories, the music player all construct AsyncMediaPlayer there) and became an
unhandled exception, reported by crash telemetry on 12.8.1 and 12.9.

Create() now names each stage as it goes and logs the stage, the HRESULT and
the message, so the next report says which driver call failed. The thrown
message says what actually happened.

The player no longer fails construction over it: the Direct3D device can
legitimately be unavailable while the display driver is resetting, and there is
nothing the caller could do about it. When the swap chain is not loaded the
player is built with --no-video instead, so audio still plays and the failure
is in the log rather than in a crash.

Also releases m_d3d11Device/m_deviceContext between adapter attempts --
com_ptr::put() overwrites without releasing, so a failed attempt could strand a
device.

Not built: no UWP/.NET Native build environment here.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/develop9b8ba301 files, +7/-1
Don't animate a deferred template part from SettingsButton's Disabled state (#3336)

* Defer DescriptionPresenter with Lazy instead of dropping the deferral

x:DeferLoadStrategy="Lazy" realizes the element when something references it,
including a visual state, so the Disabled storyboard resolves without the part
being materialized up front. x:Load does not, which is what made the target
unresolvable. The stock TextBox template deferrs HeaderContentPresenter the same
way for the same reason.

Keeps the deferral the previous commit had given up, and the disabled appearance
is still what it was before this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/developa04d5d41 files, +7/-0
Handle an instant view table with no rows (#3335)

* Handle an instant view table with no rows

ProcessTable computed its column count with table.Cells.Max(...), which throws
InvalidOperationException: Sequence contains no elements when the table has no
rows at all. It reached OnApplyTemplate through InstantContent.UpdateView, so
the whole message failed to render. Reported by crash telemetry on 12.9.1.

There is no grid to build for an empty table, so render just the caption --
which is what the method already falls back to, and callers of ProcessBlock all
handle a null return.

Not built: no UWP/.NET Native build environment here. Verified the file still
parses with Roslyn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
Ajaxy/telegram-tt/master383d4323 files, +5/-5
12.0.38

Ajaxy/telegram-tt/masterca9857410 files, +144/-2
Status Picker: Add roll effect (#7115)

Ajaxy/telegram-tt/masterac049361 files, +1/-1
Poll: Fix sending quizes with no solution text (#7116)

Ajaxy/telegram-tt/masteraf03e8330 files, +1916/-463
GramJS: Protocol hardening (#7098)

Ajaxy/telegram-tt/master9a7b7401 files, +7/-1
Albums: Fix separators for albums (#7119)

Ajaxy/telegram-tt/mastera3faeb04 files, +239/-218
[Dev] Bump Vite (#7120)

Ajaxy/telegram-tt/master549ac261 files, +1/-1
Composer: Fix sending forward on Enter (#7117)

Ajaxy/telegram-tt/masterc8049f144 files, +1309/-618
Middle Header Panes: Redesign + Refactoring (#7096)

Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>

Ajaxy/telegram-tt/masteredaa9321 files, +10/-3
Radial Pattern Background: Fix color (#7113)

Ajaxy/telegram-tt/masterc06de3a22 files, +398/-374
Left Search: Redesign (#7111)

Ajaxy/telegram-tt/master69bb99050 files, +2236/-1028
Support server wallpapers (#7076)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
Co-authored-by: Dmitry Kabanov <153344039+dmitrykabanovdev@users.noreply.github.com>

Ajaxy/telegram-tt/master2305f821 files, +6/-1
Audio Player: Fix transition animation (#7123)

Ajaxy/telegram-tt/mastereaa99563 files, +24/-12
Forward Recipient Picker: Support quick forward to saved messages (#7122)

Ajaxy/telegram-tt/master0cc747111 files, +80/-177
Manage Group Members: Fix search data (#7121)

#webz
🫡2
Ajaxy/telegram-tt/master6786a057 files, +49/-7
Wallpapers: Follow up (#7125)

Ajaxy/telegram-tt/masterc770a3877 files, +1922/-1358
Settings: Redesign icons (#7124)

Ajaxy/telegram-tt/master9716ca22 files, +24/-11
UI: Fix blink on long first load (#7129)

Ajaxy/telegram-tt/master37f136424 files, +476/-301
Stickers: Migrate to TLottie (#7128)

Ajaxy/telegram-tt/master911c59c22 files, +221/-122
[chore] Avoid Record<string, unknown> (#7130)

Ajaxy/telegram-tt/master8ae788a3 files, +83/-123
Vite: Fix startup speed (#7131)

Ajaxy/telegram-tt/master1211dd57 files, +1326/-1791
Tauri: Bump dependencies (#7133)

Ajaxy/telegram-tt/master786f1ff8 files, +2/-87
Service Messages: Remove changelog (#7134)

Ajaxy/telegram-tt/masterc5a2bff1 files, +14/-6
Message List: Fix scroll restore (#7135)

Ajaxy/telegram-tt/masterd915b1b300 files, +2984/-2890
[Build]

#webz
🫡2
UnigramDev/Unigram/develop390bfc85 files, +44/-62
Take the manager locks off the event and callback paths

winrt::event synchronises itself, so m_lock only served to serialise callbacks
against subscribe and unsubscribe — at the price of a lock order inversion with
the managed side, which unsubscribes from inside its own lock while a tgcalls
thread holds this one inside managed code that takes that same lock. Nothing
closed the cycle today, but it was there to be closed.

VoipManager keeps no lock at all now. VoipGroupManager keeps one for the only
state that is really shared, the encrypt and decrypt delegates, and copies them
out rather than holding it across the call: the managed side blocks on a TDLib
round trip in there, and it was doing that while holding the same mutex as every
audio level update and every subscribe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/developd94416d5 files, +22/-3
Stop both managers from their destructor

Stop is what the managed side calls, but nothing guarantees every path gets
there, and destroying a manager without it skips tgcalls' own teardown and
leaves a screencast still capturing loopback audio. Stop is idempotent, so
calling it again from the destructor costs nothing when it already ran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/developd75db132 files, +5/-4
Use std::mutex in the loopback capture

winrt::slim_mutex is not deprecated, but every other mutex in Telegram.Native
and Telegram.Native.Calls is std::mutex and this one is uncontended anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/developb4767514 files, +57/-41
Cut the per-frame interop out of the group call callbacks

The E2E path rebuilt its result with a push_back loop over an IIterator, which
is two COM calls per byte on every frame of a conference call; one GetMany into
a pre-sized buffer does it instead. Audio levels appended to the IVector one
participant at a time, ten times a second for the length of the call, and are
now filled into a reserved vector and handed over whole.

Same treatment for ReceiveSignalingData. SetRequestedVideoChannels stops
building its vector when there is no instance to give it to, and Protocol's
comparator stops copying both strings on every comparison.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/develop278ca6e4 files, +61/-42
Clear the remaining hygiene items in the call managers

The tgcalls version registrations had internal linkage but lived in a header, so
a second includer would have registered every version twice; they move into
VoipManager.cpp. The #ifndef _WIN32 arm of the config initialiser could never
have compiled and is gone.

A screencast runs a second group manager alongside the main one and both opened
tgcalls_group.txt, interleaving into one file; the screencast writes its own now.

EmitJoinPayload no longer completes when there is no instance to emit from. The
empty payload it used to hand back only got the caller as far as a join the
server would reject, and it did it on the calling thread rather than the one the
success path answers on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/develop5fb96f25 files, +309/-1
Base service message for community add/remove

UnigramDev/Unigram/develop03513e42 files, +22/-4
Let a null incoming video sink reach tgcalls

Skipping out early on null was wrong twice. It left m_incomingVideoOutput
pointing at the sink the caller had just asked to detach, and it swallowed the
one thing a null is good for: setIncomingVideoOutput also assigns _currentSink,
which every newly negotiated video channel is handed on creation, so a channel
appearing after the detach re-attached the old sink.

Null flows through as an empty shared_ptr now. The dedupe check handles it
without a special case, since two nulls in a row compare equal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/develop16ee8b22 files, +26/-2
Set responseTimestamp on every broadcast part status

NotReady is precisely the status tgcalls reads responseTimestamp on, to decide
where to restart a stream that has not begun yet, and it was only ever set on
the Success path — so it arrived as zero. The caller had been supplying it all
along and we dropped it.

It is also the one timestamp in this API measured in seconds rather than
milliseconds; tgcalls multiplies it back up by 1000. We passed milliseconds
straight through, a thousand times too large. Converting at the tgcalls boundary
keeps the WinRT surface consistently milliseconds, which is what
requestCurrentTime already expects.

Found by re-reading the commit that started reporting empty parts as NotReady,
which is what made this reachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/develop30ce5405 files, +502/-195
Close the lock leak in the ClientService critical sections

Thirteen Monitor.Enter/Monitor.Exit pairs had no try/finally, so anything that
threw in between leaked the lock for good. The worst is UpdateChatLastMessage,
which constructs a MessageAlbumLastMessageService while holding the Chat: one
throw there locks that chat forever, the next UpdateChatPosition for it blocks
the TDLib receive thread, and since that thread is the only one draining
td_receive, every update in the app stops arriving with no crash to show for it.
GetChatFolders takes lock (chat) from the UI thread, so the chat list wedges too.

All thirteen are plain lock blocks now. The ten synchronous ones are a straight
wrap. The three paging methods could not be, because await is illegal inside
lock — which is exactly why they were written with a hand-placed Monitor.Exit
before the await in the first place. Each now decides under the lock how much is
still to be loaded and either builds its result and returns inside the lock, or
falls out of it and awaits with nothing held. Same semantics, same lock ordering,
one exit path instead of three.

The MOCKUP blocks in those three methods moved but were left as they are. MOCKUP
is not defined in any configuration and the blocks reference an undefined index
variable, so they have not compiled in a long time; quietly repairing dead code
that cannot be tested does not belong in a lock-safety change.

UpdateChatDraftMessage was not in the review that prompted this — it turned up
grepping for Monitor. after converting the twelve that were listed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/develop533cbd93 files, +335/-147
Close the lock leak in the two topic services

The same Monitor.Enter/Exit-without-try/finally as the ClientService partials,
in the six pairs the earlier pass did not reach: it only covered ClientService.*,
and these two services are separate files owned by it. Four in ForumTopicService,
two in DirectMessagesChatTopicService, and the freeze mode is the same — a throw
between a pair leaks _order for good, and the topic list for that chat never
sorts again.

LoadForumTopicsAsync was the interesting one. It called tsc.SetResult inside the
monitor, so the continuation waiting on it — GetForumTopicsAsyncImpl, which takes
_order itself — ran inline while the lock was held. Monitor being recursive is
the only reason that was a surprise rather than a deadlock, and a throw in there
would have skipped the Exit. It now builds the result under the lock and
completes the task after releasing it.

The two paging methods got the same restructure as the three before them: decide
under the lock how much is still to load, then either answer from the cache
inside it or fall out and await with nothing held. That is five copies of this
method carrying the same bug, which is a better argument for merging them than
the duplicated line count ever was.

UpdateTopicOrder keeps publishing outside the lock, where the hand-placed
Monitor.Exit already put it. The aggregator publish still inside the lock in
LoadForumTopicsAsync is a separate finding and is left alone here.

Also records the review of both services in clientservice-review.md: seventeen
findings, of which these six are the first fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/developc6038343 files, +104/-74
Close the lock leak in the group call participants and chat positions

An app-wide grep for Monitor.Enter after the topic services turned up four more
files with the same unprotected pairs. Two of them are worth fixing now.

TdExtensions.GetPosition and GetOrder are the ones that matter. Each takes an
early Monitor.Exit inside a loop and another after it, and what they lock is the
Chat itself — the same objects ClientService.OnResult locks on the receive
thread. A throw inside AreTheSame on the UI thread would have leaked that chat's
monitor and stopped update delivery for the whole app, which is the same failure
the ClientService pass was closing. return inside lock releases correctly, so
both lose their early exits entirely.

VoipGroupCallParticipants turns out to be a sixth copy of the paging method,
with the same two shapes as the five already converted, so it gets the same
treatment: decide under the lock how many participants are still to load, then
answer from the cache inside it or fall out and await with nothing held. Its
three return paths are unchanged, including the null for a response that is
neither Ok nor Error.

That the count of this method went from three to five to six as the search
widened is a better argument for merging them than the duplicated line count.

DiceView and VideoNoteContent have the same bug and are left for now: both are
in UI code rather than on the receive thread, so they can wedge a control but
not the update pipeline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/developd9a6e672 files, +15/-2
Ignore a second VoipManager::Start

Assigning over m_impl would destroy the running instance without tgcalls' own
teardown. Nothing calls Start twice today, but it was the last hole left in the
Start/Stop lifecycle after the destructors went in.

Keeping the instance in flight is the right way round: it is the one serving the
live call, and a second descriptor would be for that same call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/develop3321c862 files, +46/-6
Sort the offered call protocols numerically

The versions were compared as strings, so the registered set

2.7.7 5.0.0 7.0.0 8.0.0 9.0.0 10.0.0 11.0.0

came out as 9 8 7 5 2.7.7 11 10 — the two newest protocols at the end of a list
the server reads newest first. Filed during the review as latent, on the
assumption the majors were single digits. They have not been for a while.

Compared component by component as numbers now, checked against that set and
for the strict weak ordering std::sort requires.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram
UnigramDev/Unigram/develop5b731d11 files, +48/-17
Reverse the ConcurrentDictionary recommendation in the review

The review ranked swapping ReaderWriterDictionary for ConcurrentDictionary as
the largest measurable win in ClientService. That was reasoning about the cost
of one lookup without ever asking how many lookups happen, and counting them
turns the recommendation around.

A full ChatCell refresh reaches a ReaderWriterDictionary eight to twelve times.
The rate of those refreshes is bounded by UI control realization — a 64px row
under a hard flick, plus aggregator refreshes of the dozen visible cells — which
puts the whole app somewhere around 0.5k to 6k lookups a second. At the ~30ns a
lock-free read would save, that is under two tenths of a millisecond per second,
or a few hundredths of one percent of a core. Wrong by a factor of a hundred it
still would not reach two percent, and at one operation per 200 microseconds the
cross-core contention that motivated the idea never happens either.

ConcurrentDictionary would cost a node allocation per entry to buy that back,
which is the wrong side of the trade in this repo. The custom class stays.

What survives is that ReaderWriterDictionary.Find allocates a closure and a LINQ
enumerator on every call to do what a foreach does for free, which is worth
fixing on its own and needs no type change.

Two things the count corrected on the way: GetChatActions is already a
ConcurrentDictionary, and GetChatFolders uses a plain Dictionary under its own
lock. Neither was ever in scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

UnigramDev/Unigram/develop4efdfba11 files, +85/-130
Pass call payloads as arrays instead of IVector<byte>

Every byte crossing this boundary ends up base64 encoded into TDLib JSON, so an
IVector was the wrong shape at both ends: native built a COM collection, the
projection built an RCW, and the managed side called ToArray to get back the
byte[] the generated TDLib request wanted anyway.

UInt8[] instead. The projection copies once in each direction and managed hands
the array straight to TDLib. On the E2E path, which runs per frame, that removes
two COM objects, two RCWs and one of three copies.

Signaling stops being an event. It only ever had one listener, so a delegate
does the same job without an args runtime class per packet, and
SignalingDataEmittedEventArgs goes away entirely.

The frame transform also stops waiting on a shared semaphore. tgcalls builds one
frame transformer per simulcast layer and one per incoming channel, each on its
own thread, so several transforms run at once as soon as a call has two people
in it — a second Release before the first Wait would have thrown
SemaphoreFullException, and a waiter could be woken by another frame is answer.
Holding m_lock across the delegate used to serialise them and hide this; it
stopped doing that when that lock came off the callback path. Each transform now
waits on its own signal, with a timeout so a silent TDLib drops the frame rather
than wedging a media thread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

#unigram