Telegram github commits and releases
4.44K subscribers
601 files
20.9K links
Broadcast from the most important Telegram clients' repositories
Download Telegram
UnigramDev/Unigram/develop • ebe5e84 • 1 files, +16/-8
fix: Update Shortcuts.md (#3193)

* fix: Update shortcuts

- Add missing formatting options;
- Reordered shortcuts to be in the same order, as they appear in the application;
- Add consistency (spaces between keys and `+` symbols) for shortcuts;

* increase rowspan

#unigram
🫡2
UnigramDev/Unigram/crash-message-selector-null-content-root • abd63ce • 1 files, +40/-5
Fix crash when MessageSelector loads before its content is realized

MessageSelector.OnLoaded built the TextSelectionManager unconditionally:

_textSelectionManager ??= new TextSelectionManager(this, ContentTemplateRoot);

TextSelectionManager's constructor rejects a null root, so whenever a
container was connected before its ContentPresenter had realized the content,
ContentTemplateRoot was still null and the constructor threw
ArgumentNullException out of a Loaded handler, which is unhandled and takes
the app down.

Reported by crash telemetry on 12.8.2 ("Value cannot be null. Parameter
name: root"). The reported log tails show it under heavy UI churn — chat
navigation with popups opening — which is when layout is most likely to lag
behind connection.

Attach as soon as a root is actually available instead: on load when the
content is already realized, otherwise on the first arrange, by which point
the presenter has always produced one. Selection is therefore attached within
the same layout pass and nothing is lost.

The arrange path is guarded by a field null check, so once attached it costs
one comparison and never allocates.

EnsureTextSelectionManager also checks IsConnected. Only OnUnloaded detaches
the manager, so attaching while disconnected would leave the pointer handlers
on the content root permanently and, through them, root the control for the
rest of the session.

The four selection accessors dereferenced the manager unconditionally, which
would have turned this window into a NullReferenceException instead; they are
now null-safe.

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

#unigram
🫡2💅1
UnigramDev/Unigram/crash-message-selector-null-content-root • 15b9841 • 1 files, +49/-5
Fix crash when MessageSelector loads before its content is set

MessageSelector.OnLoaded built the TextSelectionManager unconditionally:

_textSelectionManager ??= new TextSelectionManager(this, ContentTemplateRoot);

TextSelectionManager's constructor rejects a null root, so whenever a
container was connected before it had been given content, the constructor
threw ArgumentNullException out of a Loaded handler, which is unhandled and
takes the app down.

Reported by crash telemetry on 12.8.2 ("Value cannot be null. Parameter
name: root"). The reported log tails show it under heavy UI churn — chat
navigation with popups opening — which is when layout is most likely to lag
behind connection.

Attach whenever a content element is actually present instead: on load, and
again from OnContentChanged whenever the content itself changes. The control
is always handed a UIElement directly and is never initialized through a
ContentTemplate, so Content is the element to attach to and ContentTemplateRoot
was the wrong property to read in the first place.

OnContentChanged detaches first. The manager's pointer handlers live on the
previous content element, so they have to come off before attaching to the
new one, or they outlive the element they were added to and the manager keeps
reporting selection for content that is no longer displayed.

EnsureTextSelectionManager also checks IsConnected. Only OnUnloaded and
OnContentChanged detach the manager, so attaching while disconnected would
leave the pointer handlers on the content permanently and, through them, root
the control for the rest of the session.

The four selection accessors dereferenced the manager unconditionally, which
would have turned this window into a NullReferenceException instead; they are
now null-safe.

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

#unigram
🫡2
UnigramDev/Unigram/develop • b844676 • 1 files, +49/-5
Fix crash when MessageSelector loads before its content is set (#3316)

MessageSelector.OnLoaded built the TextSelectionManager unconditionally:

_textSelectionManager ??= new TextSelectionManager(this, ContentTemplateRoot);

TextSelectionManager's constructor rejects a null root, so whenever a
container was connected before it had been given content, the constructor
threw ArgumentNullException out of a Loaded handler, which is unhandled and
takes the app down.

Reported by crash telemetry on 12.8.2 ("Value cannot be null. Parameter
name: root"). The reported log tails show it under heavy UI churn — chat
navigation with popups opening — which is when layout is most likely to lag
behind connection.

Attach whenever a content element is actually present instead: on load, and
again from OnContentChanged whenever the content itself changes. The control
is always handed a UIElement directly and is never initialized through a
ContentTemplate, so Content is the element to attach to and ContentTemplateRoot
was the wrong property to read in the first place.

OnContentChanged detaches first. The manager's pointer handlers live on the
previous content element, so they have to come off before attaching to the
new one, or they outlive the element they were added to and the manager keeps
reporting selection for content that is no longer displayed.

EnsureTextSelectionManager also checks IsConnected. Only OnUnloaded and
OnContentChanged detach the manager, so attaching while disconnected would
leave the pointer handlers on the content permanently and, through them, root
the control for the rest of the session.

The four selection accessors dereferenced the manager unconditionally, which
would have turned this window into a NullReferenceException instead; they are
now null-safe.

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

#unigram
🫡2
UnigramDev/Unigram/develop • 6404539 • 1 files, +15/-6
Fix crash opening the context menu on a partially selected message (#3317)

Message_ContextRequested built a MessageQuote straight from the selection and
immediately dereferenced it:

quote = new MessageQuote
{
Quote = parent.GetSelectedSourceText(out selectionStart),
...
};
selectionEnd = selectionStart + quote.Quote.Text.Length;

MessageSelector.HasSelection only reports that a selection is currently shown.
It is a plain flag, tracked separately from the ranges the text actually comes
from, so it can be true while GetSelectedSourceText returns null — when there
are no selected ranges, when the range collapses to nothing in source offsets,
or when the underlying block no longer has source text (GetSourceText returns
null for either of the last two). The result was a NullReferenceException out
of an async void handler, which is unhandled and takes the app down.

Reported by crash telemetry on 12.8.2.

Take the selected text first and only build the quote when there is one.
Everything downstream already treats a null quote as "no quote" — the very
next block reads `if (quote != null && MessageQuote_Loaded(...))` and falls
back to a plain reply — so leaving it unset is the behaviour the caller
already expects.

This only makes the call site agree with what the API can return; the flag and
the ranges are still tracked separately in TextSelectionManager, which is worth
revisiting on its own.

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

#unigram
🫡2
UnigramDev/Unigram/crash-autocomplete-template-mismatch • 381ea2d • 1 files, +24/-14
Fix crash populating autocomplete rows from a mismatched template

Autocomplete_ContainerContentChanging walked the container's visual tree on
the assumption that it matches args.Item:

var content = args.ItemContainer.ContentTemplateRoot as Grid;
var title = content.Children[1] as TextBlock;
var description = title.Inlines[1] as Run;
...
description.Text = ...

The list uses ItemTemplateSelector, so containers are recycled across item
types that have different templates, and ContentTemplateRoot can still be the
previous type's tree when this fires. Every step of that walk is an `as` cast
whose result is dereferenced immediately, so a mismatch is a
NullReferenceException in an event handler — fatal, not caught anywhere.

Reported by crash telemetry on 12.8.2, landing on the QuickReplyShortcut
branch. args.InRecycleQueue was already handled; this is the other half of the
recycling problem, where the container is live but not yet re-templated.

Match the expected shape and bail out when it isn't there, rather than
indexing into whatever tree happens to be present. Applied to the reported
branch and to the User branch, which is structurally identical and has the
same latent fault.

The remaining branches index container children the same way and share the
pattern, but have no reported crashes; leaving them for now to keep the change
reviewable rather than rewriting the whole handler.

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

#unigram
🫡2
UnigramDev/Unigram/crash-sharetarget-null-window • 235fc66 • 1 files, +19/-2
Fix crash activating the app as a share target without a window context

InitializeFrame dereferenced WindowContext.Current unconditionally:

if (WindowContext.Current.Content == null)
{
WindowContext.Current.Content = CreateRootElement(e, WindowContext.Current);
}

WindowContext._current is [ThreadStatic]. It is assigned by the constructor,
which OnWindowCreated calls, and cleared again in OnShutdownCompleted when the
view's dispatcher shuts down. An activation arriving on a view that has been
through that shutdown therefore finds no context, and the app dies with a
NullReferenceException before it has a window.

Its only caller already allows for exactly this:

if (WindowContext.Current?.Content == null)
{
InitializeFrame(e);
}

The null-conditional makes a missing context indistinguishable from missing
content, so the branch is taken and InitializeFrame is called precisely when
there is nothing to dereference.

Build the context instead of assuming one. That is what OnWindowCreated would
have done, and it leaves the frame actually initialized rather than merely not
crashing, so the activation still gets a usable window. Where there is no
Window.Current either, there is nothing to build against; log and return.

Reported by crash telemetry on 12.8.1, all of it through OnShareTargetActivated,
and the code is unchanged on develop.

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

#unigram
🫡2
telegramdesktop/tdesktop/dev • 8e18cb7 • 1 files, +1/-0
Work around bugged D-Bus cmake config

It overrides pkg-config search paths but never restores them leading to pipewire being not found, disable the find_package call so that openal falls back to pkg-config directly

#tdesktop
🫡3
UnigramDev/Unigram/crash-sharetarget-null-window • e30f8e4 • 1 files, +38/-2
Fix crash activating the app as a share target before its window is created

InitializeFrame dereferenced WindowContext.Current unconditionally:

if (WindowContext.Current.Content == null)
{
WindowContext.Current.Content = CreateRootElement(e, WindowContext.Current);
}

Share target activation appears to race window creation: the view has a
Window, but OnWindowCreated has not run yet, so no WindowContext exists and
the app dies with a NullReferenceException before it has any content.

Its only caller already allows for this — it tests WindowContext.Current?.Content
— so the null-conditional makes a missing context indistinguishable from
missing content, and InitializeFrame is called precisely when there is nothing
to dereference.

Make CreateWindowWrapper idempotent, and build the context early when
activation gets there first.

The idempotence is the load-bearing half. WindowContext's constructor assigns
the thread-static Current, so constructing a second one for a view that
already has one replaces it and silently orphans whatever was attached to the
first — including the frame. Creating the context during activation is only
safe because the OnWindowCreated that follows now reuses it instead of
replacing it. It is compared by window rather than assumed, and a context
belonging to a different window is still replaced, as before.

Logging is added on each path — reusing, creating, replacing, activating
early, and activating with no window at all — since this is a race that only
shows up in the field and the log tail is the only way to see which way it
went.

Reported by crash telemetry on 12.8.1, all of it through OnShareTargetActivated,
and the code is unchanged on develop.

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

#unigram
🫡2
UnigramDev/Unigram/develop • 0d19263 • 1 files, +38/-2
Fix crash activating the app as a share target before its window is created (#3320)

InitializeFrame dereferenced WindowContext.Current unconditionally:

if (WindowContext.Current.Content == null)
{
WindowContext.Current.Content = CreateRootElement(e, WindowContext.Current);
}

Share target activation appears to race window creation: the view has a
Window, but OnWindowCreated has not run yet, so no WindowContext exists and
the app dies with a NullReferenceException before it has any content.

Its only caller already allows for this — it tests WindowContext.Current?.Content
— so the null-conditional makes a missing context indistinguishable from
missing content, and InitializeFrame is called precisely when there is nothing
to dereference.

Make CreateWindowWrapper idempotent, and build the context early when
activation gets there first.

The idempotence is the load-bearing half. WindowContext's constructor assigns
the thread-static Current, so constructing a second one for a view that
already has one replaces it and silently orphans whatever was attached to the
first — including the frame. Creating the context during activation is only
safe because the OnWindowCreated that follows now reuses it instead of
replacing it. It is compared by window rather than assumed, and a context
belonging to a different window is still replaced, as before.

Logging is added on each path — reusing, creating, replacing, activating
early, and activating with no window at all — since this is a race that only
shows up in the field and the log tail is the only way to see which way it
went.

Reported by crash telemetry on 12.8.1, all of it through OnShareTargetActivated,
and the code is unchanged on develop.

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

#unigram
🫡2
UnigramDev/Unigram/crash-vlc-dangling-audio-device • 3bd0033 • 1 files, +9/-1
Fix crash when the default audio device changes during playback

OnDefaultAudioRenderDeviceChanged handed libvlc a pointer to a string that
had already been destroyed:

Set(libvlc_audio_output_set, winrt::to_string(args.Id()).c_str());

Set forwards its arguments into a lambda that Write queues for the player's
worker thread, and captures them by value — which for a const char* copies the
pointer, not the characters. The temporary std::string returned by to_string
dies at the end of that statement, so by the time the work item is dequeued
libvlc reads freed memory.

The crash lands in strcmp, called from module_exists by
libvlc_audio_output_set as it compares the device name against known module
names, which is exactly what reading a freed string looks like.

Own the string in the capture instead, matching the Write call sites that
already exist in this file.

This fires whenever the system's default audio render device changes while a
player is alive — headphones plugged in or removed, a Bluetooth device
connecting, a call starting — which is why it is common enough to matter
despite needing a device change to trigger. Reported by crash telemetry on
12.8.1; the code is unchanged on develop.

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

#unigram
🫡3
UnigramDev/Unigram/develop • 6ea41c1 • 1 files, +9/-1
Fix crash when the default audio device changes during playback (#3321)

OnDefaultAudioRenderDeviceChanged handed libvlc a pointer to a string that
had already been destroyed:

Set(libvlc_audio_output_set, winrt::to_string(args.Id()).c_str());

Set forwards its arguments into a lambda that Write queues for the player's
worker thread, and captures them by value — which for a const char* copies the
pointer, not the characters. The temporary std::string returned by to_string
dies at the end of that statement, so by the time the work item is dequeued
libvlc reads freed memory.

The crash lands in strcmp, called from module_exists by
libvlc_audio_output_set as it compares the device name against known module
names, which is exactly what reading a freed string looks like.

Own the string in the capture instead, matching the Write call sites that
already exist in this file.

This fires whenever the system's default audio render device changes while a
player is alive — headphones plugged in or removed, a Bluetooth device
connecting, a call starting — which is why it is common enough to matter
despite needing a device change to trigger. Reported by crash telemetry on
12.8.1; the code is unchanged on develop.

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

#unigram
🫡2
UnigramDev/Unigram/develop • 7bb3a02 • 3 files, +390/-390
Update maximize icon

UnigramDev/Unigram/develop • a4f390f • 4 files, +140/-206
Untangle ImageHelper scaling and crop duplication

The "fit inside N" calculation was spelled out inline seven times. Replace it
with three named primitives — FitRatio, Fit and Clamp — so the difference
between a target and a ceiling is visible at the call site rather than implied
by a parameter named requestedMinSide that holds a maximum.

Drop both Scale overloads. They had no callers, and Scale(this Size, double)
was an exact signature match for SizeEx.Scale in Telegram.Controls, which
multiplies by a factor rather than fitting to a box. Which of the two a file
got depended only on the namespace it sat in.

CropAsync and CropAndPreviewAsync carried a copy each of the same 45 lines,
including the same TODO verbatim. Both now call ComputeCropTransform.

Fix the minimum-size branch, which took the smaller of the two ratios: that
brings the longer edge down to the floor and leaves the shorter one further
below it than it started, so a 100x400 crop with a minimum of 160 came back
40x160. No effect on the only live caller, whose crop is always square.

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

UnigramDev/Unigram/develop • 59b9050 • 3 files, +34/-17
Fix HEVC 10-bit video hanging VideoAnimation

decode_frame gated on a whitelist of pixel formats before the swscale path was
considered, so yuv420p10le was rejected outright and every frame returned -1.
The whitelist describes what libyuv can convert unaided, not what is supported;
it now gates only that fast path, and anything else goes through the scaler.

The alignment test likewise only chooses swscale in preference to libyuv, so it
applies only when a libyuv path exists. Without one the scaler is the only way
to convert the frame, and the scaling path already handles an unaligned
destination through an intermediate buffer.

RenderSync then spun. has_decoded_frames is set as soon as a frame is received,
before conversion, which freezes the try budget; the read-EOF branch does not
reset it. With conversion failing on every frame the loop decoded the file end
to end, seeked back to zero and repeated with no exit condition. A failed
conversion now spends a try, which bounds the loop regardless of the format.

RenderSync returns Boolean rather than Int32. It only ever returned 1 or 0, and
both internal callers already treated the result as a boolean.

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

#unigram
🫡2
UnigramDev/Unigram/develop • 27022ab • 1 files, +25/-6
Propagate video render failures out of ImageHelper

RenderSync answers whether it wrote a frame, and three call sites discarded it,
passing a BufferSurface that still held uninitialised memory to Encode or to a
WriteableBitmap.

Each needs a different answer. GetPreviewBitmapAsync returns null, which is
already its contract and which every caller substitutes a placeholder for.
CropAndPreviewAsync throws, because StorageMedia.Refresh recovers by falling
back to the unedited preview and does so from a catch, which a null would slip
past. OpenReadAsync throws, because callers pass the result straight to
BitmapDecoder and there is no null that does not simply move the failure; both
GenerationService and ImageCropper.SetSourceAsync catch it.

The blanket catch in GetPreviewBitmapAsync now logs. A codec the decoder cannot
handle was indistinguishable from a file with no frames.

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

UnigramDev/Unigram/develop • d4ac632 • 4 files, +208/-86
Remove per-frame allocations from message layout

Four allocations on paths that run for every message on every layout pass.

ReactionsPanel.ArrangeOverride built a List of rows and a List of rects per row
purely to centre them. A row is a contiguous run of children, complete as soon
as the next child does not fit, at which point its right edge is known, so each
run is now arranged in place. Both overrides also index Children rather than
enumerating it, since foreach over UIElementCollection allocates an
IEnumerator<UIElement> per pass.

LoadingTextBlock rebuilt its skeleton clip on every arrange: a native text
measurement, one D2D geometry per line, a group, a path, a path geometry and a
clip. It is now cached on the inputs it derives from.

ReplyMarkupInlinePanel rebuilt the rect buffers it hands to the native clip
builder, plus the clip itself, on every arrange. The buffers are reused and the
rects compared in place, so the clip is rebuilt only when one moves.

Discern gains an IList<UnreadReaction> overload over a shared core, so
ReactionsPanel no longer projects through Select on a list that is usually
empty.

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

UnigramDev/Unigram/develop • a2c8668 • 5 files, +28/-7
Release chart geometries built inside the draw pass

DrawChart and DrawPickerChart called CanvasGeometry.CreatePath once per line on
every frame and never disposed the result, leaving the native D2D geometry to
the finalizer. Seven sites now go through DrawGeometry and FillGeometry on
BaseChartView, which wrap the geometry in a using. The drawing session consumes
it before the call returns, so nothing outlives the scope.

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

#unigram
🫡2
UnigramDev/Unigram/develop • ba4ca6b • 1 files, +54/-18
Fix resource ownership and leaks in DrawStrokes

DrawStrokes is public, took a CanvasBitmap as a parameter and disposed it, so
any caller outside this file lost the bitmap it passed in. The caller now owns
both the bitmap it supplies and the render target it receives, and the summary
says so.

Nothing was scoped, so an exception between the two CanvasRenderTarget
constructions and the final dispose leaked all three objects. The body moves to
a private overload, leaving the public one to own the targets: the overlay is a
using and the returned target is disposed on the way out of a catch.

DrawStrokesAsync(SoftwareBitmap) leaked the InMemoryRandomAccessStream it
returns when SaveAsync threw. DrawStrokesAsync(StorageFile) relied on
DrawStrokes disposing the bitmap before the file was reopened for writing,
since it is loaded from that same path; the release is now explicit rather than
a side effect, and scoped to keep that order.

canvas1 and canvas2 become overlay and target.

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

UnigramDev/Unigram/develop • 532b8cf • 1 files, +20/-12
Drop dead parameters and stale docs from ImageHelper

CropAsync took a quality parameter that nothing read: the property set it was
written for is commented out. No caller passed it, and none passes more than
five positional arguments, so removing it does not disturb them.

CropAsync also took trimStart and never forwarded it to OpenReadAsync, so a
trimmed video would have been cropped at position zero. ScaleAsync already
forwards its own; this one now does too. No caller passes it yet, so nothing
changes today.

The doc block above ScaleAsync described a quality parameter the method has
never had, and omitted the ones it does. Both commented-out property sets
referred to that parameter by name.

RotateArea computed width - (width - area.X), which is area.X, through an
intermediate Point.

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

UnigramDev/Unigram/develop • bfb6d0d • 1 files, +3/-1
Accept every video container in OpenReadAsync

The frame extraction path tested for the literal ".mp4", so .mov and .m4v took
the image branch and failed inside BitmapDecoder. Constants.VideoTypes is the
set EditMediaPopup already uses to decide whether a file is a video, and the two
disagreed.

Uses the existing HasExtension helper, which compares ordinal and
case-insensitive against the array without copying it.

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

#unigram
🫡2
UnigramDev/Unigram/develop • a7f0db0 • 1 files, +34/-26
Honour requestedMinSide for every format in GetPreviewBitmapAsync

The method asked the decoder for its own BitmapPixelFormat, which
SoftwareBitmapSource accepts only when it happens to be Bgra8. The guard was
therefore testing the input rather than the output, and everything else fell
through to a BitmapImage decoded at full size, dropping requestedMinSide on the
path where it matters most.

Requesting Bgra8 premultiplied directly lets the decoder convert, which removes
the reason for the branch. One path, always the format SoftwareBitmapSource
needs, always bounded.

SoftwareBitmapSource holds a single frame, so an animated GIF would come back
static. Nothing reaches this with a GIF today, since StorageMedia previews
photos through a BitmapImage and only calls the overload above for video; the
summary records what a future caller would need.

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

UnigramDev/Unigram/develop • 8700dde • 9 files, +27/-36
Update nuget packages

UnigramDev/Unigram/develop • ec916c0 • 3 files, +3/-1
Update round font

UnigramDev/Unigram/develop • 3be5e09 • 2 files, +3/-3
Fix ephemeral messages icon

UnigramDev/Unigram/develop • af506e6 • 2 files, +4/-4
Update property name

UnigramDev/Unigram/develop • e8560fb • 1 files, +1/-1
Update MicroTeX

UnigramDev/Unigram/develop • ce8637e • 5 files, +72/-7
New unsupported message template

#unigram
🫡2
UnigramDev/Unigram/develop • c883cf7 • 1 files, +72/-3
Queue call transitions instead of blocking on the system call

TransitionToRequestingOrRinging blocked on InitializeSystemCallAsync, which
awaits the WinRT call reservation. VoipCoordinator.Handle calls Update from
inside its _activeLock on the TdReceive thread, and the VoipCall constructor
reaches the same transition, so the lock was held for the whole reservation —
including RequestNewIncomingCall and its 120 second ringing timeout. The UI
thread takes that lock through VoipCoordinator.ActiveCall, read from MainPage
and from the CallAccept and CallReject shortcuts, so it froze while a call was
being set up.

Awaiting alone would have introduced a worse problem. The block was also
ordering: a discard arriving while the reservation was in flight would reach
DisposeImpl with _systemCall still null, so nothing released it, and the
reservation completing afterwards would leave Windows showing an incoming call
that could no longer be ended.

Update now enqueues and returns, and a single pump applies transitions one at a
time in the order TDLib reported them. That keeps the ordering the block
provided while holding _activeLock only until the first await. The failure
handling matters: the catch sits inside the drain loop so one bad transition
cannot strand the rest, and _transitioning is cleared on the only exit, since a
call that stops draining would never reach Discarded.

UpdateImpl is otherwise the previous body unchanged, including its unlocked
read of _state, which races with OnStateUpdated and is left for its own change.

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

UnigramDev/Unigram/develop • 2180a5e • 1 files, +68/-19
Reserve the group call system call outside the constructor

All three VoipGroupCall constructors blocked on InitializeSystemCallAsync, and
VoipCoordinator runs each of them inside its _activeLock. The UI thread takes
that lock through VoipCoordinator.ActiveCall, read from MainPage and from
ChatGroupCallHeader, so it froze for the length of the reservation. The
ThreadPool.QueueUserWorkItem already wrapping those constructions kept the block
off the UI thread but did nothing about the lock.

Constructors cannot await, so the tail of each one moves into JoinAsync or
JoinConferenceAsync, started by the constructor and running after it returns.
The lock is then held only for field initialization. Ordering within the tail is
unchanged: reserve, create the window, then rejoin.

Leaving before the reservation completes needs handling that blocking made
unnecessary. Dispose would run with _systemCall still null, release nothing, and
the reservation completing afterwards would leave Windows holding a call that
can no longer be ended. Both paths check _isClosed once they have it and release
it themselves. _isClosed becomes volatile, since it is now read after an await
on a different thread from the one that sets it.

Dispose's system call teardown is extracted to DisposeSystemCall so the normal
and abandoned paths release it identically.

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

#unigram
🫡2
UnigramDev/Unigram/develop • 5d1c657 • 1 files, +20/-5
Notify the system call of the mute state on the conference path

Both conference constructors called TryNotifyMutedChanged before reserving the
system call, when _coordinator is still null, so the null-conditional swallowed
it and the initial mute state was never handed over. The video chat constructor
called it after, where it works.

Both now go through NotifyMutedChanged after the reservation. That reads the
manager into a local first: it runs after an await, and Dispose clears the field
from whichever thread ends the call.

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

UnigramDev/Unigram/develop • 196c8df • 1 files, +1/-1
Fix leak on voip coordinator exception

UnigramDev/Unigram/develop • e8922b2 • 1 files, +152/-0
Add crash-triage skill

Encodes the workflow for turning a production crash from the dashboard into a
fix: rank groups by affected users, symbolicate with crashctl, map the
crashing version back to its source, diagnose, and open a PR against develop.

The version mapping is the part worth writing down. Crash line numbers refer
to the released build, so reading them against a working tree silently points
at the wrong code — on the crash checked while writing this, the reported line
2341 lands exactly on the reporting function at the 12.8.1 bump commit, and is
past the end of the file in the current tree. The skill resolves the version
via its tag, falls back to the "Bump version to X" commit when the tag was
never pushed (v12.8.1 is one such case), and then requires verifying that the
reported line actually falls inside the reported function before any code is
read for diagnosis.

Work happens in a local clone, never in the user's working tree. The skill
also records the limits: never close a crash group (only a shipped release
proves a fix), never push to develop/main, and treat crash records as user
data.

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

UnigramDev/Unigram/develop • cb35007 • 8 files, +122/-380
Extract the duplicated skeleton placeholder builder

The same shimmer placeholder was written out seven times, in ReactionButton,
InteractionsView, SearchPostsTabPage, ChatView, GiftCraftChoosePopup,
ResoldGiftsPopup and StoryInteractionsPopup. Each built a list of rounded rects,
grouped them into a path geometry, and then repeated an identical gradient,
sprite shapes, clip and sweep animation, differing only in the rectangles.

VisualUtilities.SetSkeleton takes the element, the sweep extent and the shapes.
Two things fell out of merging them: every copy kept separate foreground and
background colours that were always assigned the same value, now one shimmer;
and StoryInteractionsPopup was the only one not consulting the theme for
MenuFlyoutItemBackgroundPointerOver, so it now matches the other six rather than
always using the six percent white fallback.

Shapes are params, since two call sites pass a fixed pair and the rest pass an
array that is forwarded without copying. That makes a zero-shape call
expressible, which is worth guarding anyway: the loop-driven callers already
reach it whenever their row count is zero, and an empty group clips everything
away, attaching a skeleton that renders as nothing.

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

#unigram
🫡2
UnigramDev/Unigram/develop • 5082051 • 1 files, +39/-4
crash-triage: make the app log tail a first-class diagnosis step

The log tail is the most useful part of a crash record and was only mentioned
as a flag. The stack says where the app died; the log tail says what it was
doing, which is usually what explains the crash.

Documents both halves: the state header (memory used/available, window size,
scaling, active calls, raw HRESULT) which is worth checking against the crash's
device spread, and the timestamped [File.cs:line][Method] trace, which carries
its own source locations and so corroborates the symbolicated stack
independently.

Diagnosis now requires reconciling stack, log tail and source at the reference
commit, rather than reasoning from the stack alone. Also notes that app logging
is currently sparse, so a missing log line is a legitimate follow-up to raise
in the PR.

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

UnigramDev/Unigram/develop • d36dec9 • 2 files, +48/-0
Handle clicks in gallery

UnigramDev/Unigram/develop • cfff648 • 11 files, +305/-23
Extend the orphan analysis to the gallery and mini apps

Analyze reports whatever is registered but unreachable from the roots it is
given, so it only works as a single call over the union of every area's roots:
running it per area marks every other area's live objects as leaked. ChatView
therefore stops calling it and exposes DebugRoots instead, MainPage assembles
the union, and each area contributes roots and a descent that return nothing for
types they do not own.

The gallery registers what a leak there actually costs: the window, its content
and transport controls, the compact overlay that outlives it on purpose, and the
player base, whose decoder and surfaces are the reason any of this matters. The
window stops being a root once nothing legitimately holds it, so anything still
referencing it past that point is the leak rather than the owner.

WebAppPage joins the same analysis, and its event subscriptions move with it.
Both constructors subscribed Window.Current.Activated, CloseRequested,
Consolidated and VisibleBoundsChanged, and neither unsubscribed, so the page
stayed reachable from the view for as long as the view lived. They now hook and
unhook in the UserControlEx OnLoaded and OnUnloaded pair, which is guaranteed to
alternate; the raw Loaded and Unloaded events fire again on every reparenting,
and CloseRequested and Consolidated take deferrals, so a second handler would
mean a second confirmation dialog rather than merely wasted work.

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

UnigramDev/Unigram/develop • b239b87 • 15 files, +154/-159
Code cleanup

UnigramDev/Unigram/develop • eb9411e • 7 files, +264/-15979
Improve rich editor

UnigramDev/Unigram/develop • bf24e59 • 2 files, +129/-13
Add buttons support in rich messages

#unigram
🫡2