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/develop0f035f13 files, +56/-9
Make VideoAnimation::Stop actually able to stop a decode

Nothing in the app has ever called Stop, so neither of these has been
exercised. They matter now because the cancellation survey points at Stop as
the lever for aborting a preview render, and it would not have worked.

stopped was a plain bool, written by Stop from whichever thread wants to abort
and read by the decode loop and by the IO callbacks running under it. It is
now std::atomic<bool>. Every use is a plain load or store, so only the
declaration changed. Stop still does not take m_lock, and that is deliberate:
taking it would block until the decode it is trying to interrupt had finished.

readCallback returned 0 once stopped. ffmpeg reads 0 as "no bytes this call"
rather than as a failure, so it can keep asking and spin instead of unwinding
the demuxer. It returns AVERROR_EOF now. seekCallback had the same shape, where
returning 0 reports a successful seek to offset 0 and sends the demuxer back to
the start rather than letting it fail out.

Not built: Telegram.Native needs the vcpkg ffmpeg setup, so this is unverified
by a compiler. std::atomic<bool> needs <atomic>, which is now included.

Left alone next door: seekCallback's live path has the same 0 return when
SetFilePointerEx fails, reporting a successful seek to the start. Same class of
bug, but on the path that plays video rather than the one that aborts it, so it
wants someone who can run it.

Also closes 2.3 in the review doc as won't-fix, with the reasoning. Probing a
media item twice is real, but the probe answers what a file is and how big
before the item can be published, and the thumbnail answers what it looks like
on realization and has to stay releasable. Every way of collapsing the two puts
back what moving the thumbnails off the models took away.

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

UnigramDev/Unigram/develop200e65f1 files, +20/-10
Correct what probing a video actually costs

The note closing 2.3 said the video path pays a real avformat_open_input plus
find_stream_info twice. That overstates it: VideoAnimation already has a light
mode and the probe already uses it.

LoadFromFile takes preview and probe flags that exist for this. preview sets
AVFMT_FLAG_NOBUFFER so nothing is buffered, and probe skips the frame and
packet allocation along with the no-video-stream bail. StorageVideo.CreateAsync
and StorageAudio.CreateAsync both ask for both, so the first pass is headers
only. The photo probe is the same shape, since BitmapDecoder.CreateAsync reads
a header rather than decoding.

So what runs twice is a header read plus a real decode, not two decodes. The
conclusion is unchanged and the reasoning is stronger: the design already has a
cheap probe mode and deliberately uses it, which is about as good as a lazy,
releasable thumbnail gets.

Also records an inconsistency noticed while checking, without acting on it:
ImageHelper loads with preview false, so its format context does buffer, but
passes preview true to RenderSync, which is what raises the retry count to 50.
Whether NOBUFFER helps or hurts a single frame grab is a measurement.

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

#unigram
UnigramDev/Unigram/develop18da5f63 files, +51/-27
Stop materializing the draft on every caret move

OnSelectionChanged allocated the whole message as a string on every keystroke
and every caret move, and then mostly didn't use it.

TryGetAutocomplete never read the text or query it was handed at all - only its
sticker branch reads the text, and only when the whole message is a single
emoji, so it reads it there. The inline bot search is dead without a bot to
address, so it doesn't run without one. And SearchByInlineBot only matches a
username that starts the message, which CharacterAt answers without reading the
rest.

That leaves the common path - typing ordinary text - reading no text at all.

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

UnigramDev/Unigram/develop469d6de2 files, +45/-4
Say why the frame grab does not ask for NOBUFFER

ImageHelper loads video with preview false while passing preview true to
RenderSync, which reads like an oversight and is not one. The flag sets
AVFMT_FLAG_NOBUFFER, which is what the header-only probe in
StorageVideo.CreateAsync wants, but unbuffered often fails to grab the first
frame -- the whole point of these three call sites. Right for a probe, wrong
for a frame grab. Now stated at each of them, since it is exactly the sort of
inconsistency someone tidies up later.

Also adds task 8 to the review doc: enqueue a share as it is typed rather than
after. SendMessagesView exists only to send what was shared, and still types
the whole set before building and sending message after message, so nothing
reaches the network until the slowest file has been probed. For a share of
large videos that is the probe delaying the upload rather than the UI, and the
first file could be going out while the last is still being read.

The shape is there for it: ChooseChatsViewModel.SendWithChat resolves options
and topic synchronously and never looks at the content, so it can be called
once per chat up front and each message sent to the captured chats as it is
built. Four things need deciding rather than wiring, and are written down --
grouping needs lookahead to close an album, the caption currently rides
positionally on the last item, the progress bar divides by a total that would
grow underneath it, and cancel gains a window where it must stop the probe as
well as delete what was already sent.

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

UnigramDev/Unigram/developc22b1985 files, +569/-546
Add article option to attach

UnigramDev/Unigram/develop03736741 files, +1/-1
Fix font family

UnigramDev/Unigram/develop9aea06e3 files, +14/-1
Better chat view inst

UnigramDev/Unigram/developf42aeeb1 files, +7/-1
Add transcode logging

UnigramDev/Unigram/developd3ec4f11 files, +66/-13
Optimize slide panel

#unigram
UnigramDev/Unigram/develop19633c71 files, +4/-0
Copy rich messages as formatted text

UnigramDev/Unigram/develop98836981 files, +4/-0
Ship tdjson symbols in the appxsym (#3338)

tdjson.dll is added straight to ReferenceCopyLocalPaths, which is an output
of reference resolution rather than an input, so ResolveAssemblyReference
never sees it and never runs the related-file discovery that picks up a .pdb
sitting beside a resolved reference. RLottie gets its symbols for free that
way; tdjson has to name the file.

The appxsym is built from AppxPackagePayload filtered to .pdb, and payload
includes copy-local items, so naming it here is enough. PDBs are removed
from the .appx itself afterwards, so this does not grow the installed app.

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

UnigramDev/Unigram/develop1cb16192 files, +45/-16
Fix NullReferenceException when the chat list loads before its template (#3339)

OnLoaded dereferenced ScrollViewer.ContentTemplateRoot, but ScrollViewer is
only assigned in OnApplyTemplate, which runs on the first measure pass. A
control that is in the tree and never measured raises Loaded with the template
parts still null, and the handler threw.

The element it wants is the ItemsPresenter the template already declares, so
name it and read it with GetTemplateChild like the other parts. That drops the
dependency on the ScrollViewer's ContentPresenter having realized, and the
setup can then run from whichever of OnApplyTemplate and Loaded arrives second.

Returning early instead would have left _trackerOwner null for the session and
the swipe-between-folders carousel silently dead.

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

#unigram
UnigramDev/Unigram/develop8ab18d32 files, +90/-30
Keep the originating description when an unhandled error is a bare E_FAIL (#3340)

* Keep the originating description when an unhandled error is a bare E_FAIL

A large share of unhandled errors reach OnUnhandledExceptionDetected as an
E_FAIL with no message and a stack that only shows Propagate() rethrowing it.
GetStowedException already tries to recover the real context, but it returns
null unless every step succeeds, so those reports carry nothing.

Read the description out of IRestrictedErrorInfo before giving up, and return
what was recovered even when the stowed frames are unavailable. The call was
already written out in a comment and left unused on the assumption that the
propagated managed exception would carry the details - which is exactly what
fails here.

WatchDog no longer overwrites the recovered string with the empty one from the
propagated exception.

* Read the rest of the stowed exception record

GetStowedException2 required ExceptionForm 1 and returned null otherwise, so
three fields the record always carries went unused:

- ResultCode, the HRESULT the error was stowed with, before propagation
flattened it to E_FAIL. This is the field that actually distinguishes one
failure from another.
- ThreadId, the thread it originated on, which is not necessarily the one whose
stack ends up in the report.
- ErrorText, which form 2 carries instead of a stack, and which was discarded
along with the whole record.

It also returned null when no frame resolved a module base, dropping the above
and the nested record with them. Now a FatalError is returned either way.

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

#unigram
telegramdesktop/tdesktop/nightly651f9ba3 files, +23/-23
Switched nightly Windows CI with optimized binaries without arm64.

#tdesktop
🫡2
telegramdesktop/tdesktop/nightly6d512312 files, +23/-15
Switched nightly Windows CI with optimized binaries without arm64.

#tdesktop
🫡2
UnigramDev/Unigram/theme-current-share-target7a4368b1 files, +11/-2
Register the theme before reading its settings

Theme.Current is thread-static and assigned in exactly one place, and that
assignment sat behind a call that can fail:

try
{
_isolatedStore = ApplicationData.Current.LocalSettings.CreateContainer(...);
Current ??= this;
...
}
catch { }

When CreateContainer throws, the catch is silent and the theme never registers
itself. Every later Theme.Current dereference - and they are unguarded across
the message tree, FormattedTextBlock, MessageBubblePanel, ChatBackgroundControl
- is then a NullReferenceException on that view thread.

Reported by crash telemetry on 12.9.1, through OnShareTargetActivated:
SharePage's constructor calls ChatBackgroundControl.Update, which reads
Theme.Current.ChatBackground and dies. The share target view is driven before
it is initialized - the same race as #3320, which moved this crash one frame
later rather than removing it - and ApplicationData is evidently not always
reachable that early.

That the constructor ran at all is not in doubt: SharePage.xaml resolves
StaticResource EmptyHyperlinkButtonStyle from CommonStyles.xaml, a merged
dictionary of Application.Resources that follows <common:Theme /> in the merge
list, so InitializeComponent could not have returned unless the resources of
this view - the theme among them - had been inflated.

Assign Current before the try. It depends on nothing inside it, and losing a
persisted preference should not cost the view its theme. The catch now logs,
since the whole diagnosis rests on inferring which call threw, and an empty
catch is what made that necessary.

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

#unigram
UnigramDev/Unigram/business-hours-continue1cfe0032 files, +98/-9
Implement BusinessHoursViewModel.ContinueImpl

The Done button on Settings > Telegram Business > Opening Hours is bound to
BusinessFeatureViewModelBase.Continue, which calls the abstract ContinueImpl.
BusinessHoursViewModel never implemented it and threw NotImplementedException,
so pressing Done crashed the app every time.

Build a BusinessOpeningHours from the per-day ranges and send it with
SetBusinessOpeningHours, following the other business feature view models:
early-out when nothing changed, toast an error, otherwise go back. Override
HasChanged so the unsaved-changes prompt the base class already carries a
string for actually fires.

The time zone is now resolved on every navigation rather than only when the
user already had opening hours saved, falling back to the first zone at the
current UTC offset, since otherwise a first-time user would have none to send.

Reported by crash telemetry on 12.9.1.

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

#unigram
UnigramDev/Unigram/dwrite-collection-keyc1bf81a2 files, +40/-11
Give the DWrite custom font collection a valid key

CreateCustomFontCollection takes the key size in bytes, but it was given
path1.size() + path2.size(), the summed character length of the two package
paths (~240) — while the key itself is a two-element array of pointers, 16
bytes. DWrite copied ~240 bytes out of a 16-byte stack array on every
PlaceholderImageHelper construction, so the key it stored was partly unrelated
stack memory, and how much varied with the install path length.

The key also pointed at two locals that die when the function returns, so the
pointers DWrite kept were dangling by the time it could hand them back to
CreateEnumeratorFromKey. The paths and the pointer array now live inside the
CustomFontLoader, which stays alive as long as it is registered, and the size
passed is sizeof that array. CustomFontFileEnumerator honours collectionKeySize
instead of assuming two entries.

Close() now unregisters the loader it registered on the shared DWrite factory,
after releasing the collection built from it. The factory is process-wide, so
without this every window thread left a loader and a custom collection on it for
the rest of the session.

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

#unigram
UnigramDev/Unigram/gift-variants-win10d730aa92 files, +6/-2
Fix InvalidCastException opening the gift variants popup on Windows 10

ListViewItemPresenter.SelectedBorderBrush, SelectedPointerOverBorderBrush and
SelectedPressedBorderBrush are declared on IListViewItemPresenter4, introduced
in UniversalApiContract 13.0 (build 22621). On older builds the QueryInterface
behind the setter fails and the projection throws, so GiftVariantsPopup crashed
as soon as the first container was realized.

Guard the three assignments on the property actually being present. Where it is
not, the selection border keeps its default brush.

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

#unigram
UnigramDev/Unigram/invite-links-full-info5e526bd3 files, +18/-2
Let the invite link actually arrive on ChatInviteLinksPage

ChatInviteLinksViewModel declares _supergroupId and _basicGroupId but never
assigns them, so Handle(UpdateSupergroupFullInfo) and
Handle(UpdateBasicGroupFullInfo) compared against 0 and never matched. The
"full info will arrive by push" fallback in OnNavigatedToAsync was therefore
dead, and InviteLink stayed null for every chat whose full info wasn't already
cached — which made the header's copy and share buttons dereference null.

Assign both ids, and fix the basic group branch, which asked for
GetBasicGroupFullInfo(supergroup.Id) inside the branch where TryGetSupergroup
had just failed: supergroup is null there, and it was passing a supergroup id
where a basic group id belongs.

InviteLink is also legitimately null for anyone who isn't the creator, since
TDLib only populates the primary link for them, so the header buttons are now
disabled while it is null instead of acting on nothing.

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

#unigram
UnigramDev/Unigram/location-access-guardec3172b1 files, +12/-1
Guard the geolocation access request in SendLocationPopup

Geolocator.RequestAccessAsync is a remote procedure call and throws when the
Windows Geolocation Service (lfsvc) is disabled, which privacy/debloat scripts
routinely do. In FindLocation the call was the only one not wrapped, so the
throw escaped an async void method and reached the unhandled exception handler.

Treat a failed request as "not allowed": the popup keeps the map shimmer, the
same as an explicit denial.

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

#unigram
UnigramDev/Unigram/ocr-extract-recoveryc3efb811 files, +64/-20
Recover from a damaged OCR model archive

Extracting Ocr_x64.zip crashed the app with an InvalidDataException,
"The archive entry was compressed using an unsupported compression
method". The message is misleading: Inflater maps zlib's Z_DATA_ERROR
onto that string, and the failure came from mid-entry writes in
DeflateStream.CopyToAsyncStream rather than from ZipArchiveEntry.Open,
so the deflate data was corrupt or truncated, not compressed with a
method the reader doesn't support. Why the local copy was damaged is
not established.

ExtractModelAsync had no try/catch and RecognizeText is async void, so
the failure reached the app unhandled. Catch it, delete the archive so
that the next attempt downloads a fresh copy, delete the partially
written model so that the readiness check in EnsureReadyAsync doesn't
accept it, and report the extraction as unavailable.

Separately, _extractLock.Release() sat outside any finally, so a throw
during extraction leaked the semaphore for the rest of the session:
every later extraction then returned early at Wait(0), leaving OCR
broken until restart.

Reported by crash telemetry on 12.9.1.

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

#unigram
UnigramDev/Unigram/profile-topics-context-menue3b452d1 files, +19/-8
Fix the Topics tab context menu casting to the wrong item type

ProfileTopicsTabPage.OnContextRequested was an unadapted copy of the one in
ProfileSavedChatsTabPage: it cast the right-clicked item to SavedMessagesTopic,
which is always null here because this list holds ForumTopic, and it invoked
the saved chats tab's commands instead of the topics tab's own. Right-clicking
any row threw a NullReferenceException, reported by crash telemetry on 12.9.1.

Cast to ForumTopic, route pin/delete to TopicsTab, and gate them on
CanManageTopics the way ForumView's own topic menu does.

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

#unigram
UnigramDev/Unigram/voip-video-device-lost8a6a6791 files, +5/-1
Discard the video resources when the rendering device is replaced

VoipVideoOutput's RenderingDeviceReplaced handler only flipped m_resourcesValid
back on, keeping the YUV420 effect and its three input bitmaps, all of which were
realized on the device that had just been replaced. The next frame of the same
size copied into those bitmaps and drew the effect on a context BeginDraw returned
from the new device, which fails with D2DERR_WRONG_RESOURCE_DOMAIN, "The resource
was realized on the wrong render target".

Call the existing ReleaseShader() from the handler, under the m_deviceMutex it
already holds, so the next frame recreates everything against the new device. The
sibling surfaces already do this: FreeformGradientSurface nulls its bitmap and
re-invalidates, and MessageBubbleNineGrid caches no device-bound resources.

Also fix a self-comparison in RenderFrame: finalSize.cy was compared with itself
instead of m_surfaceSize.cy, so a height-only change never resized the drawing
surface.

Reported by crash telemetry on 12.9.1.

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

#unigram
UnigramDev/Unigram/pending-message-reinsert003181f1 files, +13/-5
Recompute the message index after removing it from the collection

InsertMessageInOrder moves a message by removing it and reinserting it at the
index NextIndexOf computed against the collection as it was *before* the
removal, compensating for the single row that goes away. MessageCollection
removes more than one row: RemoveItem also drops the date or topic separator
that the removal orphans, so a message that was the only one of its day takes
its header with it and the collection shrinks by two. If the message is not the
last row -- a sponsored message below it, say -- the adjusted index is then past
the end and Insert throws ArgumentOutOfRangeException.

Reported by crash telemetry on 12.9.1, reached when a locally pending message
completes: PendingMessage_Completed -> InsertMessage -> InsertMessageInOrder.

Ask NextIndexOf again after the removal rather than trusting the pre-removal
index; clamping would have put the message in the wrong place instead. The
force branch had the same defect -- it reinserted at oldIndex, which is out of
bounds once the header ahead of the message is gone -- so both paths now share
one helper.

Not built: the file parses with Roslyn, nothing more.

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

#unigram
UnigramDev/Unigram/chat-preview-teardown9b344a24 files, +38/-7
Tear down the chat preview from the flyout's Closed event

ChatCell.ShowPreview hosts a second ChatView inside a MenuFlyout and hung its only
teardown on chatView.Unloaded, which never fires for a flyout-hosted view: the log
tail of the crash runs for another 109 seconds without the handler's "Unloaded"
line ever appearing. ChatView.OnCollectionChanged therefore stays subscribed to the
view model's Items after the flyout's XAML peers are gone, and the next collection
change reads Messages.ItemsPanelRoot through a separated RCW, throwing
InvalidComObjectException.

Teardown now hangs off the flyout's own Closed event, with Unloaded kept as a
secondary trigger, and ChatView.Deactivate ignores a second call so the order of
the two doesn't matter. The two forum topic cells already tore down from
flyout.Closing and so never had the hole; they move to Closed for a single pattern.

Reported by crash telemetry on 12.9.1.

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

#unigram
UnigramDev/Unigram/develop56232451 files, +34/-5
Trim the namespace and class from native log lines

MSVC expands __FUNCTION__ to the fully qualified name, so every line logged from
Telegram.Native carried the same winrt::...::implementation:: prefix, followed by
a class the file name already gives:

[1786432732.490][AsyncMediaPlayer.cpp:376][winrt::Telegram::Native::Media::implementation::AsyncMediaPlayer::Close]

Keep only what follows the class, which is also what [CallerMemberName] passes in
from the managed side. Cutting at the last :: instead would have reduced a lambda
to a bare "operator ()", so the enclosing method is kept there.

The std::string temporary goes with it: to_hstring takes anything convertible to
string_view, and these names are well past the small string limit.

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

UnigramDev/Unigram/develop1b3d6a94 files, +683/-1
Copy/paste in Telegram Android's rich HTML format

ProseMirror serialized its own render DOM to the clipboard (div.pm-pullquote,
span.pm-spoiler and friends), which Android's RichHtml.parse flattens to loose
paragraphs: every quote, list and table lost its structure in both directions.

Adds a clipboardSerializer/clipboardParser pair speaking the dialect Android
writes and reads, so a selection round-trips between the two clients. Three
deliberate deviations, all of which its parser accepts:

- checklist state is emitted (it reads data-checkbox/data-checked but never
writes them), as bare attributes since it tests for presence;
- a nested list is written inside the <li> that owns it, which is what its
parser expects — it writes them as a sibling of the <li> and then drops them
on the way back in;
- nodes it has no tag for (math, buttons, anchors, mentions) ride in data-*
attributes on a tag it degrades sensibly.

Media follows the same trade as its RichMediaClipboard: the HTML carries only
the file id, the attrs behind it live in an in-process registry until the next
copy, and an id we can't resolve is dropped rather than pasted broken.

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

#unigram
UnigramDev/Unigram/developdbdb4b76 files, +271/-230
Give SendFilesPopup rows of its own

StorageAlbum was answering two questions at once. It means one outgoing
message, which is what GetItemsView produces, but the template selector read it
as "a row that draws a mosaic" -- and those disagree in both directions, so
UpdateCollection had to lie three times to bridge them.

A standalone muted video was wrapped in a one-item album with a negative
ordinal, to keep a thumbnail it would otherwise lose. A documents album was
expanded and its grouping thrown away. And a files-mode row had its item
wrapped in a StorageDocument purely to carry the glyph, which
ItemsView_CollectionChanged then unwrapped again to get back to the item the
popup actually holds.

The list now holds a StorageRow: a MosaicRow of media to draw as a mosaic, or a
FileRow of one item with the flag that used to be a wrapper object. The
selector asks the question it means. StorageAlbum goes back to being purely a
send grouping -- Ordinal, Update, the mosaic layout and the display constants
all moved to MosaicRow, and StorageAlbumPanel became MosaicPanel so the naming
stops contradicting itself.

That removes a bug rather than only tidying. OnContainerContentChanging set the
delete button's Tag to the displayed item and Remove_Click looks that up in
Items, but in files mode the displayed item was a wrapper that was never in
Items, so the removal found nothing: deleting a photo or video row while
sending as files did nothing at all. FileRow.Media is the item itself.

Two things kept as they were. Files mode still names the file rather than the
track, which needed an explicit guard once the wrapper stopped hiding the audio
type. And mosaic identity counts mosaic rows rather than list positions, so a
file row appearing between two of them does not renumber everything after it.

StorageDocument's wrapping constructor and its Original property go too, the
popup having been their only consumer.

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

UnigramDev/Unigram/developeb349af7 files, +1556/-22
Paste rich HTML into the chat field as blocks

Pasting into the chat field took plain text and nothing else, so copying out of
the rich editor lost every quote, list and table on the way back in.

Adds RichHtml.Parse: the clipboard HTML the editor writes — and the dialect
Telegram Android writes — read back into page blocks. Unlike Android, whose
editor holds a flat list of rows, this builds the nested pageBlock* tree TDLib
expects, and closes the tags HTML closes for you (an unclosed <p> otherwise
swallows the rest as one paragraph, and real clipboard HTML is full of them).

A paste becomes message text whenever TryGetFormattedText can say the whole
thing with entities. When it can't, only the app's own content — stamped with
data-telegram-rich on copy — reopens the rich editor, on the field's text split
around the pasted blocks: it's a separate window and a paid feature, so a
heading copied from a web page falls back to a plain paste instead.

Media is dropped. The HTML carries only a file id, which means nothing outside
the process that copied it, so a photo can't be rebuilt here — Android does the
same with an id it can't resolve, and an orphaned caption stays as a paragraph.
What markup can't express — a button's style and type — travels as the TDLib
JSON ClientJson reads back.

SendRichMessage now opens the editor on an existing rich draft rather than on
the text field, and PasteRichMessage splices into the same source of truth.

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

#unigram
UnigramDev/Unigram/develop12085972 files, +57/-1
Focus the editor when its window opens

The caret only ever appeared after re-activating the editor window. Two reasons,
both of them ordering:

TryFocus focused the document before the control. The JS view.focus() sets
document.activeElement inside a browser whose host has no focus — ProseMirror
even draws its selection — but the keyboard stays with XAML. Focusing the
control is what routes input into the web content; the document call only
places the caret, so it has to come second.

And ViewService.OpenAsync activates the window as soon as its content exists,
which is long before CoreWebView2 does. XAML's initial focus lands on the
WebView2 while it has no controller to forward it to, so that GotFocus goes
nowhere — and focusing it later is a no-op, because the control already has it.
Re-activating the window worked precisely because it raised GotFocus again.
Focus is now taken on activation (deferred, since XAML assigns its own while
activating) and, when the control is the one already holding it, moved off and
back so the transition actually happens.

Also drops the WebView2's Source: Initialize navigates to the same file through
the editor.unigram virtual host, so setting both loaded the whole editor twice
— two parses of the bundle and two ProseMirror mounts per open.

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

UnigramDev/Unigram/develop2302a741 files, +2/-2
Hook SizeChanged to the right container

UnigramDev/Unigram/develop5e572a32 files, +9/-7
Fix purchase button

UnigramDev/Unigram/develop34ecd822 files, +8/-0
Fix default editor state

#unigram
UnigramDev/Unigram/develop50ca95932 files, +2975/-60
Pin vcpkg with a manifest instead of a hand-prepared checkout

Setting up the unmanaged dependencies meant cloning vcpkg at a 2024 commit,
editing the ffmpeg portfile by hand to insert an 85-flag `--enable-*` string,
applying Libraries/vcpkg.patch to vcpkg's own MSBuild targets, running
`vcpkg integrate install`, and installing fourteen packages per triplet from
the command line. None of it was verifiable, and that baseline can no longer
build from scratch: the msys2 runtime package it pins has fallen off every
mirror, so vcpkg_fixup_pkgconfig fails with a 404.

vcpkg.json pins 2026.03.18 and declares what is actually used. That is nine
ports, not the fourteen that were installed: cppwinrt comes from the NuGet
package and flatbuffers from the submodule, while dav1d, libvpx and
libjpeg-turbo resolve transitively. The commit matches the one TDLib
documents, so openssl and zlib cannot drift between the tdjson.dll we ship
and the copies the app links -- they reach the flat package root under the
same names from both.

ffmpeg needs a modified portfile for its decoder list, so it is vendored as
an overlay port, taken from the registry at 7.1.2 with the flag line applied.
It wins over the baseline's 8.0.1, which keeps the sonames at avcodec-61 and
avoids ffmpeg 8 removing avcodec_close out from under tgcalls. Moving to 8 is
now a self-contained change.

Directory.Build.props finds vcpkg without any environment variable -- a
checkout beside the repository, or the copy that ships with Visual Studio --
and turns off the machine-wide integration so the pin cannot be bypassed. A
guard reports a checkout older than the pin, which otherwise fails as "no
version database entry", since vcpkg reads the version database from the
working tree rather than from the pinned commit.

The runtime DLLs are copied through ReferenceCopyLocalPaths rather than
vcpkg's applocal step, which made Libraries/vcpkg.patch necessary in the first
place. applocal does not run when the linker is skipped or when a project is
only queried for its packaging outputs, and the DLLs then go missing with no
error; the patch covered one of those cases and never the other. Declaring
them also puts them in the app's own output folder, which applocal never did.
openssl and zlib are excluded there because Libraries/tdjson ships the copies
tdjson.dll was linked against, under the same file names.

Libraries/tdjson/build.ps1 builds TDLib from the same manifest and the same
installed tree, so there is one openssl and one zlib in the build.

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

#unigram