Read-through of Telegram/Controls/FormattedTextBox.cs and
Telegram/Controls/Chats/ChatTextBox.cs after the typing/pasting performance work.
Line numbers are against the working tree at the time of writing.
Everything below is unfixed. Check an item off in the same commit as its fix.
-
_updateLockedstays set ifGetFormattedTextthrows — it set the field and cleared it with the whole TOM walk in between and notry/finally. One exception disabledUpdateCustomEmojifor the lifetime of the control (it early-returns on_updateLocked), so custom emoji stopped rendering in the box with no other symptom and no way back. The same went forBatchDisplayUpdates, which left the editor frozen. Split intoGetFormattedTextImplthe waySetTextalready was, with both undone in afinally. -
_undoGroupdrifts if anything betweenBeginUndoGroupandEndUndoGroupthrows — eight pairs, none of them in atry/finally. Once the counter was stuck above zero,OnTextChangingstopped callingUpdateFormatpermanently (it's gated on_undoGroup == 0), so blockquote font sizes silently stopped being normalized.BeginUndoGroupreturns a disposable scope now, so the pairing can't be skipped. -
CharacterReceivedcan be subscribed more than once — it subscribed inLoadedand unsubscribed inUnloaded.Loadedcan fire again without an interveningUnloaded(re-parenting), andCoreWindowlives for the whole session, so a second subscription both replaced the emoticon twice per keystroke and pinned the control until the app exits. Guarded with a flag.
-
DateTime.Nowtwice per keystroke — in the typing indicator.Nowresolves the time zone on every call, whichLogger.csalready documents. It measures an interval, so it usesLogger.TickCountnow: read once, and monotonic, so a clock correction can't leave the indicator silent until the clock catches up. -
LoadQuickReplyShortcutsis sent per keystroke while typing a command —GetCommandsruns for every new query that doesn't match the previousAutocompleteList, so typing/startsent six identical requests. Answering the// TODO: is this actually needed?that sat on it: yes —GetQuickReplyShortcutsreturns empty until the update it asks for arrives, and the only other caller is a page the user may never open. It's anIClientService.LoadQuickReplyShortcuts()now, asking once per session, so every caller gets that rather than each tracking it. -
The whole draft is materialized on every selection change — this runs on every keystroke and every caret move. Only three paths needed the string: the inline-bot search, which is dead without an inline bot to address; the sticker branch of
TryGetAutocomplete, which is reached only when the whole message is one emoji; andSearchByInlineBot, which only matches a username starting the message — one character answers that.TryGetAutocompletenever used thetextandqueryit was handed, so both parameters are gone. -
UsernameCollectionre-queriesGetTopChatsfor every autocomplete query —ChatTextBox.cs:719and:736. Two round trips per@query; the top-chat lists barely move within a session. -
A
CancellationTokenSourceper keystroke on the autocomplete path —CancelEmojiboth cancelled and allocated a replacement, andSetAutocompletecalls it on nearly every keystroke with no query to hand the token to. It also dropped the cancelled source without disposing it. Split intoCancelEmojiandBeginEmoji, inChatTextBoxand inCaptionTextBox, which carries a copy of the same code._inlineBotTokenwas fine as it was: every site that replaces it goes throughCancelInlineBotTokenfirst.
-
ChatTextBoxredeclaresContentElement—ChatTextBox.cs:45shadowsFormattedTextBox.cs:74. Two fields and twoGetTemplateChildcalls for one template part. Make the base fieldprotectedand drop the derived one. -
UpdatePaddingis an unfinished experiment —FormattedTextBox.cs:213-:259, marked as one, reachable only from theFooterSizesetter. The scale-factor fallback its comment describes doesn't exist::228assigns1.0and the next line overwrites it inside a bare block that dereferencesXamlRootunguarded. Finish it or delete it. -
HandwritingView.Unloadedis subscribed with a local function —FormattedTextBox.cs:412-:415. It does unsubscribe itself, and delegate equality makes that work, but it's the pattern the project rules forbid, and ifTryClose()doesn't raiseUnloadedthe handler stays subscribed with its captured state. -
ContentElement.ViewChangedis subscribed on everyOnApplyTemplate—FormattedTextBox.cs:271, never removed, so a second template application doubles the handler. The same method null-checksBlocksandCustomEmojibut notContentElement. -
Check that
MergeParagraphsalways advances —FormattedTextBox.cs:1961. Thewhilebody does nothing whensearchRange.StartPosition <= range.StartPosition, and nothing else moves the range, so termination rests entirely onFindTextcontinuing past its own match.
-
Textreads withTextGetOptions.None, so it carries hidden runs — hyperlink URLs and custom emoji metadata — alongside the visible text. That is on purpose: its consumer isChatView.CheckMessageBoxEmpty, and the URL of a hyperlink lives only in that hidden run, soNoHiddenwould mean no link preview for a text-url entity. Don't "fix" it. -
IsLongerThanMaxLengthreturnsexceeding = lengthwhenIsReadOnly(FormattedTextBox.cs:1850), so callers that truncate toexceedinginsert the whole string instead of nothing — a paste into a read-only box would go through. Nothing setsIsReadOnlyon aFormattedTextBoxtoday, so it's unreachable; fix it if the property ever gets used. -
IsCustomEmojimutates the range it is handed (range.EndPosition -= ...) and its callers depend on that. It reads as a query and isn't one, but the behaviour is load-bearing.
Fixed while investigating; listed so the file reads as the current state:
- The three per-keystroke document walks (
UpdateFormat,UpdateBlocks,UpdateCustomEmoji) now start from one whole-document uniformity probe, and the ranges they walk with are reused rather than reallocated. UpdateBlocksremoved stale blockquote decorations with an index loop that skipped every other element.CustomEmojiCanvas.UpdateEntitiesrecreated everyCustomEmojiFileSourceon every keystroke, each one firing agetCustomEmojiStickersrequest from its constructor.OnCharacterReceivedallocated a byte array, a string and a closure per character typed.SearchInlineBotResultssplit the entire message on spaces per keystroke.ChatViewasked TDLib for a link preview on every keystroke, whatever the text.IsValidUrlhanded the whole string to the entity parser before checking it could be a URL.- Pasting applied entities through one COM range per entity, and inserted without batching display updates.
- Pasting a URL over a selection went through
SetText, and turned an already-linked selection into the label of a link to the new URL. - The composer's font family led with a packaged emoji font, which cost about a millisecond per
word to resolve — a 17,000 character paste froze the UI for 2.7 seconds. See
Telegram/Common/Theme.cs.