UnigramDev/Unigram/develop • a7d3657 • 1 files, +8/-4
Improve quote style
UnigramDev/Unigram/develop • e068bd3 • 1 files, +22/-5
crash-triage: correct the PR base and branch naming
Two things went wrong on the first real run of this skill.
The PR must be based on GitHub's develop, not on origin/develop inside a
clone of the user's repo. That ref is their remote-tracking branch: it was
both stale and carrying unpushed local commits, so it had diverged from the
branch the PR actually merges into, and the file being patched differed
between the two. Add the GitHub remote explicitly and branch from there.
Branches cannot be prefixed with fix/, because a branch literally named "fix"
exists on the remote and git rejects the push with "directory file conflict".
Also requires stating whether the change was built and run. A UWP/.NET Native
build is usually unavailable here, and a reviewer should not have to guess.
Roslyn parsing is offered as the weaker check it is, with its limits spelled
out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 79e90af • 1 files, +20/-7
crash-triage: never publish crash impact numbers
The repository is public, so affected-user counts, device counts and per-OS
breakdowns are private telemetry being published to the world. The PR
template asked for exactly that, and the first real run put "46 crashes / 32
users" into both a PR body and a pushed commit message.
Impact figures are now explicitly for choosing what to work on, never for
justifying it in public. PRs say "reported by crash telemetry on <version>"
and nothing more; group hashes and device breakdowns stay out too.
Calls out that the commit message is as public as the PR body and must be
checked before pushing, since scrubbing it afterwards needs a force-push.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 6a24748 • 1 files, +12/-3
crash-triage: generate public text with crashctl --public
Stripping impact figures by hand already failed once, so point the skill at
the mechanical path instead: crashctl show --public emits the exception, the
frames and the missing-symbol list with impact figures, device/OS breakdown,
group hash and log tail withheld.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Improve quote style
UnigramDev/Unigram/develop • e068bd3 • 1 files, +22/-5
crash-triage: correct the PR base and branch naming
Two things went wrong on the first real run of this skill.
The PR must be based on GitHub's develop, not on origin/develop inside a
clone of the user's repo. That ref is their remote-tracking branch: it was
both stale and carrying unpushed local commits, so it had diverged from the
branch the PR actually merges into, and the file being patched differed
between the two. Add the GitHub remote explicitly and branch from there.
Branches cannot be prefixed with fix/, because a branch literally named "fix"
exists on the remote and git rejects the push with "directory file conflict".
Also requires stating whether the change was built and run. A UWP/.NET Native
build is usually unavailable here, and a reviewer should not have to guess.
Roslyn parsing is offered as the weaker check it is, with its limits spelled
out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 79e90af • 1 files, +20/-7
crash-triage: never publish crash impact numbers
The repository is public, so affected-user counts, device counts and per-OS
breakdowns are private telemetry being published to the world. The PR
template asked for exactly that, and the first real run put "46 crashes / 32
users" into both a PR body and a pushed commit message.
Impact figures are now explicitly for choosing what to work on, never for
justifying it in public. PRs say "reported by crash telemetry on <version>"
and nothing more; group hashes and device breakdowns stay out too.
Calls out that the commit message is as public as the PR body and must be
checked before pushing, since scrubbing it afterwards needs a force-push.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 6a24748 • 1 files, +12/-3
crash-triage: generate public text with crashctl --public
Stripping impact figures by hand already failed once, so point the skill at
the mechanical path instead: crashctl show --public emits the exception, the
frames and the missing-symbol list with impact figures, device/OS breakdown,
group hash and log tail withheld.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
🫡2
UnigramDev/Unigram/develop • 73fcda9 • 1 files, +26/-4
crash-triage: close the crash group when the PR merges
Merging is what marks a crash fixed, so the loop now ends by closing the
group rather than leaving it to be done by hand.
Because the group hash cannot appear in the PR, the pr-number -> group-hash
mapping has to be recorded locally when the PR is opened, or the link is lost
as soon as the PR is the only artefact left.
Also records the PR-watching gotcha found this session: gh posts comments
under the user's own account, so a watcher cannot use author to tell the
agent's comments from the user's, and unfiltered it reads its own words back
as instructions. Agent comments carry an HTML-comment marker and the watcher
skips them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 4caca6d • 1 files, +17/-0
crash-triage: verify the mechanism instead of inferring it
A rejected PR's worth of lesson. The autocomplete crash was diagnosed as a
recycled container carrying the previous item type's template, a story that
fitted the stack and was never checked. The actual cause was
AutocompleteTemplateSelector returning CommandTemplate for both UserCommand
and QuickReplyShortcut, so the tree never matched what the code-behind read —
one file would have shown it, and it was never opened.
Requires naming and opening the file that would confirm a hypothesis before
writing code, and adds two checks that would have caught this one:
- Does the crash frequency match the story? A race is intermittent; a
mismatched template fails every time that path runs. Rare-but-deterministic
contradicts a race.
- For a visual-tree mismatch, read the DataTemplateSelector before blaming
recycling. Guarding the symptom there turns a crash into a silently blank
row and leaves the real bug in place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • e66dbd0 • 1 files, +14/-0
crash-triage: don't construct singletons to silence a null
Second rejected PR, same root habit as the first: fixing the symptom at the
call site without establishing who owns the state.
InitializeFrame was patched to build a WindowContext when Current was null.
WindowContext's constructor assigns the thread-static _current, and
OnWindowCreated constructs one on its own schedule, so the patch produced two:
the later assignment wins and the frame attached to the first is silently
discarded. That is worse than the NullReferenceException it removed, because
it corrupts startup state instead of aborting it.
Adds two rules:
- Before constructing anything to make a null go away, grep every construction
site and work out the ordering. If it is a not-yet-initialised singleton,
creating one here means two exist and one loses.
- When the missing state is a race, neither inventing it nor returning early
is the fix — returning leaves the feature silently dead, which is the same
trap as guarding a template mismatch. Carrying the input forward until the
state arrives is a sequencing decision about startup: propose it, don't
guess at it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • ecb532a • 1 files, +2/-2
Fix MediaDeviceList release
#unigram
crash-triage: close the crash group when the PR merges
Merging is what marks a crash fixed, so the loop now ends by closing the
group rather than leaving it to be done by hand.
Because the group hash cannot appear in the PR, the pr-number -> group-hash
mapping has to be recorded locally when the PR is opened, or the link is lost
as soon as the PR is the only artefact left.
Also records the PR-watching gotcha found this session: gh posts comments
under the user's own account, so a watcher cannot use author to tell the
agent's comments from the user's, and unfiltered it reads its own words back
as instructions. Agent comments carry an HTML-comment marker and the watcher
skips them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 4caca6d • 1 files, +17/-0
crash-triage: verify the mechanism instead of inferring it
A rejected PR's worth of lesson. The autocomplete crash was diagnosed as a
recycled container carrying the previous item type's template, a story that
fitted the stack and was never checked. The actual cause was
AutocompleteTemplateSelector returning CommandTemplate for both UserCommand
and QuickReplyShortcut, so the tree never matched what the code-behind read —
one file would have shown it, and it was never opened.
Requires naming and opening the file that would confirm a hypothesis before
writing code, and adds two checks that would have caught this one:
- Does the crash frequency match the story? A race is intermittent; a
mismatched template fails every time that path runs. Rare-but-deterministic
contradicts a race.
- For a visual-tree mismatch, read the DataTemplateSelector before blaming
recycling. Guarding the symptom there turns a crash into a silently blank
row and leaves the real bug in place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • e66dbd0 • 1 files, +14/-0
crash-triage: don't construct singletons to silence a null
Second rejected PR, same root habit as the first: fixing the symptom at the
call site without establishing who owns the state.
InitializeFrame was patched to build a WindowContext when Current was null.
WindowContext's constructor assigns the thread-static _current, and
OnWindowCreated constructs one on its own schedule, so the patch produced two:
the later assignment wins and the frame attached to the first is silently
discarded. That is worse than the NullReferenceException it removed, because
it corrupts startup state instead of aborting it.
Adds two rules:
- Before constructing anything to make a null go away, grep every construction
site and work out the ordering. If it is a not-yet-initialised singleton,
creating one here means two exist and one loses.
- When the missing state is a race, neither inventing it nor returning early
is the fix — returning leaves the feature silently dead, which is the same
trap as guarding a template mismatch. Carrying the input forward until the
state arrives is a sequencing decision about startup: propose it, don't
guess at it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • ecb532a • 1 files, +2/-2
Fix MediaDeviceList release
#unigram
🫡2
UnigramDev/Unigram/develop • c5beb02 • 3 files, +30/-8
Fix file id packing
UnigramDev/Unigram/develop • b9e38a2 • 10 files, +674/-265
Improve media streaming
UnigramDev/Unigram/develop • e18b2ba • 1 files, +3/-1
Fix conditional check
UnigramDev/Unigram/develop • aabf3a0 • 1 files, +104/-9
Calculate can be forwarded/replied locally in MessageSelector
UnigramDev/Unigram/develop • 1ae3795 • 1 files, +70/-0
Support buttons in text
UnigramDev/Unigram/develop • 7979820 • 2 files, +10/-3
Fix recycle paths
UnigramDev/Unigram/develop • 9a61aba • 2 files, +2/-0
Add handled true where missing
UnigramDev/Unigram/develop • 333170e • 7 files, +14/-0
Add tlottie test
UnigramDev/Unigram/develop • 2a2fd79 • 1 files, +8/-0
Handle unsupported message click
UnigramDev/Unigram/develop • d4268bb • 1 files, +5/-4
Fix quick reply shortcut crash
UnigramDev/Unigram/develop • ee0d49d • 1 files, +5/-0
Handle force reply
UnigramDev/Unigram/develop • 16acb43 • 1 files, +2/-2
Fix rich draft
UnigramDev/Unigram/develop • f2305b4 • 3 files, +80/-100
Text editor improvements
UnigramDev/Unigram/develop • 8bde885 • 1 files, +2/-2
Fix csproj
UnigramDev/Unigram/develop • f046915 • 1 files, +3/-3
Fix AddFileToDownloads calls
#unigram
Fix file id packing
UnigramDev/Unigram/develop • b9e38a2 • 10 files, +674/-265
Improve media streaming
UnigramDev/Unigram/develop • e18b2ba • 1 files, +3/-1
Fix conditional check
UnigramDev/Unigram/develop • aabf3a0 • 1 files, +104/-9
Calculate can be forwarded/replied locally in MessageSelector
UnigramDev/Unigram/develop • 1ae3795 • 1 files, +70/-0
Support buttons in text
UnigramDev/Unigram/develop • 7979820 • 2 files, +10/-3
Fix recycle paths
UnigramDev/Unigram/develop • 9a61aba • 2 files, +2/-0
Add handled true where missing
UnigramDev/Unigram/develop • 333170e • 7 files, +14/-0
Add tlottie test
UnigramDev/Unigram/develop • 2a2fd79 • 1 files, +8/-0
Handle unsupported message click
UnigramDev/Unigram/develop • d4268bb • 1 files, +5/-4
Fix quick reply shortcut crash
UnigramDev/Unigram/develop • ee0d49d • 1 files, +5/-0
Handle force reply
UnigramDev/Unigram/develop • 16acb43 • 1 files, +2/-2
Fix rich draft
UnigramDev/Unigram/develop • f2305b4 • 3 files, +80/-100
Text editor improvements
UnigramDev/Unigram/develop • 8bde885 • 1 files, +2/-2
Fix csproj
UnigramDev/Unigram/develop • f046915 • 1 files, +3/-3
Fix AddFileToDownloads calls
#unigram
🫡2
UnigramDev/Unigram/develop • 765bc86 • 5 files, +127/-22
Merge branch 'develop' of https://github.com/UnigramDev/Unigram into develop
UnigramDev/Unigram/develop • 440776a • 197 files, +399978/-6501
Merge branch 'layer228' into develop
#unigram
Merge branch 'develop' of https://github.com/UnigramDev/Unigram into develop
UnigramDev/Unigram/develop • 440776a • 197 files, +399978/-6501
Merge branch 'layer228' into develop
#unigram
🫡2
morethanwords/tweb/master • b1f87b5 • 6 files, +224/-7
Fix XSS via forged Electron helpers in message links
Link hrefs were built by interpolating the URL into a javascript: string
literal, so an apostrophe in a URL escaped into code. The Electron check
guarding that path read the ambient electronHelpers global, which any
element id shadows — and Temml turns a message's \label{} into a raw id,
so message content could forge it in an ordinary browser.
Keep the URL as data and hand it to openExternal on click, require a
callable bridge, and namespace ids Temml renders from message content.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
morethanwords/tweb/master • c4618cc • 3 files, +63/-23
Fix Instant View math never rendering
Temml's lexer builds its token regex by concatenating string literals
holding raw lone surrogates (the surrogate-pair alternative,
"|[\uD800-\uDBFF][\uDC00-\uDFFF]"). Both the esbuild dev dep-optimizer
and the rolldown production build re-print that folded string with each
lone surrogate written as U+FFFD, degrading the class to
[<FFFD>d800-<FFFD>dbff]. The 0-<FFFD> range that falls out of it
swallows a backslash, so every control word lexed as a one-letter
command: \frac died with "Unsupported function name: \f", nothing but
bare symbols parsed, and renderLatexInto silently fell back to showing
the raw source. This was never dev-only — production shipped the same
broken regex.
Load the library as an asset no bundler re-prints: ?url plus a runtime
script load. Take the package's own terser-minified build, which escapes
the lone surrogates correctly and is 49KB gzip against the .mjs bundle's
115KB — lighter than the broken bundled copy's 59KB. The comment
claiming Temml PR #128 made a plain dynamic import safe was wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 4c5a237 • 2 files, +277/-10
Handle postMessage clone failures in SuperMessagePort
postMessage errors were swallowed with a log line, so a payload that could
not be structured-cloned left the caller's invoke promise pending forever
and dropped the task for every port.
Route every send through sendTask, which now retries a failed batch
per-inner-task, answers a result/ack that could not be cloned with an
explicit DATA_CLONE_ERROR, and rejects an invoke that reached no port at
all. Also record the invoke's payload name/method in `awaiting` so a
timed-out task can be identified from the log.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 701e811 • 1 files, +3/-3
Fix join button label for channels with join requests
A broadcast channel that requires approval showed "Subscribe" instead of
"Request to Join": isBroadcast was tested before the request type, so the
request branch was unreachable for channels. Test the request type first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Fix XSS via forged Electron helpers in message links
Link hrefs were built by interpolating the URL into a javascript: string
literal, so an apostrophe in a URL escaped into code. The Electron check
guarding that path read the ambient electronHelpers global, which any
element id shadows — and Temml turns a message's \label{} into a raw id,
so message content could forge it in an ordinary browser.
Keep the URL as data and hand it to openExternal on click, require a
callable bridge, and namespace ids Temml renders from message content.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
morethanwords/tweb/master • c4618cc • 3 files, +63/-23
Fix Instant View math never rendering
Temml's lexer builds its token regex by concatenating string literals
holding raw lone surrogates (the surrogate-pair alternative,
"|[\uD800-\uDBFF][\uDC00-\uDFFF]"). Both the esbuild dev dep-optimizer
and the rolldown production build re-print that folded string with each
lone surrogate written as U+FFFD, degrading the class to
[<FFFD>d800-<FFFD>dbff]. The 0-<FFFD> range that falls out of it
swallows a backslash, so every control word lexed as a one-letter
command: \frac died with "Unsupported function name: \f", nothing but
bare symbols parsed, and renderLatexInto silently fell back to showing
the raw source. This was never dev-only — production shipped the same
broken regex.
Load the library as an asset no bundler re-prints: ?url plus a runtime
script load. Take the package's own terser-minified build, which escapes
the lone surrogates correctly and is 49KB gzip against the .mjs bundle's
115KB — lighter than the broken bundled copy's 59KB. The comment
claiming Temml PR #128 made a plain dynamic import safe was wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 4c5a237 • 2 files, +277/-10
Handle postMessage clone failures in SuperMessagePort
postMessage errors were swallowed with a log line, so a payload that could
not be structured-cloned left the caller's invoke promise pending forever
and dropped the task for every port.
Route every send through sendTask, which now retries a failed batch
per-inner-task, answers a result/ack that could not be cloned with an
explicit DATA_CLONE_ERROR, and rejects an invoke that reached no port at
all. Also record the invoke's payload name/method in `awaiting` so a
timed-out task can be identified from the log.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 701e811 • 1 files, +3/-3
Fix join button label for channels with join requests
A broadcast channel that requires approval showed "Subscribe" instead of
"Request to Join": isBroadcast was tested before the request type, so the
request branch was unreachable for channels. Test the request type first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • 088d690 • 7 files, +334/-53
Support guard-bot joins through their WebView (layer 228)
Layer 227 turned the join result into a union, and tweb handled the
web-view-gated branch by caching the bot and reporting no updates — the
user got nothing at all. Layer 228 splits that flow into an initial query
plus a separate messages.requestChatJoinWebView, so it can be driven.
processChatInviteJoinResult now returns a chatInviteJoinWebView descriptor
instead of undefined, and openWebApp grows a joinChat mode that requests
the guard bot's web view, tracks the query ids, and follows
updateJoinChatWebViewDecision: a webView result reloads the iframe with the
new url, while approved/declined/queued closes it and toasts the outcome,
opening the chat when the join went through.
Also make WebApp.destroy idempotent and bail out of the async init steps
once destroyed — the join flow can tear the popup down mid-init.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 2d2f188 • 206 files, +30012/-2291
Implement Telegram Communities
morethanwords/tweb/master • 6619fdb • 14 files, +944/-73
Support guard bots end to end
Layer 228 hands a join off to a guard bot, which greets the newcomer in its
own mini app and answers the query. tweb only had the joining half, and its
outcome toast interpolated the chat title — which an invite-link join never
has, so a declined or queued join showed a hole where the name should be.
Joining side: the toast now uses the wording the official clients use and
tells a group from a channel without needing the title; an approved join
opens the chat through `open` so a forum lands on its topics tab; the
disclaimer names both the chat and the bot, ported from iOS. `loadExternal`
and the iframe show/hide it shared with `createWebView` no longer duplicate
each other.
Admin side, which was missing entirely: `toggleJoinRequest` carries the guard
bot (and clears it with `inputUserEmpty`), the bot's admin rights grow a
Process Join Requests switch with the enable and replace confirmations, and
the chat type screen names the guard bot by @username with a link to its
rights. Channels get the approval section they never had, so a guard bot set
there can also be seen and removed.
The ephemeral bot fixture gains Guard Mode: it answers join request queries
with approve, decline or queue, optionally showing a Mini App first. A query
it can no longer answer is logged and skipped rather than fatal — the same
update is redelivered until the batch is acknowledged, so dying on it wedges
the fixture forever on that one update.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 62c3ad6 • 9 files, +99/-7
Support stars_spend_topup_invoice_disabled
Don't offer the Stars topup when the spend is attributed to a bot or a
channel and the app config forbids it — show a toast instead, like other
clients do.
The spend purpose peer is now resolved the same way as in tdesktop (bots
and channels only, monoforums resolve to their broadcast) and is used
both for inputStorePaymentStarsTopup.spend_purpose_peer and for the new
check. Paid messages, suggested posts and the "Buy Stars" service
buttons pass their peer as well now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Support guard-bot joins through their WebView (layer 228)
Layer 227 turned the join result into a union, and tweb handled the
web-view-gated branch by caching the bot and reporting no updates — the
user got nothing at all. Layer 228 splits that flow into an initial query
plus a separate messages.requestChatJoinWebView, so it can be driven.
processChatInviteJoinResult now returns a chatInviteJoinWebView descriptor
instead of undefined, and openWebApp grows a joinChat mode that requests
the guard bot's web view, tracks the query ids, and follows
updateJoinChatWebViewDecision: a webView result reloads the iframe with the
new url, while approved/declined/queued closes it and toasts the outcome,
opening the chat when the join went through.
Also make WebApp.destroy idempotent and bail out of the async init steps
once destroyed — the join flow can tear the popup down mid-init.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 2d2f188 • 206 files, +30012/-2291
Implement Telegram Communities
morethanwords/tweb/master • 6619fdb • 14 files, +944/-73
Support guard bots end to end
Layer 228 hands a join off to a guard bot, which greets the newcomer in its
own mini app and answers the query. tweb only had the joining half, and its
outcome toast interpolated the chat title — which an invite-link join never
has, so a declined or queued join showed a hole where the name should be.
Joining side: the toast now uses the wording the official clients use and
tells a group from a channel without needing the title; an approved join
opens the chat through `open` so a forum lands on its topics tab; the
disclaimer names both the chat and the bot, ported from iOS. `loadExternal`
and the iframe show/hide it shared with `createWebView` no longer duplicate
each other.
Admin side, which was missing entirely: `toggleJoinRequest` carries the guard
bot (and clears it with `inputUserEmpty`), the bot's admin rights grow a
Process Join Requests switch with the enable and replace confirmations, and
the chat type screen names the guard bot by @username with a link to its
rights. Channels get the approval section they never had, so a guard bot set
there can also be seen and removed.
The ephemeral bot fixture gains Guard Mode: it answers join request queries
with approve, decline or queue, optionally showing a Mini App first. A query
it can no longer answer is logged and skipped rather than fatal — the same
update is redelivered until the batch is acknowledged, so dying on it wedges
the fixture forever on that one update.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 62c3ad6 • 9 files, +99/-7
Support stars_spend_topup_invoice_disabled
Don't offer the Stars topup when the spend is attributed to a bot or a
channel and the app config forbids it — show a toast instead, like other
clients do.
The spend purpose peer is now resolved the same way as in tdesktop (bots
and channels only, monoforums resolve to their broadcast) and is used
both for inputStorePaymentStarsTopup.spend_purpose_peer and for the new
check. Paid messages, suggested posts and the "Buy Stars" service
buttons pass their peer as well now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡3
🫡3
UnigramDev/Unigram/develop • 70a965e • 2 files, +72/-17
Reuse the text selection manager across recycled containers
MessageSelector built a TextSelectionManager on load and dropped it on unload,
so a virtualized history allocated one per container per scroll-through, each
registering four routed pointer handlers and two owner subscriptions on the way
in and removing them on the way out.
The manager now takes its content element through Attach rather than only in the
constructor, so one instance can follow a container through its whole life while
the element it watches changes underneath. The four PointerEventHandler
instances move to the constructor: AddHandler and RemoveHandler match by delegate
identity, so they always had to be stable, and creating them once per manager
rather than once per attach costs nothing.
Detach now clears the selection and empties _items. That list holds the
selectable controls the selection was made of, and a manager that outlives its
content would otherwise keep them alive and highlight them again the next time it
was asked. It is also idempotent and leaves _root null, which the two detach
paths on the selector, unload and content change, both rely on. RebuildItems
guards against the detached state because SelectAll is public.
An active selection is therefore dropped when content detaches, where before it
was abandoned with the manager. Nothing observes the difference:
TextSelectionCoordinator holds a weak reference and SelectionChanged has no
subscribers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 55e0be3 • 2 files, +16/-10
Wire up button click in text
UnigramDev/Unigram/develop • 1785402 • 1 files, +34/-1
crash-triage: diagnose why the value was wrong, not just that it was
Both rejected PRs this session shared a habit: reaching for a guard as soon as
the faulting value was identified, without establishing how it came to be that
way. "It was null" is where the investigation starts.
Requires tracing back to whatever should have set the value and being able to
name the state that produced it in one sentence, pointing at the code that
creates it. "Somehow it was null" means the diagnosis is unfinished.
Where that lands decides the fix, and the three outcomes want different things:
- the API legitimately returns null and the caller assumed otherwise, where a
guard at the call site is correct;
- something upstream is wired wrong, where a guard hides a permanent bug and
degrades silently instead;
- two things race and the state is merely not there yet, which needs
sequencing rather than a default or an early return.
All three occurred here within a day, so the table names the real examples.
Also notes that a guard shipped without knowing which case applies is still a
guess even when it demonstrably stops the crash — it costs the next person the
evidence, since the reports stop while the bug remains — and that the PR should
state the cause, because that is what a reviewer can actually check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Reuse the text selection manager across recycled containers
MessageSelector built a TextSelectionManager on load and dropped it on unload,
so a virtualized history allocated one per container per scroll-through, each
registering four routed pointer handlers and two owner subscriptions on the way
in and removing them on the way out.
The manager now takes its content element through Attach rather than only in the
constructor, so one instance can follow a container through its whole life while
the element it watches changes underneath. The four PointerEventHandler
instances move to the constructor: AddHandler and RemoveHandler match by delegate
identity, so they always had to be stable, and creating them once per manager
rather than once per attach costs nothing.
Detach now clears the selection and empties _items. That list holds the
selectable controls the selection was made of, and a manager that outlives its
content would otherwise keep them alive and highlight them again the next time it
was asked. It is also idempotent and leaves _root null, which the two detach
paths on the selector, unload and content change, both rely on. RebuildItems
guards against the detached state because SelectAll is public.
An active selection is therefore dropped when content detaches, where before it
was abandoned with the manager. Nothing observes the difference:
TextSelectionCoordinator holds a weak reference and SelectionChanged has no
subscribers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 55e0be3 • 2 files, +16/-10
Wire up button click in text
UnigramDev/Unigram/develop • 1785402 • 1 files, +34/-1
crash-triage: diagnose why the value was wrong, not just that it was
Both rejected PRs this session shared a habit: reaching for a guard as soon as
the faulting value was identified, without establishing how it came to be that
way. "It was null" is where the investigation starts.
Requires tracing back to whatever should have set the value and being able to
name the state that produced it in one sentence, pointing at the code that
creates it. "Somehow it was null" means the diagnosis is unfinished.
Where that lands decides the fix, and the three outcomes want different things:
- the API legitimately returns null and the caller assumed otherwise, where a
guard at the call site is correct;
- something upstream is wired wrong, where a guard hides a permanent bug and
degrades silently instead;
- two things race and the state is merely not there yet, which needs
sequencing rather than a default or an early return.
All three occurred here within a day, so the table names the real examples.
Also notes that a guard shipped without knowing which case applies is still a
guess even when it demonstrably stops the crash — it costs the next person the
evidence, since the reports stop while the bug remains — and that the PR should
state the cause, because that is what a reviewer can actually check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
🫡2
UnigramDev/Unigram/develop • 1e841d6 • 7 files, +658/-308
Get the VLC build working again, and generate the plugin cache
The build failed while fetching contribs, so nothing downstream had been
exercised in a while. Fixing that surfaced several more problems.
The submodule fix is carried as 0005: $(SF) pointed at a SourceForge mirror
that no longer resolves, and the libvlc-only build tried to package an NSIS
installer around a vlc.exe that -z never builds.
build.ps1:
- dropped -it from docker run. It requires a terminal on stdin, so the script
could not run from CI or any non-interactive shell.
- check the exit code. Previously a failed build fell straight through to
packing, turning a broken build into a stale package rather than an error.
- pass -D as a separate argument. getopts captured the '=' from -D=<path> into
OPTARG, which reached -fdebug-prefix-map and put a literal '=' in front of
every source path recorded in the PDBs. Crash reports showed frames as
"=C:\...\win64-uwp\modules\=C:\...\modules\audio_output\wasapi.c", and they
now resolve normally.
The plugin cache was never generated: Makefile.am only runs vlc-cache-gen when
build == host, so cross-compiling always skipped it, and the committed
plugins.dat was left over from 3.0.22-rc1. cache.c compares the version with
memcmp, so it was being rejected outright and every launch rescanned all the
plugins.
plugins-cache.ps1 generates it. The UWP build loads plugins with
LoadPackagedLibrary, which only resolves inside the calling process's package,
so cache-gen finds nothing when run from a plain directory. It therefore stages
the plugins the nuspec ships, registers that as a loose package, and runs
cache-gen inside it.
ARM64 cannot execute on an x64 host, so its cache is derived rather than
generated. bank.c validates each entry by mtime and size only, and the module
descriptors are architecture-independent, so the x64 cache is re-stamped with
the ARM64 files' metadata. Verified independently: all 36 entries carry the
right mtime and size for their architecture, and 31 of them genuinely differ
between the two.
That also means one plugins.dat cannot serve both architectures, so the nuspec
now ships one per architecture instead of the same file twice.
The nuspec referred to the install directory as vlc-3.0.23-2 in 284 places
while the build produces vlc-3.0.23, so packing could never succeed. The
version is now substituted via -Properties. A wildcard was simpler but NuGet
then treats each target as a directory and produces
build/win10-x64/libvlc.dll/libvlc.dll.
Verified end to end: both architectures build, the caches are generated and
checked, and nuget pack produces a package with 36 plugins and a distinct
cache per architecture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 07ab82d • 2 files, +0/-0
Latest tlottie fixes
UnigramDev/Unigram/develop • 942b8a3 • 3 files, +15/-8
Update libvlc
UnigramDev/Unigram/develop • 0525c26 • 3 files, +4/-33
Display voice notes in rich messages properly
UnigramDev/Unigram/develop • 3418495 • 1 files, +20/-33
Fix instant view styles
#unigram
Get the VLC build working again, and generate the plugin cache
The build failed while fetching contribs, so nothing downstream had been
exercised in a while. Fixing that surfaced several more problems.
The submodule fix is carried as 0005: $(SF) pointed at a SourceForge mirror
that no longer resolves, and the libvlc-only build tried to package an NSIS
installer around a vlc.exe that -z never builds.
build.ps1:
- dropped -it from docker run. It requires a terminal on stdin, so the script
could not run from CI or any non-interactive shell.
- check the exit code. Previously a failed build fell straight through to
packing, turning a broken build into a stale package rather than an error.
- pass -D as a separate argument. getopts captured the '=' from -D=<path> into
OPTARG, which reached -fdebug-prefix-map and put a literal '=' in front of
every source path recorded in the PDBs. Crash reports showed frames as
"=C:\...\win64-uwp\modules\=C:\...\modules\audio_output\wasapi.c", and they
now resolve normally.
The plugin cache was never generated: Makefile.am only runs vlc-cache-gen when
build == host, so cross-compiling always skipped it, and the committed
plugins.dat was left over from 3.0.22-rc1. cache.c compares the version with
memcmp, so it was being rejected outright and every launch rescanned all the
plugins.
plugins-cache.ps1 generates it. The UWP build loads plugins with
LoadPackagedLibrary, which only resolves inside the calling process's package,
so cache-gen finds nothing when run from a plain directory. It therefore stages
the plugins the nuspec ships, registers that as a loose package, and runs
cache-gen inside it.
ARM64 cannot execute on an x64 host, so its cache is derived rather than
generated. bank.c validates each entry by mtime and size only, and the module
descriptors are architecture-independent, so the x64 cache is re-stamped with
the ARM64 files' metadata. Verified independently: all 36 entries carry the
right mtime and size for their architecture, and 31 of them genuinely differ
between the two.
That also means one plugins.dat cannot serve both architectures, so the nuspec
now ships one per architecture instead of the same file twice.
The nuspec referred to the install directory as vlc-3.0.23-2 in 284 places
while the build produces vlc-3.0.23, so packing could never succeed. The
version is now substituted via -Properties. A wildcard was simpler but NuGet
then treats each target as a directory and produces
build/win10-x64/libvlc.dll/libvlc.dll.
Verified end to end: both architectures build, the caches are generated and
checked, and nuget pack produces a package with 36 plugins and a distinct
cache per architecture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnigramDev/Unigram/develop • 07ab82d • 2 files, +0/-0
Latest tlottie fixes
UnigramDev/Unigram/develop • 942b8a3 • 3 files, +15/-8
Update libvlc
UnigramDev/Unigram/develop • 0525c26 • 3 files, +4/-33
Display voice notes in rich messages properly
UnigramDev/Unigram/develop • 3418495 • 1 files, +20/-33
Fix instant view styles
#unigram
🫡2
UnigramDev/Unigram/develop • 9aa0fea • 1 files, +4/-4
Fix variable name
UnigramDev/Unigram/develop • 11b83a7 • 1 files, +7/-6
Comment out broken code
UnigramDev/Unigram/develop • b28a53d • 2 files, +43/-3
WIP changes
UnigramDev/Unigram/develop • 2e74d91 • 1 files, +2/-2
Fix menu flyout style
UnigramDev/Unigram/develop • 7db4c04 • 1 files, +3/-3
Fix markup button clip
UnigramDev/Unigram/develop • c2805dc • 1 files, +11/-1
Handle UpdateChatHasWelcomeMessages
UnigramDev/Unigram/develop • e35125c • 1 files, +10/-6
Support multiple qr code sizes
UnigramDev/Unigram/develop • 4d506bf • 1 files, +0/-34
Remove dead code
UnigramDev/Unigram/develop • 654bae0 • 1 files, +1/-1
Make field static
UnigramDev/Unigram/develop • 0a33466 • 1 files, +1/-1
Fix poll document type check
UnigramDev/Unigram/develop • 1ec2c2b • 1 files, +12/-0
WriteableBitmap span extension
UnigramDev/Unigram/develop • 257d8b1 • 1 files, +13/-2
Support color and offset in shadow helper
UnigramDev/Unigram/develop • d43f2ca • 2 files, +521/-0
Tilt effect (wip)
UnigramDev/Unigram/develop • b85a83e • 1 files, +1/-0
Fix libvlc_audio_output_device_set
UnigramDev/Unigram/develop • 89d879b • 2 files, +66/-12
Add more logs to media player
UnigramDev/Unigram/develop • c6a778f • 1 files, +7/-0
Add missing has_decoded_frames case
#unigram
Fix variable name
UnigramDev/Unigram/develop • 11b83a7 • 1 files, +7/-6
Comment out broken code
UnigramDev/Unigram/develop • b28a53d • 2 files, +43/-3
WIP changes
UnigramDev/Unigram/develop • 2e74d91 • 1 files, +2/-2
Fix menu flyout style
UnigramDev/Unigram/develop • 7db4c04 • 1 files, +3/-3
Fix markup button clip
UnigramDev/Unigram/develop • c2805dc • 1 files, +11/-1
Handle UpdateChatHasWelcomeMessages
UnigramDev/Unigram/develop • e35125c • 1 files, +10/-6
Support multiple qr code sizes
UnigramDev/Unigram/develop • 4d506bf • 1 files, +0/-34
Remove dead code
UnigramDev/Unigram/develop • 654bae0 • 1 files, +1/-1
Make field static
UnigramDev/Unigram/develop • 0a33466 • 1 files, +1/-1
Fix poll document type check
UnigramDev/Unigram/develop • 1ec2c2b • 1 files, +12/-0
WriteableBitmap span extension
UnigramDev/Unigram/develop • 257d8b1 • 1 files, +13/-2
Support color and offset in shadow helper
UnigramDev/Unigram/develop • d43f2ca • 2 files, +521/-0
Tilt effect (wip)
UnigramDev/Unigram/develop • b85a83e • 1 files, +1/-0
Fix libvlc_audio_output_device_set
UnigramDev/Unigram/develop • 89d879b • 2 files, +66/-12
Add more logs to media player
UnigramDev/Unigram/develop • c6a778f • 1 files, +7/-0
Add missing has_decoded_frames case
#unigram
🫡2
UnigramDev/Unigram/develop • aaf2a06 • 3 files, +314/-0
Unused code
UnigramDev/Unigram/develop • ba351eb • 4 files, +189/-9363
Temp changes
UnigramDev/Unigram/develop • 1601a27 • 1 files, +1/-1
Update libvlc in main project too
UnigramDev/Unigram/develop • b1e62d2 • 2 files, +2/-1
Maintenance
#unigram
Unused code
UnigramDev/Unigram/develop • ba351eb • 4 files, +189/-9363
Temp changes
UnigramDev/Unigram/develop • 1601a27 • 1 files, +1/-1
Update libvlc in main project too
UnigramDev/Unigram/develop • b1e62d2 • 2 files, +2/-1
Maintenance
#unigram
🫡2
UnigramDev/Unigram/temp • 59596cb • 29 files, +7/-945
Downgrade TDLib to 1.8.66
UnigramDev/Unigram/temp • 09c2faa • 2 files, +2/-2
Bump version to 12.9
#unigram
Downgrade TDLib to 1.8.66
UnigramDev/Unigram/temp • 09c2faa • 2 files, +2/-2
Bump version to 12.9
#unigram
🫡2
morethanwords/tweb/master • 229ee61 • 4 files, +24/-4
Support stars_purchase_blocked
Stars can be unavailable for purchase at all. Opening the topup for a
purchase — an insufficient balance or a gift — now shows an alert
instead, and the plain Stars screen keeps the balance and the history,
but drops the topup options and the gift button.
Also remove the duplicate stars_purchase_blocked from the app config.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 31620b1 • 2 files, +1/-3
Fix sticker viewer not looping stickers
Passing `emoji` to wrapSticker to get the right skin tone also disabled
looping, since the loop was suppressed for any non-custom emoji. Move
that decision to the only call site that needs it — big emoji bubbles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • d064fdb • 4 files, +122/-26
Fix a drag selection starting inside an album
Pressing next to an album and moving the cursor selected the whole album
before reaching another bubble, for two reasons.
The very first move ran getElementsBetween with first === last, and the
album override expanded its endpoints anyway, so an empty range came back
as every item of the album — enough elements to start the selection.
An album bubble also carries the group's main mid, so crossing from one of
its items to the bubble itself (the spacing between the photos, the
caption, the free space beside it) counted as a second element. A grouped
bubble is now one drag unit: only an item-to-item move inside it makes a
range, and that range stays granular as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 4126377 • 2 files, +25/-1
Fix an ordinary forum opening as a navigation tab
getChat() returns any Chat, so isCommunity was true for every forum
channel and shouldOpenForumAsNavigationTab pushed it into the sidebar
slider instead of the floating forum drawer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 85f27ea • 1 files, +4/-8
Enable the object URL lifecycle in production
The kill switch was on in production builds while the revoke-on-evict
lifecycle waited to be dogfooded. Drop the `import.meta.env.PROD` gate so
shared blob URLs get evicted and revoked everywhere, leaving
?noObjectUrlRevoke=1 as the only way back to the leak-forever behaviour —
which also makes ?forceObjectUrlRevoke=1 pointless, so it goes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Support stars_purchase_blocked
Stars can be unavailable for purchase at all. Opening the topup for a
purchase — an insufficient balance or a gift — now shows an alert
instead, and the plain Stars screen keeps the balance and the history,
but drops the topup options and the gift button.
Also remove the duplicate stars_purchase_blocked from the app config.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 31620b1 • 2 files, +1/-3
Fix sticker viewer not looping stickers
Passing `emoji` to wrapSticker to get the right skin tone also disabled
looping, since the loop was suppressed for any non-custom emoji. Move
that decision to the only call site that needs it — big emoji bubbles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • d064fdb • 4 files, +122/-26
Fix a drag selection starting inside an album
Pressing next to an album and moving the cursor selected the whole album
before reaching another bubble, for two reasons.
The very first move ran getElementsBetween with first === last, and the
album override expanded its endpoints anyway, so an empty range came back
as every item of the album — enough elements to start the selection.
An album bubble also carries the group's main mid, so crossing from one of
its items to the bubble itself (the spacing between the photos, the
caption, the free space beside it) counted as a second element. A grouped
bubble is now one drag unit: only an item-to-item move inside it makes a
range, and that range stays granular as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 4126377 • 2 files, +25/-1
Fix an ordinary forum opening as a navigation tab
getChat() returns any Chat, so isCommunity was true for every forum
channel and shouldOpenForumAsNavigationTab pushed it into the sidebar
slider instead of the floating forum drawer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 85f27ea • 1 files, +4/-8
Enable the object URL lifecycle in production
The kill switch was on in production builds while the revoke-on-evict
lifecycle waited to be dogfooded. Drop the `import.meta.env.PROD` gate so
shared blob URLs get evicted and revoked everywhere, leaving
?noObjectUrlRevoke=1 as the only way back to the leak-forever behaviour —
which also makes ?forceObjectUrlRevoke=1 pointless, so it goes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • 08d07c2 • 4 files, +149/-21
Fix a forum row rendering two '@' badges for one mention
The unread badge turning into an '@' was decided from the unread topics while
the separate mentions badge was decided from the unread messages, so a forum
with a single unread topic holding more than one unread message rendered both
at once (#628). In "view as messages" both came from the same count, which is
why only one of the badges cleared per mode.
Both now come from one helper, which keeps them mutually exclusive by
construction instead of by the two counts happening to agree, and decides
"there is more unread than this mention" from the message count — the forum
tab's All Chats row suppresses its unread badge, so the mentions badge is the
only indicator it has. The Community row counted its chats the same way and
gets the same treatment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 108d3f3 • 16 files, +386/-121
Speed up the chat list
The collapsed-Community projection key walked the whole peer store with
Object.values(). That store is a reactive proxy, so the walk allocated a signal
per peer and subscribed the memo to all of them: ~1.1s of main thread on a
2000-chat account, re-run whenever any peer changed at all — an unrelated user
coming online was enough. It now looks up only the known Community ids, the way
the worker's getCollapsedCommunityPeerIds already did.
Every row of the main list also carried the Community child badge as a custom
element, so the virtual list tore its reactive root down and rebuilt it on every
scroll pass — 93% of a row's whole mount cost, paid whether or not the account
belongs to a single Community. The badge's lifetime now follows the dialog
element instead of DOM connectedness.
Rows that arrived already loaded were revealed one per ~8ms timer, so a window
of them needed as many serial hops to show up; the whole ready batch goes at
once now. The shrink check kept its debounce — it must not cut the list out from
under a running scroll — but re-armed the timer on every row mount and unmount,
where one timer re-checking on each tick debounces the same way for a fraction
of the traffic. The awaiting-task debug name was built on every single invoke
even though only the stuck watchdog and the debug log ever read it.
Touching the projection key turned up the predicate behind it spelled out in
seven places, three of them inverted, and the peer-to-Community lookup in four
more; isCollapsedCommunity() and getLinkedCommunityId() now hold both. The copy
in the Community's own settings tab is left alone on purpose and says why: it
mirrors the flag for a toggle rather than asking whether the Community folds
chats into our list, so it must not demand membership.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 81ac5d4 • 300 files, +1362/-1374
Build
#webk
Fix a forum row rendering two '@' badges for one mention
The unread badge turning into an '@' was decided from the unread topics while
the separate mentions badge was decided from the unread messages, so a forum
with a single unread topic holding more than one unread message rendered both
at once (#628). In "view as messages" both came from the same count, which is
why only one of the badges cleared per mode.
Both now come from one helper, which keeps them mutually exclusive by
construction instead of by the two counts happening to agree, and decides
"there is more unread than this mention" from the message count — the forum
tab's All Chats row suppresses its unread badge, so the mentions badge is the
only indicator it has. The Community row counted its chats the same way and
gets the same treatment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 108d3f3 • 16 files, +386/-121
Speed up the chat list
The collapsed-Community projection key walked the whole peer store with
Object.values(). That store is a reactive proxy, so the walk allocated a signal
per peer and subscribed the memo to all of them: ~1.1s of main thread on a
2000-chat account, re-run whenever any peer changed at all — an unrelated user
coming online was enough. It now looks up only the known Community ids, the way
the worker's getCollapsedCommunityPeerIds already did.
Every row of the main list also carried the Community child badge as a custom
element, so the virtual list tore its reactive root down and rebuilt it on every
scroll pass — 93% of a row's whole mount cost, paid whether or not the account
belongs to a single Community. The badge's lifetime now follows the dialog
element instead of DOM connectedness.
Rows that arrived already loaded were revealed one per ~8ms timer, so a window
of them needed as many serial hops to show up; the whole ready batch goes at
once now. The shrink check kept its debounce — it must not cut the list out from
under a running scroll — but re-armed the timer on every row mount and unmount,
where one timer re-checking on each tick debounces the same way for a fraction
of the traffic. The awaiting-task debug name was built on every single invoke
even though only the stuck watchdog and the debug log ever read it.
Touching the projection key turned up the predicate behind it spelled out in
seven places, three of them inverted, and the peer-to-Community lookup in four
more; isCollapsedCommunity() and getLinkedCommunityId() now hold both. The copy
in the Community's own settings tab is left alone on purpose and says why: it
mirrors the flag for a toggle rather than asking whether the Community folds
chats into our list, so it must not demand membership.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 81ac5d4 • 300 files, +1362/-1374
Build
#webk
🫡4
morethanwords/tweb/claude/story-viewer-not-opening-owlr57 • 4954a9c • 3 files, +98/-5
Fix the stories list not opening the viewer in Firefox
The stories row mounts folded and is only ever unfolded by the wheel handler in
useCollapsable, which read the direction off `wheelDeltaY`. That property is a
non-standard WebKit/Blink leftover Firefox has never implemented, so there it
came back `undefined`, `-wheelDeltaY` was NaN, and `NaN < 0` resolved every
wheel to STATE_FOLDED — including the guard right below it, which then saw no
state change and returned. The row could not be expanded at all, and a folded
one carries `disable-hover` (`pointer-events: none !important`), so clicking a
story did nothing and the viewer never opened. It reads `deltaY` now: the
standard equivalent, same sign, and the sign is all onMove takes from it. The
profile avatar carousel uses the same hook and was collapsed the same way.
Reproducing this also turned up the segments-folded memo being declared after
the element that renders the items: building it maps `peers()` through `Item`
straight away, so a list whose provider already holds its peers read the memo
inside its temporal dead zone and threw instead of rendering a single item. No
current call site constructs it that way — both pass their peers in later — so
the declaration just moves up above the element.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNLQh1QbfpPyLmQk9vhECq
#webk
Fix the stories list not opening the viewer in Firefox
The stories row mounts folded and is only ever unfolded by the wheel handler in
useCollapsable, which read the direction off `wheelDeltaY`. That property is a
non-standard WebKit/Blink leftover Firefox has never implemented, so there it
came back `undefined`, `-wheelDeltaY` was NaN, and `NaN < 0` resolved every
wheel to STATE_FOLDED — including the guard right below it, which then saw no
state change and returned. The row could not be expanded at all, and a folded
one carries `disable-hover` (`pointer-events: none !important`), so clicking a
story did nothing and the viewer never opened. It reads `deltaY` now: the
standard equivalent, same sign, and the sign is all onMove takes from it. The
profile avatar carousel uses the same hook and was collapsed the same way.
Reproducing this also turned up the segments-folded memo being declared after
the element that renders the items: building it maps `peers()` through `Item`
straight away, so a list whose provider already holds its peers read the memo
inside its temporal dead zone and threw instead of rendering a single item. No
current call site constructs it that way — both pass their peers in later — so
the declaration just moves up above the element.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNLQh1QbfpPyLmQk9vhECq
#webk
🫡3
morethanwords/tweb/master • b9d75a0 • 10 files, +123/-40
Polish the Community chats panel
A bot linked to a Community shows up in "Chats you can view" as an ordinary
dialog row, and until its chat is started it has no last message at all, so the
row was left with an empty subtitle. It falls back to the peer's own status now
— "bot", the string the request-to-join and hidden sections already use and
what tdesktop puts under such a row. The fallback lives in one place with the
botforum's "no messages yet" and applies only to rows that belong to a
Community list, so the main chat list keeps its blank subtitle.
The header carried the Community name alone, while every forum tab has a second
row under its title. It now says how many chats the Community has, counted from
the linked peers the same way the Community row in the chat list counts them.
The avatar moved out of the title into a slot the forum tab itself provides, so
the title and the subtitle keep the shared sidebar-header styles instead of a
private copy that had to override their padding; that shared inline padding is
2px wider now, for every forum header.
Opening a chat from the panel rebuilt its row. The projection runs several
times per open — the dialog, the peer and the read state all move — and every
pass reappended each row to its list, reinserted the hidden-chat icon and
rewrote the dataset. Reappending a node that is already in place still detaches
it, taking everything rendered inside it down with it. Rows are positioned by
index now, which does nothing while the order holds, and the icon and the
dataset are only written when they actually change: an open went from 29 DOM
mutations to the three the click itself makes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 7577288 • 8 files, +146/-23
Stop offering menu actions that aren't available
Editing a Community was offered to every member of it. hasRights ran a
Community through the group branch, where change_info is a member right that
the default banned rights can only take away, and on top of that granted
change_info outright when the Community carried no rights object at all — so
canEditCommunity, which starts at change_info, was true for anyone. A Community
is administered like a channel: tdesktop maps it to a channel without the
megagroup flag, so its canEditInformation() asks for the granted admin right or
the creator, and tweb's own Community admin editor already lists change_info
among the four rights an admin is given. hasRights agrees now — change_info and
invite_users on a Community need that admin right.
That leaves a member looking at a menu button that opens nothing. The toggle
already refuses to open a menu whose every item failed its verify, but the
button itself stayed on screen. The Community tab rechecks its items whenever
the Community changes and hides the trigger while none of them pass; the check
is the one the shared media header was already doing, moved next to
filterButtonMenuItems so both go through the same code.
"View as topics" had the same shape of problem: its only condition was the
dialog's view_forum_as_messages flag, which outlives the forum it was set for,
so the action showed up for chats that are not forums at all and would have
opened a topics list that does not exist. The chat list menu and the chat
header now ask the peer whether it is a forum first, the way tdesktop's
addViewAsTopics does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Polish the Community chats panel
A bot linked to a Community shows up in "Chats you can view" as an ordinary
dialog row, and until its chat is started it has no last message at all, so the
row was left with an empty subtitle. It falls back to the peer's own status now
— "bot", the string the request-to-join and hidden sections already use and
what tdesktop puts under such a row. The fallback lives in one place with the
botforum's "no messages yet" and applies only to rows that belong to a
Community list, so the main chat list keeps its blank subtitle.
The header carried the Community name alone, while every forum tab has a second
row under its title. It now says how many chats the Community has, counted from
the linked peers the same way the Community row in the chat list counts them.
The avatar moved out of the title into a slot the forum tab itself provides, so
the title and the subtitle keep the shared sidebar-header styles instead of a
private copy that had to override their padding; that shared inline padding is
2px wider now, for every forum header.
Opening a chat from the panel rebuilt its row. The projection runs several
times per open — the dialog, the peer and the read state all move — and every
pass reappended each row to its list, reinserted the hidden-chat icon and
rewrote the dataset. Reappending a node that is already in place still detaches
it, taking everything rendered inside it down with it. Rows are positioned by
index now, which does nothing while the order holds, and the icon and the
dataset are only written when they actually change: an open went from 29 DOM
mutations to the three the click itself makes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • 7577288 • 8 files, +146/-23
Stop offering menu actions that aren't available
Editing a Community was offered to every member of it. hasRights ran a
Community through the group branch, where change_info is a member right that
the default banned rights can only take away, and on top of that granted
change_info outright when the Community carried no rights object at all — so
canEditCommunity, which starts at change_info, was true for anyone. A Community
is administered like a channel: tdesktop maps it to a channel without the
megagroup flag, so its canEditInformation() asks for the granted admin right or
the creator, and tweb's own Community admin editor already lists change_info
among the four rights an admin is given. hasRights agrees now — change_info and
invite_users on a Community need that admin right.
That leaves a member looking at a menu button that opens nothing. The toggle
already refuses to open a menu whose every item failed its verify, but the
button itself stayed on screen. The Community tab rechecks its items whenever
the Community changes and hides the trigger while none of them pass; the check
is the one the shared media header was already doing, moved next to
filterButtonMenuItems so both go through the same code.
"View as topics" had the same shape of problem: its only condition was the
dialog's view_forum_as_messages flag, which outlives the forum it was set for,
so the action showed up for chats that are not forums at all and would have
opened a topics list that does not exist. The chat list menu and the chat
header now ask the peer whether it is a forum first, the way tdesktop's
addViewAsTopics does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • fa6ec9f • 2 files, +10/-179
Fix the emoji panel jumping to the window edge in production
The picker attached to an input field (folder name, topic title) opened
pinned to the right edge of the window instead of next to its button —
but only in a built bundle, never in dev, which is what made it so
puzzling.
lightningcss lowers logical inset properties into separate `:lang()`
rules emitted after the declaration block, and those carry a higher
specificity. So `.is-standalone`'s `inset-inline-start: auto` came back
as `left: auto` behind the `left: min(…, var(--left))` authored right
below it, and the panel fell through to `right: 0` from the base rule.
The vertical anchor kept working, which is why it looked like only the
horizontal position was broken. Sass alone doesn't do that rewrite, so
dev always resolved the physical `left` last and looked correct.
Anchor the standalone panel with physical properties only, matching the
physical viewport coordinates JS measures into --top/--left, and reset
`right` explicitly — in RTL it's `left` that gets dropped when the box
is over-constrained.
The same trap was swallowing an optical nudge in the empty-chat
placeholder: `margin-inline-end` + `margin-left` in one block leaves RTL
with the end margin and no nudge at all. Made it `margin-inline-start`
so it mirrors. Also dropped a verbatim 178-line duplicate of
`.empty-bubble-placeholder` that a merge left behind in cef780979 —
comparing the compiled rule sets before and after confirms it changed
nothing but the margin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • c8e1a97 • 3 files, +98/-5
Fix the stories list not opening the viewer in Firefox
The stories row mounts folded and is only ever unfolded by the wheel handler in
useCollapsable, which read the direction off `wheelDeltaY`. That property is a
non-standard WebKit/Blink leftover Firefox has never implemented, so there it
came back `undefined`, `-wheelDeltaY` was NaN, and `NaN < 0` resolved every
wheel to STATE_FOLDED — including the guard right below it, which then saw no
state change and returned. The row could not be expanded at all, and a folded
one carries `disable-hover` (`pointer-events: none !important`), so clicking a
story did nothing and the viewer never opened. It reads `deltaY` now: the
standard equivalent, same sign, and the sign is all onMove takes from it. The
profile avatar carousel uses the same hook and was collapsed the same way.
Reproducing this also turned up the segments-folded memo being declared after
the element that renders the items: building it maps `peers()` through `Item`
straight away, so a list whose provider already holds its peers read the memo
inside its temporal dead zone and threw instead of rendering a single item. No
current call site constructs it that way — both pass their peers in later — so
the declaration just moves up above the element.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNLQh1QbfpPyLmQk9vhECq
#webk
Fix the emoji panel jumping to the window edge in production
The picker attached to an input field (folder name, topic title) opened
pinned to the right edge of the window instead of next to its button —
but only in a built bundle, never in dev, which is what made it so
puzzling.
lightningcss lowers logical inset properties into separate `:lang()`
rules emitted after the declaration block, and those carry a higher
specificity. So `.is-standalone`'s `inset-inline-start: auto` came back
as `left: auto` behind the `left: min(…, var(--left))` authored right
below it, and the panel fell through to `right: 0` from the base rule.
The vertical anchor kept working, which is why it looked like only the
horizontal position was broken. Sass alone doesn't do that rewrite, so
dev always resolved the physical `left` last and looked correct.
Anchor the standalone panel with physical properties only, matching the
physical viewport coordinates JS measures into --top/--left, and reset
`right` explicitly — in RTL it's `left` that gets dropped when the box
is over-constrained.
The same trap was swallowing an optical nudge in the empty-chat
placeholder: `margin-inline-end` + `margin-left` in one block leaves RTL
with the end margin and no nudge at all. Made it `margin-inline-start`
so it mirrors. Also dropped a verbatim 178-line duplicate of
`.empty-bubble-placeholder` that a merge left behind in cef780979 —
comparing the compiled rule sets before and after confirms it changed
nothing but the margin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
morethanwords/tweb/master • c8e1a97 • 3 files, +98/-5
Fix the stories list not opening the viewer in Firefox
The stories row mounts folded and is only ever unfolded by the wheel handler in
useCollapsable, which read the direction off `wheelDeltaY`. That property is a
non-standard WebKit/Blink leftover Firefox has never implemented, so there it
came back `undefined`, `-wheelDeltaY` was NaN, and `NaN < 0` resolved every
wheel to STATE_FOLDED — including the guard right below it, which then saw no
state change and returned. The row could not be expanded at all, and a folded
one carries `disable-hover` (`pointer-events: none !important`), so clicking a
story did nothing and the viewer never opened. It reads `deltaY` now: the
standard equivalent, same sign, and the sign is all onMove takes from it. The
profile avatar carousel uses the same hook and was collapsed the same way.
Reproducing this also turned up the segments-folded memo being declared after
the element that renders the items: building it maps `peers()` through `Item`
straight away, so a list whose provider already holds its peers read the memo
inside its temporal dead zone and threw instead of rendering a single item. No
current call site constructs it that way — both pass their peers in later — so
the declaration just moves up above the element.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNLQh1QbfpPyLmQk9vhECq
#webk
🫡3
morethanwords/tweb/master • 5052955 • 1 files, +114/-51
Fix the stories store losing track of its current story
Clicking a story in the sidebar list threw before the viewer could open:
`actions.set` looked the clicked peer up with `indexOf`, and on a miss wrote the
resulting -1 straight into `index`, so `state.peer` read undefined and the
`checkForNearestSkipped` prefetch dereferenced it. That prefetch is an
optimization — it must never be able to break opening a story. It bails on a
missing peer now, `set` falls back to looking the peer up by id, and every
writer of `index` clamps into the list instead of parking it out of bounds
(`deletePeer` used to leave it past the end, `addPeers` could write -1).
What put a peer out of the list in the first place was `addPeers`: it built a
peerId-to-index map once, then spliced the array inside the loop. From the
second peer of a batch on, those positions were stale, so `peers[idx] = peer`
overwrote an unrelated entry — a reordering update over `[p1, p2, p3]` yields
`[1, 1, 2]`, p3 gone and p1 duplicated. The position is looked up per iteration
now.
Two more things that were quietly not working:
`load()` never paginated. `loadPromise` is assigned synchronously, so the
recursive call inside `.then` hit the in-flight guard and got handed the very
promise it was called from — one page, always, and `loaded` stuck at false. It
forces the next page now, only the latest run may clear the guard, and a server
that keeps `has_more` on an unchanged state no longer spins us on one page.
`getNearestStories` fed only the peer index back into `getNearestStory`, which
re-derives the story index from the store — so all three prefetch iterations
returned the same story. It passes the story index it reached along now.
Also: `onStoryNew` sized its index shift against `state.peer` (a different peer
than the one it was inserting into) and `onStoryDeleted` decremented on
`index >= storyIndex`, producing -1 when both were 0. Both go through one
`shiftStoryIndex` helper. Album snapshots take a generation guard so a slow
reply for an abandoned album can't clobber the current one, async callbacks and
the story timer stop at disposal, and `freezedSorting` / `changeTimeout` move
out of the store — a `Set` is not wrappable and a timer id has no business
notifying subscribers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Fix the stories store losing track of its current story
Clicking a story in the sidebar list threw before the viewer could open:
`actions.set` looked the clicked peer up with `indexOf`, and on a miss wrote the
resulting -1 straight into `index`, so `state.peer` read undefined and the
`checkForNearestSkipped` prefetch dereferenced it. That prefetch is an
optimization — it must never be able to break opening a story. It bails on a
missing peer now, `set` falls back to looking the peer up by id, and every
writer of `index` clamps into the list instead of parking it out of bounds
(`deletePeer` used to leave it past the end, `addPeers` could write -1).
What put a peer out of the list in the first place was `addPeers`: it built a
peerId-to-index map once, then spliced the array inside the loop. From the
second peer of a batch on, those positions were stale, so `peers[idx] = peer`
overwrote an unrelated entry — a reordering update over `[p1, p2, p3]` yields
`[1, 1, 2]`, p3 gone and p1 duplicated. The position is looked up per iteration
now.
Two more things that were quietly not working:
`load()` never paginated. `loadPromise` is assigned synchronously, so the
recursive call inside `.then` hit the in-flight guard and got handed the very
promise it was called from — one page, always, and `loaded` stuck at false. It
forces the next page now, only the latest run may clear the guard, and a server
that keeps `has_more` on an unchanged state no longer spins us on one page.
`getNearestStories` fed only the peer index back into `getNearestStory`, which
re-derives the story index from the store — so all three prefetch iterations
returned the same story. It passes the story index it reached along now.
Also: `onStoryNew` sized its index shift against `state.peer` (a different peer
than the one it was inserting into) and `onStoryDeleted` decremented on
`index >= storyIndex`, producing -1 when both were 0. Both go through one
`shiftStoryIndex` helper. Album snapshots take a generation guard so a slow
reply for an abandoned album can't clobber the current one, async callbacks and
the story timer stop at disposal, and `freezedSorting` / `changeTimeout` move
out of the store — a `Set` is not wrappable and a timer id has no business
notifying subscribers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡4
morethanwords/tweb/master • 06cdebc • 1 files, +11/-5
Stop counting an empty media type in the shared media subtitle
The Media tab's header subtitle joined both counters unconditionally, so a chat
that has photos and no videos read "12313 photos, 0 videos" — naming a type it
has nothing of. It now lists only the types with a non-zero count.
Dropping the zeros outright can leave nothing to print, though: the filter menu
can narrow the tab to photos only in a chat whose photos count is 0, and an
empty subtitle looks broken. So when no enabled type has anything, it falls
back to a single zero entry ("0 photos") rather than to blank.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Stop counting an empty media type in the shared media subtitle
The Media tab's header subtitle joined both counters unconditionally, so a chat
that has photos and no videos read "12313 photos, 0 videos" — naming a type it
has nothing of. It now lists only the types with a non-zero count.
Dropping the zeros outright can leave nothing to print, though: the filter menu
can narrow the tab to photos only in a chat whose photos count is 0, and an
empty subtitle looks broken. So when no enabled type has anything, it falls
back to a single zero entry ("0 photos") rather than to blank.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡3
morethanwords/tweb/master • d2040a0 • 16 files, +636/-120
Give Community chats the context menu every other dialog gets
A chat row inside a Community panel had almost no context menu: the menu kept a
whitelist, and an item was shown there only if it carried a `communityMode`,
which left Mute/Unmute and Remove from Community and nothing else. But such a
row IS a dialog row — the whitelist is backwards. It is an opt-OUT now: the row
gets the standard items, and only what makes no sense there is dropped.
Add to Folder, Pin/Unpin and Archive act on the chat's place in OUR chat list,
so they are offered only while the Community isn't folded into a single row —
folded, those chats have no place of their own to act on. Everything else is
left to its own verify, which already reduces to what a given row can do: a
chat we only watch has no dialog, so nothing that acts on one survives.
The "chats you can request to join" and "hidden chats" sections had no menu at
all — they are drawn by CommunityPeerDialogList, which never passed
`withContext` to setListClickListener, and their rows carried none of the
dataset the menu reads. Both are wired now, so an admin can unlink a chat from
there too; the rest is verified away, since we can neither open nor read those
chats. Preview is the one item that had to learn the difference: it now also
shows for a `viewable` chat, which has no dialog but does have history in the
row (shift+click already previewed it), and stays hidden for the two kinds we
have no access to.
Remove from Community wore the trash icon, the same one as the Delete item
right below it. It unlinks a chat rather than deleting it, so it takes the
cross — as it does on Android, and as "remove from album" / "remove ads" do
here. Both places that offer the action also had a full copy of the same
confirm-then-unlink flow; they share one now.
Mute on the Community row never appeared, however the Community was set up:
`!managers.appCommunitiesManager.isCommunityMuted(id)` negates a Promise, which
is always false. Unmute worked because it returned the promise instead of
negating it — hence "there is an Unmute but never a Mute". It awaits now.
Muting a whole Community was also reachable from that row alone, i.e. only
while "Show as One Chat" was on. The Community panel's menu offers it too now.
Its popup was a stray as well: PopupPeer draws its avatar from `peerId`, and a
Community can't go through that path (its avatar is the decorated one), so the
popup was passed no peer at all and showed a bare title where every other mute
popup shows an avatar. PopupPeer takes an avatar element now, and the popup
builds the same Community avatar the chat list uses.
Unrelated, from the same panel: the header title and subtitle each held a
wrapper div, because `<Portal mount>` always creates its own container. Both
carry `text-overflow: ellipsis`, which a block child defeats — a long Community
name ran past the header instead of ellipsizing. They mount through a small
InlinePortal helper now, which inserts the children directly into the host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
Give Community chats the context menu every other dialog gets
A chat row inside a Community panel had almost no context menu: the menu kept a
whitelist, and an item was shown there only if it carried a `communityMode`,
which left Mute/Unmute and Remove from Community and nothing else. But such a
row IS a dialog row — the whitelist is backwards. It is an opt-OUT now: the row
gets the standard items, and only what makes no sense there is dropped.
Add to Folder, Pin/Unpin and Archive act on the chat's place in OUR chat list,
so they are offered only while the Community isn't folded into a single row —
folded, those chats have no place of their own to act on. Everything else is
left to its own verify, which already reduces to what a given row can do: a
chat we only watch has no dialog, so nothing that acts on one survives.
The "chats you can request to join" and "hidden chats" sections had no menu at
all — they are drawn by CommunityPeerDialogList, which never passed
`withContext` to setListClickListener, and their rows carried none of the
dataset the menu reads. Both are wired now, so an admin can unlink a chat from
there too; the rest is verified away, since we can neither open nor read those
chats. Preview is the one item that had to learn the difference: it now also
shows for a `viewable` chat, which has no dialog but does have history in the
row (shift+click already previewed it), and stays hidden for the two kinds we
have no access to.
Remove from Community wore the trash icon, the same one as the Delete item
right below it. It unlinks a chat rather than deleting it, so it takes the
cross — as it does on Android, and as "remove from album" / "remove ads" do
here. Both places that offer the action also had a full copy of the same
confirm-then-unlink flow; they share one now.
Mute on the Community row never appeared, however the Community was set up:
`!managers.appCommunitiesManager.isCommunityMuted(id)` negates a Promise, which
is always false. Unmute worked because it returned the promise instead of
negating it — hence "there is an Unmute but never a Mute". It awaits now.
Muting a whole Community was also reachable from that row alone, i.e. only
while "Show as One Chat" was on. The Community panel's menu offers it too now.
Its popup was a stray as well: PopupPeer draws its avatar from `peerId`, and a
Community can't go through that path (its avatar is the decorated one), so the
popup was passed no peer at all and showed a bare title where every other mute
popup shows an avatar. PopupPeer takes an avatar element now, and the popup
builds the same Community avatar the chat list uses.
Unrelated, from the same panel: the header title and subtitle each held a
wrapper div, because `<Portal mount>` always creates its own container. Both
carry `text-overflow: ellipsis`, which a block child defeats — a long Community
name ran past the header instead of ellipsizing. They mount through a small
InlinePortal helper now, which inserts the children directly into the host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#webk
🫡3