Release v12.9.2-3901 — Телеграм - #41
Draft
decoder-dev wants to merge 75 commits into
Draft
Conversation
Reserve left-of-avatar slots for Calls outgoing/voice-chat icons so they no longer clamp under the 52pt photo. Match the composer capsule to the 40pt side circles (2/2 insets). Unify Day/Night accent and folder pill on system blue; make Night incoming links readable; center Contacts thread avatar icons on the avatar frame.
Idealism pass: Calls/Contacts overlaps, composer 40pt, unified #007AFF.
Stock Telegram parked send at capsule.maxX − width so the blue disc ate the pill’s rounded end. Place send at maxX + 6 with a reserved right slot (8 + size + 6) for both empty and has-text — capsule width no longer jumps on the first character, emoji stays inside without clearing an overlay, and ActionButtons returns the true 40pt circle (title pills still expand).
Replace the left paperclip with a drawn Plus (Messages). Keep mic and send in one right-hand slot and crossfade/scale them in place — drop the off-screen mic slide, paper-plane fly-in, and blur. Z-order swaps with recording so lock/stop stay hittable. Telegram attach menu, video notes, long-press send, bots, slowmode, and paid send stay.
Recording lock/stop live on mediaActionButtons; if send was above it from a prior text state, hits would miss the mic chrome. Always elevate media when recording (or when send is not in the slot).
Icon audit: cross-referenced all 823 bundleImageName literals against the
1511 imagesets in the tree. Five names resolve to nothing.
Star the call-rating success HUD asked for a bare
"Star"; the asset is "Call/Star", so the HUD
showed text with no glyph. Path corrected.
Media Editor/MediaVideo the picker grid's video, slo-mo and time-lapse
Media Editor/MediaSlomo badges. No imageset by those names ships (the
Media Editor/MediaTimelapse only MediaVideo in the tree belongs to the watch
target), so every video thumbnail drew its
duration beside an empty 19 pt square. Now taken
from SF Symbols - video.fill, slowmo, timelapse -
the same three Photos uses for the same three
subtypes.
Settings/Permissions/Siri genuinely absent and left absent: the sibling
permission screens use full-colour illustrations
and no substitute matches. PermissionContentNode
already lays out cleanly for a nil icon.
Chat List/Tabs/Holiday/* are also absent but unreachable - useSpecialTabBarIcons
gates them on 24 Dec 2020 to 2 Jan 2021, a window that cannot recur.
Calls list avatar column. The overlap fix reserved the glyph slot per row:
CallListCallItem only when the row had an outgoing arrow and at that arrow's
own 20 pt, CallListGroupCallItem always at the indicator's 22 pt. Three kinds
of row in one list therefore had three avatar origins - 16, 42 and 44 pt - and
the avatars stopped forming a column. Both items now reserve one shared 22 pt
slot unconditionally and centre their glyph inside it: avatars land at 44 pt
everywhere, the 20 pt arrow sits at +1 within its slot.
Composer action slot. frameForInputActionButton() decided the mic was visible
by testing frame.minX against the bounds. That worked while the mic was parked
at width + 8; it now stays in the slot and collapses under a scale transform,
so its frame degenerates to a sub-point rect at the slot centre - still inside
the bounds. Tooltips and recording affordances would anchor to an invisible
control. The panel now records the slot rect during layout and reports nil when
send owns the slot. The z-order swap between mic and send also ran an
unconditional insertSubview on every layout pass, i.e. on every keystroke; it
now reorders only when the two are actually inverted.
Contrast. outgoingSecondaryTextColor is white at 0.5 in Night and 0.65 in Day -
alphas tuned against the stock outgoing bubble, which is darker than the #007AFF
this fork pins. On that blue they measure 2.02:1 and 2.50:1, so the timestamp
and ticks stop being readable. Both to 0.8, i.e. 3.08:1. The voice waveform's
unplayed part rode on the same value and reads by its distance from the played
part rather than absolute legibility, so it keeps its old alpha through a
separate outgoingInactiveControlColor. Night's three list hairlines go from
#545458 at 0.55 - Apple's value, which assumes a #1C1C1E ground - to 0.8, since
plain lists and the chat list here sit on pure black where 0.55 is 1.55:1.
Moving send outside the capsule left three cases deriving the right-hand reservation from whichever control happened to be showing. .empty is the serious one. It means "no custom right control", not "no send button" — MessageInputPanelComponent sets it whenever sendStarsAction is nil, which covers the story reply and story camera composers, and those delegate their send button to this node. Its branch kept a flat 8 pt right inset, which was correct only while send was drawn at maxX − width, inside the pill. With send at maxX + 6 the capsule runs to 8 pt from the panel edge and the 40 pt button starts 2 pt short of it: typing a story reply put roughly 38 of those 40 pt past the edge. The stars and live-mic branches reserved their own widths, but those controls park off-screen the moment there is text — which is exactly when send arrives in the same slot at a different width. And the has-text branch reserved send while the empty branch reserved mic, so any width difference between them resized the capsule on the first keystroke, the very thing moving send outside was meant to stop. All four collapse into one reservation: 8 pt margin, 6 pt gap, and a slot as wide as the widest control that can occupy it. Every case keeps the geometry it already had except .empty, which gains the 46 pt it was missing. Two things fell out of that. hasForward was left with no readers once its branch went, so sendOccupiesActionSlot now uses it instead of recomputing forwardMessageIds != nil inline — same value, and it would otherwise have failed the build under -warnings-as-errors. And effectiveActionButtonsSize survived only to shrink the recorded-voice preview inside the capsule, a clearance the preview needed when send overlapped the pill; with send outside it was 46 pt of dead space at the end of every waveform. Night placeholder text. The palette sweep set itemSecondaryTextColor, itemDisabledTextColor and itemPlaceholderTextColor all to #8E8E93, so an empty field's prompt reads exactly like a filled field's secondary label. Day keeps them apart at #8E8E93 against #C8C8CE, about half the contrast; #636366 mirrors that ratio on a #1C1C1E card, 5.22:1 against 2.84:1. Disabled matching secondary is upstream behaviour in Day too, so that pair is left alone.
A custom right action (stars, or .empty) takes the right slot over, so the mic
node is removed from layout entirely rather than faded. That gate was written
as a bare 'if customRightAction != nil { isHidden = true }' with no else, while
customRightAction itself is assigned in both directions by
ChatTextInputPanelComponent — including back to nil. Any node that made that
transition kept its microphone hidden for the rest of its life.
Everything else driving the mic in this method works in both directions
already, through alpha and scale; the gate now matches.
Messages composer polish, Calls column, contrast, icon fixes, story-reply slot.
The chat-list folder tab strip picks its selection pill's frame with an index taken from reorderedFilters, then bounds that index against filters.count before reading selectionFrames[currentIndex + 1]. Three different arrays. They do line up today. The reconciliation earlier in the method rebuilds reorderedItemIds to exactly the set of ids in filters, and the loop that fills selectionFrames can only skip a filter whose item node is missing, which the loop immediately before it has just created for every filter. So the bound is correct — by a chain of three unstated invariants living in three separate places, any one of which drifting gives an out-of-range read mid-drag. Bounding against selectionFrames itself cannot drift, and the sibling branch higher up in the same method already bounds against the right array. No behaviour change while the invariants hold.
Wrapping. calculateTextFieldMetrics decides how tall the field must be by measuring the text at the width the field will get. It called textFieldInsets(metrics:) itself, which returns the base insets — 8 pt on the right. Layout then overwrote that right inset with 54 pt to reserve the action slot. So the height was measured for a line 46 pt wider than the field really is, and the measure pass also skipped the 10 pt trim layout applies to the in-field accessory clearance: 36 pt of disagreement in total. The visible effect is text that wraps onto a second line while the panel still believes it fits on one, so the field does not grow to meet it. The inset block now resolves before the measurement rather than after it, and is handed to calculateTextFieldMetrics instead of being recomputed there. The accessory clearance moves into one resolvedTextInputRealInsets() shared by both passes, so the trim cannot be applied on one side only. updateTextHeight, which re-measures outside a layout pass, reads the last resolved insets from a stored property, exactly as it already does for leftMenuInset and rightSlowModeInset. Separators. Telegram insets row separators 16 pt on the trailing side, so every hairline in the chat list, Calls and Contacts stopped short of the edge and read as an unfinished line rather than a divider. iOS tables — and Messages, which this fork follows — inset only on the leading side. All three lists now run to the trailing edge; the leading inset, which aligns the hairline with the row's text, is unchanged.
Field height measured on a different width than the field gets, part two. updateTextHeight re-measures the composer outside a layout pass, and its baseWidth carried two terms updateLayout's does not: an offset fed from a hardcoded 0.0, dead since it was introduced, and a full subtraction of additionalSideInsets.right, which double-counts now that the resolved insets are passed in — those already carry that inset, and carry it as the third updateLayout actually applies rather than the whole of it. Both formulas are now character for character the same expression, and the dead offset is gone rather than left to be re-derived by the next reader. Attachment slot reserved for a button parked off-screen. attachmentButtonX sends the button to -8 - leftButtonsWidth for either of two reasons: media recording, or !displayMediaButton. Only the recording half had a matching branch clearing the 46 pt reservation, so the one state that sets displayMediaButton = false — the business-link composer — kept a gap on the capsule's left where no button is drawn. Cloud-password fields invisible on black. The OLED pass flattened freeInputField and freePlainInputField to a #000000 fill. Every consumer of those two draws the field as generateStretchableFilledCircleImage(color: backgroundColor) and never reads strokeColor — the Passport password box in SecureIdAuthPasswordOptionContentNode, and both cloud-password fields in TwoFactorAuthDataInputScreen — so the boxes were black on a black page, and the only state that showed an edge was failure, which paints its own red tint. Back to a #1C1C1E fill, matching itemInputField, whose greys they now share as well; the last two #98989E in the theme went with them.
The screenshot settled it, and not the way I guessed: the deciding term was hasSolidWallpaper, not displayHeader. Stock draws the bubble behind media on any wallpaper that is not a flat colour or a two-stop gradient, so the same photo gets a frame of bubble colour in a chat with a picture background and no frame at all in a chat on the default one. Nothing about the message decides it — only which chat it landed in, which is why some sent photos had the outline and others did not. On stock's blue gradient that frame reads as the bubble. On this fork's pinned flat #007AFF it reads as a blue outline drawn around the image. hideBackground now depends only on whether there is a header to hold. The header term stays: a reply preview or author name needs the bubble behind it to stay readable. The date falls back to the floating pill, which the theme already defines separately for custom wallpapers — serviceMessage.components .withCustomWallpaper.dateFillFloating, an opaque #2C2C2E, so it stays legible over any picture. This widens a path that is already the default rather than opening a new one: both shipped themes use a solid background, so bubble-less media is what every user on the default wallpaper already sees. hasSolidWallpaper had no other reader and is removed rather than left to be written and ignored. Incoming media on a custom wallpaper loses its frame too, by the same rule. Worth a look on a device with a picture background, in both directions, since this is message rendering and I cannot see it from here.
Composer wrap/measure, separators, media bubble rule, password fields, folder bound.
Alternate app icons are the one Premium exclusive in this codebase that is entirely client-side. The artwork ships inside the bundle, selection goes through UIApplication.setAlternateIconName, and nothing about the choice is requested from, granted by or recorded on the server — so the Premium marker on some of them locked an asset the build already contains. Two gates, both in ThemeSettingsController: the entry passed the account's real premium flag to ThemeSettingsAppIconItem, which drew the padlock badge, and selectAppIcon looked the account up purely to decide between applying the icon and pushing PremiumDemoScreen instead. The first now passes true; the second applies directly, and the peer lookup that existed only for that branch is gone with it. Left alone deliberately: the isPremiumDisabled filter above, which removes these icons from the list entirely where the server says Premium is not offered at all. That is a different question from locking them, and this change is about the padlock.
Swiping between tabs was clean and tapping one duplicated the list. That asymmetry is the whole clue, and it points at chatListLocationGeneration — a counter this fork invented that upstream has no equivalent of. How it fails. mapToSignal captures the counter when it subscribes, tags every emission of that chatListViewForLocation with it, and mapToQueue drops any emission whose tag no longer matches. The counter is bumped by setChatListLocation. But the location feeding mapToSignal goes through distinctUntilChanged, so setting a location equal to the current one bumps the counter without producing a new subscription — and the live signal is left tagged with a number that can never match again. From that moment every update it produces is dropped, previousView stops advancing while the list keeps rendering, and the next real location change diffs against a base that is stale by however many updates were swallowed. Rows already on screen get inserted a second time. Only one of the eight setChatListLocation call sites checks that the location actually changed — the pagination one. The scroll-to-top pair does not, and a tab tap reaches it with identical parameters when the list is already at top. A swipe never goes near it, which is exactly the reported asymmetry. The guard was never needed. mapToSignal disposes the previous chatListViewForLocation the moment the location changes, so a superseded view cannot arrive after a switch — that is what mapToSignal is for. Upstream has no filter anywhere along this path. Both halves are removed: the tag is gone from the tuple, the guard is gone from mapToQueue, and setChatListLocation is back to two lines. Verified mapToSignal, the destructure and dequeueTransition are now character for character upstream's. The comment in dequeueTransition described the old mechanism and is rewritten to describe the invariant instead.
Audited the fork's own divergence from upstream rather than the app at large: 325 files carry a real hand-written difference, 17413 added lines. Two findings, both the same shape as every other one this session — a value computed in one place and then re-computed by hand somewhere else. ghostMode. A stored Bool mirroring ghostDontReadMessages && ghostDontSendOnline && ghostDontSendTyping, with that expression written out six times: decode, encode, the memberwise init, and three separate settings mutations — plus isFullGhostMode, which is the same expression under a better name. Nothing reads it: Ghost Mode is driven from the three raw flags in SharedAccountContext, and neither isFullGhostMode nor suppressesMessageReads has a single consumer. A stored copy of a derived value is one forgotten assignment away from disagreeing with what it mirrors, and a fourth ghost toggle would have been exactly that assignment. It is computed now, delegating to isFullGhostMode; the encode stays so an older build still reads a coherent value out of the blob. Equatable is synthesised and its behaviour is unchanged, since the three flags it derives from are still compared. Forwarded media size. convertMediaForAyuForward fell back to the filesystem size when a file arrived without one, handed that to LocalFileReferenceMediaResource, and then built the TelegramMediaFile with the original file.size — so a file with no size produced a resource that knew how big it was and a media object that did not. One resolvedSize now feeds both. Swept clean, reported as such rather than padded: every force-unwrap the fork added (15) is safe by construction — literal keys in a dictionary literal declared beside them, or a dictionary pre-populated from allCases immediately above, which is upstream's own idiom at 98 other sites in the same file. No try!, three as! on statically known types, six fatalError/preconditionFailure all in unreachable protocol stubs. Every Timer, CADisplayLink and NotificationCenter observer the fork added either has matching teardown or is a documented, idempotent process-lifetime singleton.
…ls 44 - Drop leftover Int from ChatListNode filter map (compile break after folders guard drop). - Align sendOccupiesActionSlot with keepSend/slowmode/search; reserve paid-stars width; tooltip respects mic isHidden. - Apply base Night/Day outgoing white@0.8 and Night chrome separators @0.8 on the default theme path. - Calls avatars 44pt; Contacts thread rows keep the shared left column.
Unify mic↔send morph on 0.18s easeInOut, gate hits to the occupying slot, use schedule icons (not filled discs) on the blue well, size send/apply/schedule icons and the stretchable disc to 40pt, align pointer circles, rename media hide case to whenNoHeader, and match Night waveform inactive to Day @0.65.
Release Swift treats unused binding as error (#no-usage). After the folders generation guard drop the weak-self capture was only a nil gate — use a boolean test instead.
Previous commit only updated the changelog note; the #no-usage binding remained. Gate with `self != nil` so the release IPA builds.
CDN decryption. MTAesCtrDecrypt passes both buffers straight to MTAesCtr with a hardcoded keyLength:32, and the initializer reads a full AES block out of the IV (MtProtoKit/Sources/MTEncryption.m:713). Neither encryptionKey nor encryptionIv is validated anywhere upstream of the call, and both arrive in the CDN redirect response, so a short buffer is an out-of-bounds read inside the C layer on an ordinary media download that gets redirected to a CDN datacenter. The existing guard covered only the memcpy on the Swift side and asked for four bytes, so an IV between 4 and 15 bytes passed it and still under-ran the block read. Both sites now require exactly 16 bytes of IV and exactly 32 of key before calling, failing through each file's own error case. Round video transcription. animateTo gated its whole hand-off block on snapshotView(afterScreenUpdates:) returning non-nil - including the completion that sets isHidden and canAttachContent. That call returns nil for a zero-sized or off-screen view, and when it did the node stayed in the state it had already been animated out of. Only the crossfade depends on the snapshot now. Also documented, not changed: ChatTextInputPanelComponent's primary rightAction switch assigns .liveMicrophone to the *secondary* slot. That reads like a typo and I nearly corrected it - but ChatTextInputPanelNode only reads .liveMicrophone out of customSecondaryRightAction, and any non-nil customRightAction hides the mic, so the corrected-looking version would hide the mic and draw nothing. The existing routing is the one that would work; it now says why. Attempted and reverted: removing higChatListCardInset from ChatListItem, which returns 0.0 from all three branches and is threaded through eight sites. A mechanical edit corrupted the expression "width - higCardInset * 2.0" into "width * 2.0". It is inert dead weight with no behavioural cost, and removing it correctly needs per-site edits for no functional gain, so it stays.
In dual-camera round-video mode two threads run processVideoRecording on the same instance - the master's own videoQueue, and the additional output's videoQueue reaching the master through masterOutput.processVideoRecording. The composition branch ran entirely unguarded. Two of the fields it touches are read-modify-write, and those are corruption, not just torn reads: lastSampleTimestamp - read, compared, then conditionally appended and written. Both threads can read the same value, both pass the ordering check and both append, putting out-of-order frames into the recording. The append has to be inside the critical section; leaving it out puts the interleaving straight back. needsCrossfadeTransition - checked and cleared. Both threads can see it set, both clear it, and both arm a crossfade. Guarded by a dedicated compositionSemaphore rather than the existing one: processRoundVideoSampleBuffer takes `semaphore` internally, so holding that across the branch would deadlock on a non-recursive semaphore. The two field sets do not overlap, and the new lock is never held across a call that takes the old one, so they cannot form a cycle. markPositionChange writes currentPosition and lastSwitchTimestamp from the camera-control layer while both capture queues read them every frame, so it takes the lock too; it is not reachable from processVideoRecording, so there is no re-entrancy. Deliberately not locked: the plain reads of currentPosition, lastSwitchTimestamp and crossfadeTransitionStart scattered through the branch. Taking a lock around every read in a 30-60fps callback costs more than it buys, and a stale read there picks a slightly different transition factor for one frame rather than corrupting the file. The read-modify-writes were the actual defect. Not verifiable here - this needs a device with dual-camera round video and, to prove the race is gone rather than narrowed, a Thread Sanitizer run.
MessageMediaPlaylistItem.playbackData and .displayData are instance lazy vars, which Swift does not synchronise. Confirmed both readers rather than assuming: - arePlaylistItemsEqual reaches them from the distinctUntilChanged at MediaManager.swift:217, comparing SharedMediaPlayerItemPlaybackStateOrLoading on the producing queue. That sits before the pipeline's deliverOnMainQueue, not after it. - MediaNavigationAccessoryHeaderNode and OverlayAudioPlayerControlsNode read the same properties on main, after that hop. Two threads racing the first access can both run the initialiser, or one can observe a half-written value. Forced in init, while the object is still private to the constructing thread. That costs nothing: exactly three of these exist per state update - current, previous and next, built at PeerMessagesMediaPlaylist.swift:530-535 - and arePlaylistItemsEqual reads both properties on all three regardless. The work is identical, it just happens somewhere single-threaded. Chose this over hoisting both closures into static factories and storing lets. That is the tidier shape, but it moves eighty lines I cannot compile-check, and it buys nothing the two-line version does not already guarantee. Upstream code, not a fork regression.
The intermittent launch crash has been reported several times and is still undiagnosed, because there is no crash log and guessing at it by reading a codebase this size is not a method. This makes the next occurrence produce evidence. ForkLaunchBreadcrumbs installs as the first statement of didFinishLaunchingWithOptions, before anything that can fault, and records which launch stage was running when the process died. Seven stages are marked, from didFinishLaunchingBegan through didBecomeActive, so a crash narrows to a span of launch rather than to the whole of it. The record is read back and logged on the following launch, once Logger.setSharedLogger has run - the file is written independently of Logger precisely because Logger does not exist yet at the point where this has to be armed. Async-signal-safety is the design constraint on the signal path. Almost nothing is legal inside a handler: no allocation, no locks, no Foundation, no String. So the file descriptor and the four-byte record buffer are both prepared at install time, the static the handler touches is initialised there too - first access to a Swift static goes through swift_once, which takes a lock - and the handler itself does nothing but fill four bytes and write() them. The NSException handler is not in signal context and uses Foundation freely. Every handler restores the default disposition and re-raises, so the operating system still produces its own crash report; this only adds the one thing that report cannot carry, which is where in launch the app was. There is no other signal or exception handler in this app, so nothing competes. Nothing here changes behaviour on a launch that succeeds: install truncates the file, a clean run writes nothing to it, and the next launch finds it empty.
The debug screen has always existed but was reachable only through the tg://settings/debug deep link, which is not something anyone finds without being told it is there. It now has a row at the end of the Advanced section, below the everyday settings. New PeerInfoSettingsSection case, dispatched in PeerInfoScreenSettingsActions to the same makeDebugSettingsController the deep link uses. Title follows the fork's existing bilingual pattern, next to forkCustomizationSettingsTitle, because the localisation catalogue has no entry for this. Icon reuses Item List/Icons/Settings on grey, matching how Privacy already pairs that colour with a system-ish glyph. Half that screen was hidden. Two blocks were gated on appBuildType == .internal, and this fork builds from appstore-configuration.json, where is_appstore_build is true - so appBuildType resolves to .public and a Developer Mode row would have opened a screen with pieces missing. Both blocks are now unconditional: reaching the screen is the opt-in, and gating rows inside it on top of that only produces a half-populated screen. Gated there rather than by flipping appBuildType, which has three other readers. One is a commented-out no-op in ChatListNode. One shows the video quality debug overlay. The third sets the default for automatic call-reflector benchmarking in AppDelegate, which starts periodic network work - not something a Settings row should switch on as a side effect. Only the debug screen's own gates moved.
Both ways of unlocking the Archive end in the same conclusion - the owner proved who they are - but only one of them cleared the brute-force failure counter. The password path calls clearFailureState on a match; the biometric path called ArchiveLockSession.unlock() and nothing else. So a few wrong password attempts followed by a successful Face ID left the counter and its cooldown in place, and the next password entry could still be refused. The counter throttles guessing, not the owner. Not a bypass in the other direction: the biometric branch cannot be used to skip a cooldown that is protecting against someone who does not have the password, because passing Face ID means passing Face ID. Verified while here, and left alone: - The trim applied to the entered password is symmetric. Both the set path and the verify path trim whitespace and newlines identically, so a password with a trailing space cannot be stored and then never match. - bindBackgroundRelock claims its slot atomically with a placeholder before subscribing, so two concurrent callers cannot both subscribe and orphan one subscription. - ManagedAutomaticMtProxy's switch hysteresis reads correctly: a voluntary move needs sixty seconds since the last one and a candidate at least thirty per cent faster, and a server that actually failed is in excludedActiveServer and skips both checks. excludedActiveServer is cleared when the connection comes back online and on settings changes, so a server that recovers is not excluded forever and the pool does not shrink over time. Flagged, not changed: archivePasswordHash is a single round of SHA-256 with a per-account salt. The salt does what its comment claims - one precomputed table will not work against every account - but without key stretching a short password still falls quickly if the Keychain is ever extracted. PBKDF2 with a real iteration count would be the right shape, and the file already has a hash-generation upgrade path to model a migration on. It is a security-design change with a migration, on a threat model that starts with the attacker already holding the Keychain, so it wants a deliberate decision rather than being folded into a bug-fix pass.
The typing-suppression gate sits at the top of requestActivity, which is the right place - it is the single chokepoint for outgoing activity, feeding both messages.setTyping and messages.setEncryptedTyping. But it is not only typing that passes through it. PresentationGroupCall acquires .speakingInGroupCall on that same path to drive the live "is talking" highlight other participants see while you speak in a voice chat. So turning on "do not send typing" also stopped that indicator. Other people in the call no longer saw you light up while talking - and nothing in the setting says it does that. It is about not advertising that you are composing a message, not about going quiet in a call you already joined. .speakingInGroupCall is now exempt. Written with the same shape the function uses a few lines further down, where the broadcast-channel branch already singles out this one activity. Checked the two sibling flags for the same over-reach and found none: suppressOnline is enforced in ManagedAccountPresence at the single site that calls account.updateStatus, and suppressMessageReads is applied at the read sites; neither passes through a chokepoint shared with unrelated traffic.
Telegram carries its own language setting, independent of iOS. Every string the fork added itself - the ones with no entry in the localisation catalogue - picked Russian or English by reading Locale.preferredLanguages, which is the device's language. Run iOS in English with Telegram set to Russian and the Archive lock, the extras settings screen, the saved-messages history screen and two context menu entries all came back in English among Russian ones. The reverse pairing gave the mirror image. Six sites, three of them whole string tables: ArchiveLockLocalizedString the entire Archive lock vocabulary ForkExtrasController the extras settings screen MessageSavingHistoryController the saved-messages history screen ChatInterfaceStateContextMenus "Save to Saved Messages", "Select from Author" PeerInfoSettingsItems the proxy row's Auto label DataAndStorageSettingsController the same label, second copy ForkPresentationLanguage holds the presentation language, pushed from SharedAccountContext on every presentation-data emission - the same pushdown the fork already uses for its cross-module values. Set unconditionally rather than inside the stringsUpdated branch, because that flag compares against the value seeded from initialPresentationDataAndSettings, so the first emission can match and skip, leaving the override nil for the rest of the session. Every reader still falls back to its old device-language lookup while the override is nil, so nothing regresses in the window before the first push. Separately: the Edit History context menu entry was a raw English literal, in a file where the fork's two other added entries are bilingual. It now goes through the same helper the Settings rows use, which also gives it uk and be rather than only ru.
Previous 3900 tag only updated the changelog; the Swift compile error (var never mutated) was still on the branch tip. Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
Same -c opt unused-mutation error as SettingsUI appIcons: the icon list is now a fixed array, so declare it let. Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
Empty "${RELEASE_FLAGS[@]}" under bash set -u aborts Publish on
non-*-pre tags after a successful archive. Gate optional flags with
a length check and pass --latest only for full releases.
Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
…y links Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
…d actions Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
…to servers Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
…down Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
makeIncomingVideoViewWithEndpointId called getSink on a nil cloneRenderer (requestClone == false), leaving a non-trivial shared_ptr uninitialized. Only call getSink when cloneRenderer exists. CallAudioTone::asTone sized its buffer to _samples.length/2 but memcpy'd the full NSData — one-byte heap overflow on odd-length tones. Copy only what fits.
CallListCallItem only showed a type icon for outgoing calls; incoming and missed rows had an empty slot. Mirror the outgoing voice/video PDFs 180° for incoming, tint missed with the destructive color already used in status text. No new assets — same PresentationResourceKey caching path.
Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
Add missing .webProxy cases in ChatRecentActionsControllerNode, TextLinkHandling, and OpenUserGeneratedUrl so release builds compile and tg://webproxy links open the proxy preview screen. Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
The row was gated behind `!settings.proxySettings.servers.isEmpty || settings.proxySettings.autoFetchPublicMtProxy`, so a fresh install (or after removing every saved server) had no way to even discover proxy settings from the main Settings screen - the row disappeared entirely instead of showing "Disabled". The label logic already had a correct fallback to Settings_ProxyDisabled for this exact state; only the outer visibility gate needed to go. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015LD9rT1B8mc4YRgymdiKmu Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
…Bag/MTBag/DeviceProximityBag) New logs show the same "Collection was mutated while being enumerated" crash at didBecomeActive (last one fixed was MTContext's listener broadcast) recurring after that fix was deployed - so a second, unguarded enumeration site exists somewhere else. Found it: NSBag/SBag/MTBag/DeviceProximityBag are four copies of the same "callback bag" utility, and all four had the identical bug in enumerateItems: - it iterates the raw _items ivar directly while invoking each item's block, so if any callback reenters addItem:/ removeItem: on the same bag (unregistering itself, or registering a new listener in response to the notification), the array is mutated mid-enumeration and crashes. NSBag specifically backs the swizzled UINavigationItem/UIBarButtonItem/ UITabBarItem property proxies (title, badge, bar-button listeners) - exercised constantly as the UI re-syncs on every app resume, which matches the crash's reported launch stage exactly. Give all four the same defensive copy already used for MTContext's _changeListeners. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015LD9rT1B8mc4YRgymdiKmu Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
The "SAVED PROXIES" section always showed three permanent rows - "Add SOCKS5" / "Add MTProto" / "Add WEB Proxy" - before any actual saved server, and that only grows once the WEB proxy catalog also starts listing rows in the same section. Replaced the three with a single "Add Proxy" row that presents an action sheet to pick the type, matching the single-entry-point pattern upstream Telegram uses while still exposing all three connection types this fork added. The "Use proxy" toggle's implicit "no servers yet, tapping this adds one" behavior forced SOCKS5 specifically (arguments.addNewServer(.socks5)), inconsistent with the fact three types now exist. It now opens the same type-picker menu as the dedicated Add Proxy row. Also reviewed this whole screen and its ProxyServerSettingsController counterpart line by line for correctness bugs beyond the redesign - the issues found there (undefined `value` reference, optional/non-optional secret mismatch, fail-open proxy configure, duplicate catalog stableId, unconditional catalog fetch, unguarded Bag/MTContext enumeration, unused BUILD dep) were already fixed in earlier commits on this branch; no new ones turned up in this pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015LD9rT1B8mc4YRgymdiKmu Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
- Fail-closed: unreachable loopback when sidecar fails on first enable - Serialize stop() with startLock to avoid sidecar resurrection race - Always fetch WEB catalog (not gated on Auto MTProxy toggle) - Hide :443 suffix and ping row for WEB proxies in list/preview - Conference declined calls show missed icon Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
- WebProxyManager: non-blocking configure with generation-tracked async start - registerWebProxySidecarReapply: network re-applies settings when sidecar becomes ready, fails to start, or stops at runtime - Remove WEB proxy catalog fetch/UI; proxies are added manually only - Localize WEB proxy strings via SocksProxySetup.ProxyWeb / MaskingSite Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
- Multi-stop radial disc with specular highlight, rim light, vignette - Metallic annulus ring with sheen and shadow - Layered plane: shadow, body, fold facet, wing highlight, crease line - Supersampled downscale for crisp small sizes - Enhanced Icon Composer SVG layers (Plane/Oval/Ring gradients) - Regenerate all 20 alternate icon PNG sets Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
Cartoon soldier launching a paper plane with Russian flag background. Available in Settings → Customization → App Icon as «Патриот» / Patriot. Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
Regenerate PatriotPlaneIcon with higher detail: tactical plate carrier, ushanka with eagle emblem, sleeve chevron (tricolor + РФ), paper plane launch, Russian flag backdrop. Add missing TelegramUIPreferences import for ForkPresentationLanguage label. Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PatriotPlaneIcon v3
Updated the custom Patriot alternate app icon with higher detail:
Also fixes missing
TelegramUIPreferencesimport for the localized «Патриот» / Patriot label in icon settings.Preview:
/opt/cursor/artifacts/telegram-icon-patriot-plane-1024.png