UnigramDev/Unigram/develop • 206109c • 3 files, +6/-2
Update libvlc build script
UnigramDev/Unigram/develop • 45d151b • 2 files, +32/-13
Probe a batch of files concurrently
The batch CreateAsync overload was still the serial loop the popup stopped
using, and the share target had become its only remaining caller, so sharing
thirty photos typed them one at a time before anything was sent.
It is now an ordered wrapper over ProbeAsync: concurrent, with results written
into a positional array so the order the caller gave survives, and failures
dropped at the end. Same contract, so no call site changes.
Order matters to the one caller: SendMessagesView attaches the caption to the
last item of the grouping, and GetItemsView needs the whole set before it can
group at all.
Streaming does not apply there and is not attempted. That view is an animation
and a progress bar, swapped in after the chats have been picked, so there is no
list to append into as items land -- only the pipelining was missing.
Its second stage is left alone: each InputMessageContent is still built
serially through MessageFactory, which for a large share is plausibly the
bigger delay, but parallelising it touches TDLib generation, ordering and
resource pressure at once and wants to be its own change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 4230adc • 3 files, +67/-36
Clear the dead branches out of SendFilesPopup, and log what it swallowed
FileItem_PointerEntered read an item it never used, through
ItemFromContainer on the template root rather than the container, so it was
always null anyway. Its glyph assignment was also unguarded where the matching
PointerExited uses ?.; both do now.
OnContainerContentChanging had a branch setting AspectView.Constraint that
cannot run. The template selector only ever returns FileItemTemplate or
AlbumTemplate, both rooted on a plain Grid; MediaItemTemplate, the one rooted
on an AspectView, is only used as a Button.ContentTemplate inside
StorageAlbumPanel and so is never a container's ContentTemplateRoot.
The popup's two catch { } now record what they caught. Reading a data package
is someone else's data over remote calls, so they have to keep swallowing, but
silently meant a paste that did nothing left nothing to look at. The per-file
catch in ProbeAsync logs too: the per-type factories already return null for
the expected "this is not a photo" case, so reaching that handler is a
surprise. Their own catch { return null; } is left alone, being both the
documented contract and, at one entry per unsupported file, a good way to flood
a 200 entry ring buffer that ships with crash reports.
Also folds the glyph expression, which appeared three times, into GlyphFor.
Three findings from the review doc do not survive a second look and are
corrected there rather than acted on. FindName on a template root is a
namescope table lookup rather than a tree walk, and the filename substrings run
once per row realization. Extensions.Prepend is misnamed and appends, so the
log line is linear and in natural order, not quadratic and reversed. And the
LINQ passes in IsMediaAllowed and TitleText use static lambdas the compiler
caches, on a path that runs about ten times per popup. Churning code for a cost
that is not there is how a file gets worse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Update libvlc build script
UnigramDev/Unigram/develop • 45d151b • 2 files, +32/-13
Probe a batch of files concurrently
The batch CreateAsync overload was still the serial loop the popup stopped
using, and the share target had become its only remaining caller, so sharing
thirty photos typed them one at a time before anything was sent.
It is now an ordered wrapper over ProbeAsync: concurrent, with results written
into a positional array so the order the caller gave survives, and failures
dropped at the end. Same contract, so no call site changes.
Order matters to the one caller: SendMessagesView attaches the caption to the
last item of the grouping, and GetItemsView needs the whole set before it can
group at all.
Streaming does not apply there and is not attempted. That view is an animation
and a progress bar, swapped in after the chats have been picked, so there is no
list to append into as items land -- only the pipelining was missing.
Its second stage is left alone: each InputMessageContent is still built
serially through MessageFactory, which for a large share is plausibly the
bigger delay, but parallelising it touches TDLib generation, ordering and
resource pressure at once and wants to be its own change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 4230adc • 3 files, +67/-36
Clear the dead branches out of SendFilesPopup, and log what it swallowed
FileItem_PointerEntered read an item it never used, through
ItemFromContainer on the template root rather than the container, so it was
always null anyway. Its glyph assignment was also unguarded where the matching
PointerExited uses ?.; both do now.
OnContainerContentChanging had a branch setting AspectView.Constraint that
cannot run. The template selector only ever returns FileItemTemplate or
AlbumTemplate, both rooted on a plain Grid; MediaItemTemplate, the one rooted
on an AspectView, is only used as a Button.ContentTemplate inside
StorageAlbumPanel and so is never a container's ContentTemplateRoot.
The popup's two catch { } now record what they caught. Reading a data package
is someone else's data over remote calls, so they have to keep swallowing, but
silently meant a paste that did nothing left nothing to look at. The per-file
catch in ProbeAsync logs too: the per-type factories already return null for
the expected "this is not a photo" case, so reaching that handler is a
surprise. Their own catch { return null; } is left alone, being both the
documented contract and, at one entry per unsupported file, a good way to flood
a 200 entry ring buffer that ships with crash reports.
Also folds the glyph expression, which appeared three times, into GlyphFor.
Three findings from the review doc do not survive a second look and are
corrected there rather than acted on. FindName on a template root is a
namescope table lookup rather than a tree walk, and the filename substrings run
once per row realization. Extensions.Prepend is misnamed and appends, so the
log line is linear and in natural order, not quadratic and reversed. And the
LINQ passes in IsMediaAllowed and TitleText use static lambdas the compiler
caches, on a path that runs about ten times per popup. Churning code for a cost
that is not there is how a file gets worse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
UnigramDev/Unigram/develop • dc192a3 • 2 files, +12/-8
Time the typing indicator with ticks, not the clock
It ran DateTime.Now twice on every keystroke. Now resolves the local time zone
on each call, which Logger.cs already documents, but the bigger point is that
this measures an interval: the clock can jump backwards on an NTP correction,
and the gate would then stay shut until it caught up. Logger.TickCount is
milliseconds since boot, read once.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 9c8aa44 • 2 files, +14/-7
Ask for the quick reply shortcuts once, not per typed character
GetCommands runs for every autocomplete query that doesn't match the previous
one, so typing /start sent six identical loadQuickReplyShortcuts requests.
Answering the TODO that sat on the line: it is needed. GetQuickReplyShortcuts
returns empty until the update this asks for arrives, and the only other caller
is the business replies page, which a user may never open. So it stays - once
per box rather than once per character.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • dc1f251 • 4 files, +22/-11
Track the quick reply shortcuts request in the service
Following up 9c8aa44: the flag belongs next to the state it guards, not in one
of the callers. LoadQuickReplyShortcuts is an IClientService method now, so the
business replies page gets the same treatment, and Clear() resets it with the
shortcuts it goes with.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 99d1ad7 • 3 files, +39/-14
Don't allocate a cancellation source just to cancel
CancelEmoji cancelled the pending emoji query and allocated a replacement in
the same breath, and SetAutocomplete calls it on nearly every keystroke - with
nothing to hand the new token to. It also dropped the cancelled source without
disposing it, unlike CancelInlineBotToken next door.
Cancelling and starting are separate now. CaptionTextBox carries a copy of the
same code, so it gets the same split.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Time the typing indicator with ticks, not the clock
It ran DateTime.Now twice on every keystroke. Now resolves the local time zone
on each call, which Logger.cs already documents, but the bigger point is that
this measures an interval: the clock can jump backwards on an NTP correction,
and the gate would then stay shut until it caught up. Logger.TickCount is
milliseconds since boot, read once.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 9c8aa44 • 2 files, +14/-7
Ask for the quick reply shortcuts once, not per typed character
GetCommands runs for every autocomplete query that doesn't match the previous
one, so typing /start sent six identical loadQuickReplyShortcuts requests.
Answering the TODO that sat on the line: it is needed. GetQuickReplyShortcuts
returns empty until the update this asks for arrives, and the only other caller
is the business replies page, which a user may never open. So it stays - once
per box rather than once per character.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • dc1f251 • 4 files, +22/-11
Track the quick reply shortcuts request in the service
Following up 9c8aa44: the flag belongs next to the state it guards, not in one
of the callers. LoadQuickReplyShortcuts is an IClientService method now, so the
business replies page gets the same treatment, and Clear() resets it with the
shortcuts it goes with.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 99d1ad7 • 3 files, +39/-14
Don't allocate a cancellation source just to cancel
CancelEmoji cancelled the pending emoji query and allocated a replacement in
the same breath, and SetAutocomplete calls it on nearly every keystroke - with
nothing to hand the new token to. It also dropped the cancelled source without
disposing it, unlike CancelInlineBotToken next door.
Cancelling and starting are separate now. CaptionTextBox carries a copy of the
same code, so it gets the same split.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
UnigramDev/Unigram/develop • 0f035f1 • 3 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/develop • 200e65f • 1 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
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/develop • 200e65f • 1 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/develop • 18da5f6 • 3 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/develop • 469d6de • 2 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/develop • c22b198 • 5 files, +569/-546
Add article option to attach
UnigramDev/Unigram/develop • 0373674 • 1 files, +1/-1
Fix font family
UnigramDev/Unigram/develop • 9aea06e • 3 files, +14/-1
Better chat view inst
UnigramDev/Unigram/develop • f42aeeb • 1 files, +7/-1
Add transcode logging
UnigramDev/Unigram/develop • d3ec4f1 • 1 files, +66/-13
Optimize slide panel
#unigram
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/develop • 469d6de • 2 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/develop • c22b198 • 5 files, +569/-546
Add article option to attach
UnigramDev/Unigram/develop • 0373674 • 1 files, +1/-1
Fix font family
UnigramDev/Unigram/develop • 9aea06e • 3 files, +14/-1
Better chat view inst
UnigramDev/Unigram/develop • f42aeeb • 1 files, +7/-1
Add transcode logging
UnigramDev/Unigram/develop • d3ec4f1 • 1 files, +66/-13
Optimize slide panel
#unigram
UnigramDev/Unigram/develop • 19633c7 • 1 files, +4/-0
Copy rich messages as formatted text
UnigramDev/Unigram/develop • 9883698 • 1 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/develop • 1cb1619 • 2 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
Copy rich messages as formatted text
UnigramDev/Unigram/develop • 9883698 • 1 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/develop • 1cb1619 • 2 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/develop • 8ab18d3 • 2 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
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/nightly • 651f9ba • 3 files, +23/-23
Switched nightly Windows CI with optimized binaries without arm64.
#tdesktop
Switched nightly Windows CI with optimized binaries without arm64.
#tdesktop
🫡2
telegramdesktop/tdesktop/nightly • 6d51231 • 2 files, +23/-15
Switched nightly Windows CI with optimized binaries without arm64.
#tdesktop
Switched nightly Windows CI with optimized binaries without arm64.
#tdesktop
🫡2
UnigramDev/Unigram/theme-current-share-target • 7a4368b • 1 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
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-continue • 1cfe003 • 2 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
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-key • c1bf81a • 2 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
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-win10 • d730aa9 • 2 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
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-info • 5e526bd • 3 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
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-guard • ec3172b • 1 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
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-recovery • c3efb81 • 1 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
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-menu • e3b452d • 1 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
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-lost • 8a6a679 • 1 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
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-reinsert • 003181f • 1 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
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-teardown • 9b344a2 • 4 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
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/develop • 5623245 • 1 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/develop • 1b3d6a9 • 4 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
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/develop • 1b3d6a9 • 4 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/develop • dbdb4b7 • 6 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/develop • eb349af • 7 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
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/develop • eb349af • 7 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