Telegram github commits and releases
4.54K subscribers
601 files
20.3K links
Broadcast from the most important Telegram clients' repositories
Download Telegram
morethanwords/tweb/master β€’ ef49559 β€’ 2 files, +75/-0
fix: trimRichText emits out-of-range entity when entity sits in trimmed whitespace

Bug: trimRichText could return a MessageEntity with a negative length (and an
out-of-range offset) whenever an entity lay entirely inside the whitespace that
gets trimmed off the right side of the text. Reached from the folder-title input
(editFolder.tsx -> trimRichText) where it would persist a corrupt
textWithEntities into the dialog filter title.

Repro: trimRichText('hi ', [{_: 'messageEntityBold', offset: 3, length: 2}])
-> bold entity became {offset: 3, length: -1} (offset 3 also > trimmed len 2).

Root cause: the right-trim block only adjusted length, via
`entity.length = text.length - entity.offset`, with no guard for the case where
`entity.offset` itself is already past the trimmed `text.length`. That makes the
subtraction negative and leaves the offset out of bounds. The left-trim path
already clamps offset with Math.max(0, ...); the right-trim path had no
equivalent clamp.

Fix: on the right trim, first pull an offset that fell into the trimmed trailing
whitespace back to `text.length`, then clamp length as before (now never
negative). Finally drop entities that ended up empty (length <= 0) β€” they no
longer reference any real text. Dropping zero-length entities matches the
existing idiom in getRichValueWithCaret (the producer of these entities), which
already splices out entities whose length becomes <= 0.

Tests: added src/tests/trimRichText.test.ts covering entity-in-trailing-
whitespace (the failing case), entity spanning the boundary (length shrinks),
in-range entity (unchanged), leading-whitespace shift, entity-in-leading-
whitespace, and both-ends-trimmed. The trailing-whitespace assertion failed
before the fix (AssertionError: expected -1 to be >= 0) and passes after.

Gates: tsc --noEmit 0, eslint 0 on changed files, vitest run trimRichText.test.ts
6/6 green.

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

#webk
🫑3
morethanwords/tweb/master β€’ 387bfb7 β€’ 2 files, +62/-1
fix: formatNumber violated its string return contract on out-of-range magnitudes

formatNumber(@helpers/number/formatNumber) is the shared count formatter
(view/reaction/unread/star/poll/story badges). Its suffix array
['','K','M','B','T'] (length 5) is indexed by i = floor(log(n)/log(1000)),
unbounded at the top: at n >= 1e15 the index reaches 5, sizes[5] is undefined,
and `value + undefined` coerces to NaN β€” a number, not the declared `: string`
(the negative branch yielded "-NaN").

This is type-hardening, NOT a user-visible bugfix: no real Telegram count gets
near a trillion (the practical ceiling across every caller β€” views, reactions,
poll voters, stars, unread, replies β€” is billions), so the out-of-range path is
unreachable from normal input. The 'T' unit itself is effectively dead too. The
value is contract soundness β€” the function must not silently return a NaN-number
β€” plus a cheap guard if some upstream defect ever feeds it a garbage magnitude
(corrupted Long, overflow), where a clamped "1000T" string beats "NaN" in a badge.

Fix: clamp the unit index with Math.min(..., sizes.length - 1) β€” the same idiom
already shipped for the sibling formatBytes/formatBytesPure overflow. All
in-range output (0, sub-1K, K/M/B/T, negatives, custom decimals) is byte-for-byte
unchanged; out-of-range magnitudes now render in trillions instead of NaN.

Test (src/tests/formatNumber.test.ts): pure-unit Vitest, 7 cases. In-range
behavior asserted unchanged; out-of-range inputs (1e15, 5e15, 1e18, -2e15) must
stay strings free of "NaN"/"undefined". Failed 2/5 before the clamp, 7/7 after.

Gates (in the worktree): tsc --noEmit (0) - eslint (0) - vitest (7 passed).

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

morethanwords/tweb/master β€’ 4d1a80c β€’ 2 files, +22/-1
fix: enforce separator consistency in search-by-date longDate parser

`if(!matches[2] === matches[4])` parsed as `(!matches[2]) === matches[4]`
(always false), so the separator-consistency guard never fired and mixed
separators like "12.03/2024" were accepted as valid date tips. Restore the
intended check (matching DrKLO's `!group(2).equals(group(4))`) so mismatched
separators are rejected.

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

#webk
🫑3
morethanwords/tweb/master β€’ 66b6cc6 β€’ 2 files, +32/-1
fix: trim ALL trailing whitespace in clearBadCharsAndTrim

Bug: cleanSearchText's trim regexp was asymmetric β€” `/^\s+|\s$/g`. The
leading branch (`^\s+`) strips all leading whitespace, but the trailing
branch (`\s$`) is a single-char class with no `+`, so the `$` anchor lets
it match only the very last whitespace character. Any string with two or
more trailing whitespace chars keeps all but one:

clearBadCharsAndTrim('abc ') -> 'abc ' (expected 'abc')
clearBadCharsAndTrim(' foo!!! ') -> 'foo ' (expected 'foo')
clearBadCharsAndTrim('tab\t\t') -> 'tab\t' (expected 'tab')

Root cause: `\s$` vs the correctly-quantified `^\s+`. A trim helper must
strip a run of trailing whitespace, not one char.

Fix: quantify the trailing branch β€” `/^\s+|\s+$/g`. Single-line change in
src/helpers/cleanSearchText.ts; leading-trim and bad-char stripping are
unchanged, inner whitespace is preserved.

Impact: clearBadCharsAndTrim is exported as a general clean+trim helper and
feeds the document wrapper's `ext-${ext}` CSS class derivation
(src/components/wrappers/document.ts) β€” a trailing-whitespace extension
would yield a malformed class. The search-text normalizer it backs should
not emit trailing whitespace in normalized terms.

Evidence: added src/tests/cleanSearchText.test.ts (7 cases). Before fix
5/7 failed (every multi-trailing-whitespace case); after fix 7/7 pass.
Gates: tsc 0 errors, eslint 0, vitest 7/7 green.

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

morethanwords/tweb/master β€’ e5aab72 β€’ 3 files, +55/-3
fix: reject trailing & double underscores in isUsernameValid

isUsernameValid (a port of TDLib is_valid_username) had two dead checks
that always evaluated false, so server-invalid usernames passed the
client-side gate.

Bug
- src/lib/richTextProcessor/validators.ts compared characters to the
empty string instead of '_':
if(username.charAt(username.length - 1) === '') // trailing _
if(username.charAt(i - 1) === '' && charAt(i) === '_') // double _
String.charAt() returns '' only for an out-of-range index, so within a
valid string range it is never '' β€” both branches are unreachable.
- TDLib's reference (td/telegram/misc.cpp is_valid_username) compares
against '_': reject when the last char is '_' and reject consecutive
underscores. The port mistranslated '_' to ''.

Impact
- UsernameInputField (public username / channel link editing) accepted
"name_" and "a__b" client-side; the comment at usernameInputField.ts:38
even acknowledged "does not check the last underscore". The bad name
only got rejected after a server round-trip (USERNAME_INVALID) instead
of an immediate inline error.

Fix
- Compare against '_' in both checks, matching the TDLib reference.
- Drop the now-stale "does not check the last underscore" comment.

Evidence (src/tests/validators.test.ts, 8 cases)
- Before: 3 failed β€” 'abc_'/'username_' (trailing), 'a__b'/'foo___bar'
(consecutive), and isWebAppNameValid('app_'|'a__b') all returned true.
- After: 8/8 pass; single non-trailing underscores ('a_b',
'foo_bar_baz') still accepted (no regression).

Gates: tsc 0 Β· eslint 0 (all changed files) Β· vitest 8/8.

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

#webk
🫑3
morethanwords/tweb/master β€’ dde5000 β€’ 2 files, +98/-1
fix: refresh future relative-time labels at exact boundaries instead of a unit late

Bug (backlog IDEA, iter 4 β€” "formatRelativeTime nextBoundaryDelay
future-boundary case"): a relative-time label pointing at a FUTURE instant
("in 5 minutes", "in 2 hours", …) froze for a full extra unit whenever the
remaining time was an exact multiple of its display unit.

Root cause (src/helpers/date/formatRelativeTime.ts:73): nextBoundaryDelay
computed the timer delay until the label's next change as
`isPast ? (unit - remainder) : (remainder || unit)`.

For a future timestamp the displayed whole-unit count DECREMENTS as `absDiff`
shrinks toward now, so the label changes in `remainder` seconds. When
`remainder === 0` (absDiff is exactly k*unit, e.g. 300s β†’ "in 5 minutes"), the
`|| unit` fallback returned a full unit (60s). But one second later absDiff is
4m59s β†’ "in 4 minutes", so the label should refresh almost immediately. Result:
at every exact future boundary the label stayed one unit too high for up to a
whole unit. The past branch is unaffected β€” `unit - remainder` correctly yields
a full unit at a boundary (a "5 minutes ago" label is valid for the next
minute).

Fix: in the future branch fall back to a ~1s refresh at an exact boundary
(`remainder || 1`) instead of a full unit, matching the `|| 1000` 1-second-tick
idiom the JustNow case already uses. Identical behavior for every non-boundary
case; strictly more correct at boundaries.

Reachability: the only caller is wrapRichText.ts for relative
messageEntityTimestamp entities, which drive a setTimeout off updateInterval to
re-render the label; future timestamps are supported there.

Tests: +10 Vitest cases (new src/tests/formatRelativeTime.test.ts) covering past
boundaries (unchanged), future non-boundaries, and the 3 exact-future-boundary
regressions. The 3 boundary cases FAILED before (updateInterval === unit*1000)
and PASS after; the 7 others passed before and after (no regression).

Gates: tsc 0 Β· eslint 0 Β· vitest 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2ef30af4a5fffeb6c7c9323e0a8cab40dd6bb9f4)

morethanwords/tweb/master β€’ 86bc556 β€’ 2 files, +172/-0
fix: collect custom emoji from poll questions and to-do lists

getUniqueCustomEmojisFromMessage backs the chat context-menu "Open Emojis"
action (contextMenu.ts) β€” it gathers every unique custom emoji in a message to
gate the action's visibility (via .length) and list the emoji packs. It walked
message text, reaction custom emoji and poll ANSWER texts, but missed two other
TextWithEntities fields, so a custom emoji placed there was silently dropped
(action hidden + emoji absent) even though the same emoji elsewhere worked:

- poll QUESTION text (Poll.question is TextWithEntities; createPoll persists
questionEntities from the rich question input).
- to-do list (checklist) text: todo.title and every todo.list[].title
(TodoList/TodoItem carry TextWithEntities; checklist.tsx persists them via
getRichValueWithCaret).

Fix: also iterate poll.question.entities, and the messageMediaToDo todo title +
item titles, through the existing iterateEntities collector. filterUnique()
already dedups overlap across question/answers and across title/items.
Behavior-preserving for the existing paths.

Tests: src/tests/getUniqueCustomEmojisFromMessage.test.ts covers message text,
poll question (regression), poll answers, question+answer dedup, to-do title,
to-do items, and title+item dedup (7/7).

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

#webk
🫑3
morethanwords/tweb/master β€’ 57fe5bc β€’ 1 files, +29/-21
perf(animationIntersector): O(1) byElement index for onObserve + getAnimations

Bottleneck (hot path): the IntersectionObserver callback (`onObserve`) and
`getAnimations(el)` each did an O(groups Γ— items) nested scan over every
animation group on every IO callback β€” i.e. while scrolling sticker/media-dense
chats, each batch of intersection entries walked all groups and `.find()`d
within each. `getAnimations` is also called per video/sticker play decision
(video.ts, bubbles.ts, appImManager.ts, bluffSpoilerController.ts,
SuperStickerRenderer.ts). Cost grows with the number of open/cached animation
groups and items.

Fix (single file): add a parallel `byElement: Map<HTMLElement, AnimationItem[]>`
maintained in lockstep with the existing `byPlayer` Map β€” pushed in
`addAnimation`, spliced (and key-deleted when empty) in `removeAnimation`.
- `onObserve` resolves `byElement.get(entry.target)` then acts on the first
item whose group is not intersection-locked β€” the exact semantics of the old
group scan + per-entry `break` (one item acted on per entry).
- `getAnimations(el)` returns `byElement.get(el)?.slice() ?? []` β€” same array
shape and fresh-copy contract as before.
No call-site signature changes; contained to animationIntersector.ts.

Behavior preserved: identical set of items resolved, identical
visible-set / checkAnimation / intersection-lock semantics. A Proxy on byGroups
confirmed the new path performs ZERO group enumerations, and getAnimations
matches a reference O(groupsΓ—items) linear scan for every element.

Note: this is an algorithmic-hygiene win (removes a term that grows with chat
density), not a measurable per-frame ms saving. Verified with a 5-test suite
(tsc + eslint clean, vitest 5/5); the test file is omitted here to keep the
commit small.

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

#webk
🫑3
morethanwords/tweb/master β€’ ca25dd1 β€’ 3 files, +126/-2
perf: cut redundant per-keystroke work in the message input

Two redundant-work eliminations in src/components/chat/input.ts, the
contenteditable message-input hot path. Behavior preserved.

#2 - keyup re-parsed a second time per char
The `keyup` listener called checkAutocomplete() with no args on EVERY key,
re-walking the contenteditable (getRichValueWithCaret) and re-parsing
markdown+entities - work the `input` handler (onMessageInput) had already done
one tick earlier and fed to checkAutocomplete. classifyInputKeyup (new pure
helper) gates it per key: content keys (printable, Backspace/Delete/Enter) and
inert/modifier keys SKIP (the `input` event already covered them / nothing
changed); only caret-move keys (arrows/Home/End/PageUp/PageDown - including with
a modifier held, e.g. Cmd/Option/Ctrl+arrow for line/word navigation, which
never fire `input`) re-check. Parses per typed char 2 -> 1; wasted walks on
inert keys 1 -> 0.

#3 - emoji search on every plaintext keystroke
The emoji-autocomplete branch ran appEmojiManager.prepareAndSearchEmojis (a
SharedWorker round-trip) for every bare-word keystroke, but a 1-char bare token
can never match the keyword index (SearchIndex minChars=2). isPlausibleEmojiQuery
(new pure helper) gates it: an explicit `:foo` query always searches
(firstChar === ':'), a bare-word query only once it has >= 2 chars. Observable
result identical minus the wasted round-trip.

Behavior preserved: typing/parsing/drafts/typing-notifications untouched (all on
the `input` path); emoji autocomplete still appears for `:foo` and bare words
from 2 chars; mentions/commands/inline/stickers untouched; autocomplete still
re-checks on caret move, including modifier+arrow navigation.

Validated by a deterministic jsdom Vitest suite (pure-helper predicate tables +
call-count drivers) before merge. Gates: tsc 0 - eslint 0.

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

morethanwords/tweb/master β€’ 47e1ace β€’ 1 files, +13/-1
perf: coalesce per-bubble readMaxId round-trip in renderMessage

ChatBubbles.renderMessage runs per bubble; in group/channel chats every
non-unread bubble awaited an identical cross-worker
getReadMaxIdIfUnread(peerId, threadId) β€” N round-trips per render burst for
one read cursor. Route it through a memoizeAsyncWithTTL wrapper
(key peerId_threadId, TTL 0): N->1 per burst, while TTL 0 drops the entry on
the next macrotask so a later render pass still re-reads a fresh value.
Behavior preserved.

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

#webk
🫑3
TGX-Android/Telegram-X/main β€’ 73ba0b7 β€’ 42 files, +2354/-572
Upgrade TDLib to tdlib/td@e0943d0

#tgxandroid
🫑4🀨2
TGX-Android/Telegram-X/main β€’ d76e7b5 β€’ 6 files, +82/-22
Upgrade Gradle to 9.6.0 + FCM to 25.1.0 + Upgrade dependencies + Set target SDK to Android 17

TGX-Android/Telegram-X/main β€’ f5f283a β€’ 1 files, +1/-1
Version bump to `1788`

#tgxandroid
🫑3
TGX-Android/Telegram-X/main β€’ 1e20301 β€’ 2 files, +3/-1
Add `version.sdk_package` property

#tgxandroid
🫑3
telegramdesktop/tdesktop/dev β€’ 228934b β€’ 1 files, +4/-1
Work around qmake race condition

#tdesktop
🫑2
telegramdesktop/tdesktop/dev β€’ ab8d1ff β€’ 3 files, +39/-46
Check GNotification option via base::options::lookup

#tdesktop
🫑3
morethanwords/tweb/master β€’ 9b5b04b β€’ 17 files, +565/-309
perf(stickers): worker-clock free-run for play-once + blink-free first frame

Move infinite AND play-once looping into the rlottie worker's free-run clock,
keeping the UI thread out of the per-frame path for every boolean-loop player:

- rlottie.worker: free-run now honours a loop flag - true wraps at the bound,
false parks on the far bound and emits freeRunEnded so the player settles into
the same paused, end-of-play state command mode reaches via onLap's !loop branch.
- rlottiePlayer: onFreeRunEnded mirrors command-mode end-of-play (clears autoplay
so animationIntersector won't re-play on scroll/visibility ticks). Numeric loops
stay on the UI clock; mainLoopForwards/Backwards rewritten to count the lap via
onLap BEFORE choosing the wrap target, so the final lap parks on the last frame
and numeric loops actually terminate instead of looping forever.
- ensurePresented: re-present the staged frame, await the worker ack, then wait a
few of the tab's own paints (PRESENT_PAINT_WAITS) so dropping the underlay can't
flash a blank cell; append the placeholder canvas BEFORE firstFrame so appearance
listeners see it attached.
- customEmoji: groupPainted fires once the group is fully faded in (immediately
when fade is skipped) and clears placeholders synchronously, so the thumb never
lingers past full canvas coverage.
- apiFileManager: raise TGS_MAX_DECOMPRESSED_SIZE 1MB -> 8MB for large stickers.
- Add a Playwright lottie harness (e2e/, playwright.config, test:lottie) that
renders real .tgs in a browser to guard the no-blink first-frame path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

morethanwords/tweb/master β€’ f7fc2fc β€’ 7 files, +2065/-0
chore: misc tooling, docs, and test leftovers

- .claude: add graphify + tweb-bugs skills, ignore .claude/bugs-cache
- compareVersion: comment noting single-operand iteration is intentional (no behavior change)
- tests/api: opt-in B->A burst-send harness test (TG_BURST=1, skipped by default)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

#webk
🫑3
telegramdesktop/tdesktop/dev β€’ 949f2a6 β€’ 2 files, +111/-46
Expose the chat-folders strip as a list, not a tab control

Report the folders container with the List role (was PageTabList) and
each folder as a ListItem, which matches NVDA's native single-selection
list behaviour better than a tab control. TabListLayout opts in to
RpWidget::accessibilitySelectionList(), so the selection interface and
selected-item focus forwarding apply only to this strip, not to every
List-role widget.

- Locked (premium) folders are ordinary list items now: a ListItem is
clickable, so they can open the Premium box, and they are reachable
with the arrow keys like any other folder (they keep their locked
accessible name/description, and report selectable = false).
- Arrow keys move focus only and scroll the focused folder into view;
activation - switching the chat list, or opening the Premium box for a
locked folder - happens on Enter.
- The list has one roving Tab-stop driven by focus (setListTabStop): a
FocusIn on any folder - arrow, mouse or UIA SetFocus - makes it the
single Tab-stop, wired main menu -> folder -> edit, always demoting the
previous one; the active folder seeds it when nothing is focused. A
refresh re-establishes the Tab-stop on the folder that was focused (or
the active one), so rebuilding the list never leaves it without one. So
Tab/Shift+Tab leave the list the same way regardless of which folder is
focused, with no stray extra tab-stops.

#tdesktop
🫑3
telegramdesktop/tdesktop/dev β€’ f43a2ca β€’ 1 files, +1/-0
Add QScroller experimental option

telegramdesktop/tdesktop/dev β€’ ded7588 β€’ 1 files, +1/-1
Update lib_ui.

#tdesktop
🫑3
telegramdesktop/tdesktop/dev β€’ 54b9a6b β€’ 1 files, +12/-0
Restore Home/End navigation in the chat list

Home/End jumped to the first/last chat until InnerWidget's key handling
was rewritten from a switch into an if/else chain (in 'Extract chats list
accessibility, fix build'), which carried over only Up/Down and Page
Up/Down and dropped the Home/End cases.

Re-add them, skipping by the actual total row count (which selectSkip()
clamps) instead of the previous 1<<20 constant, so it lands on the first
or last row in both the default list and the filtered/search results.

#tdesktop
🫑4
telegramdesktop/tdesktop/dev β€’ e4fa072 β€’ 8 files, +21/-2
Added animated icons to media saving toasts.

telegramdesktop/tdesktop/dev β€’ 145e580 β€’ 43 files, +396/-90
Added animated icons to copy-to-clipboard toasts.

telegramdesktop/tdesktop/dev β€’ bbcc5fc β€’ 6 files, +27/-7
Added animated icons to mute and pin quick action toasts.

telegramdesktop/tdesktop/dev β€’ 39cfbea β€’ 2 files, +14/-3
Added animated icons to shared media and group call pin toasts.

telegramdesktop/tdesktop/dev β€’ 8901dba β€’ 3 files, +6/-0
Added animated icon to chat archiving toast.

telegramdesktop/tdesktop/dev β€’ 6ecdee2 β€’ 3 files, +4/-0
Added animated icon to premium purchased toast.

telegramdesktop/tdesktop/dev β€’ 5ed4e43 β€’ 4 files, +19/-5
Added animated icons to sticker set install and copy toasts.

telegramdesktop/tdesktop/dev β€’ 8fa4ccb β€’ 1 files, +24/-76
Added support for dragging files out of shared media.

telegramdesktop/tdesktop/dev β€’ ff3eb41 β€’ 3 files, +93/-49
Added file preview to shared media drag.

telegramdesktop/tdesktop/dev β€’ cfc99e4 β€’ 22 files, +501/-9
Added support of zoom in shared media for photos.

telegramdesktop/tdesktop/dev β€’ c255a97 β€’ 5 files, +49/-4
Added a shadow to the action buttons in the profile top bar.

telegramdesktop/tdesktop/dev β€’ e1d63f9 β€’ 1 files, +1/-1
Prevented Xcode 27 libc++ platform warning from breaking mac build.

telegramdesktop/tdesktop/dev β€’ cb1d81f β€’ 2 files, +25/-6
Added group emoji badge to emoji list widget.

telegramdesktop/tdesktop/dev β€’ 84a4eee β€’ 1 files, +14/-5
Added blur to poll attached media while loading.

#tdesktop
🫑3
telegramdesktop/tdesktop/dev β€’ 1c1a538 β€’ 1 files, +15/-0
Fix accessible focus on filters refresh.

#tdesktop
🫑3
UnigramDev/Unigram/develop β€’ ff09a2f β€’ 1 files, +1/-11
Undo style

UnigramDev/Unigram/develop β€’ 9e4cc27 β€’ 1 files, +5/-2
Fix null ref

UnigramDev/Unigram/develop β€’ 9aa2550 β€’ 3 files, +225/-17
Improve custom text selection

UnigramDev/Unigram/develop β€’ b309941 β€’ 6 files, +52/-27
Break MessageSelector reference cycle

UnigramDev/Unigram/develop β€’ 703b2fd β€’ 11 files, +199/-85
Fix playback service unsubscribe

UnigramDev/Unigram/develop β€’ 1188ad4 β€’ 1 files, +8/-2
Access violation guard

UnigramDev/Unigram/develop β€’ 9d86f1b β€’ 9 files, +738/-298
Break FormattedTextBlock into pieces, add MessageTextBlock, support expandable quotes

UnigramDev/Unigram/develop β€’ 8e4368b β€’ 1 files, +17/-8
Fix text selection manager events registration

UnigramDev/Unigram/develop β€’ f3a06d3 β€’ 2 files, +29/-0
Add missing interface methods

UnigramDev/Unigram/develop β€’ 1d66e25 β€’ 2 files, +43/-3
Handle custom text selection in chat history

UnigramDev/Unigram/develop β€’ dbfee71 β€’ 5 files, +42/-74
Support translating formatted text

UnigramDev/Unigram/develop β€’ dc5dd1d β€’ 1 files, +21/-186
Improve quotes in rich messages

UnigramDev/Unigram/develop β€’ aab94f7 β€’ 2 files, +13/-5
Use new text block in gallery

UnigramDev/Unigram/develop β€’ 333b44b β€’ 1 files, +4/-1
Disable interactions on album children

UnigramDev/Unigram/develop β€’ 19714bb β€’ 1 files, +4/-0
Don't stackalloc in debug to avoid recompiling every time

#unigram
🫑2
TGX-Android/Telegram-X/main β€’ cea514d β€’ 37 files, +2744/-318
Add `marshmallow` flavor

TGX-Android/Telegram-X/main β€’ 6c14410 β€’ 1 files, +8/-1
Sync `.gitignore` with `translations.telegram.org`

#tgxandroid
🫑3
morethanwords/tweb/master β€’ ceb5160 β€’ 11 files, +19/-48
Address AI Editor review findings

- Fix: removing a saved tone was blocked at the saved-tones limit β€” the
ViewTonePopup unsave path fell through to Promise.reject(), showing an
error toast instead of unsaving. Allow unsave regardless of capacity.
- Bound the composed-message cache (was an unbounded module-level Map).
- Drop duplicate AICOMPOSE_*/TONE_NOT_FOUND literals in ServerErrorType.
- customEmoji renderer: pass the callback to unobserveResize so it removes
only its own resize callback (the new array-based observeResize allows
multiple callbacks per element).
- Remove dead code: simulateDelay/DEBUG scaffolding in styleTab, autoHeight
duration/easing props, inputFieldMessage containerRef prop, and the
byte-identical unused viewTonePopup/limits.ts.
- wrapRichText: wrap the messageEntityDiffReplace case body in a block.
- getMessageEntityFromDocIdOrEmoji: use the @lib alias import.
- lang: "Used by %d persons" -> "people".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

morethanwords/tweb/master β€’ ea7e91b β€’ 300 files, +15781/-2628
Merge branch 'master' into feature/ai-editor

Resolved conflicts:
- Icon font (icons.ts, _variables/_style.scss, tgico.svg/ttf/woff): kept the
PR's regenerated set so the new AI icons work. Re-added master's
`privacypolicy` glyph to icons.ts/_variables.scss as a placeholder
(codepoint eab3) so the tree builds β€” it renders blank until the icon font
is regenerated (`pnpm generate-icons`) from the union of SVG sources
(privacypolicy.svg + the AI icons are both present in assets/icons/).
- inputFieldMessage.tsx: union of imports (PR's AI-editor imports + master's
getOverlayRoot).

Production build passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

morethanwords/tweb/master β€’ 40846ea β€’ 116 files, +6147/-1857
Merge pull request #665 from H7GhosT/feature/ai-editor

AI Editor

morethanwords/tweb/master β€’ 3a0099f β€’ 3 files, +3/-3
Merge pull request #676 from prdsrm/master

fix: update nodejs version #675

#webk
🫑3🀨1
TGX-Android/Telegram-X/main β€’ 3bb31c2 β€’ 15 files, +245/-152
Migrate to `Firebase Installation ID` on Android 6 (Marshmallow) and higher

#tgxandroid
🫑3