In Create group and Create public channel screens the action buttons
(Create / Configure relays) and incognito toggle were rendered as loose
SectionItemViews on the gray canvas with no card chrome. Wrap them in
SectionView so they read as a single card matching the iOS-style facelift.
The display-name input above and the descriptive footer below stay
outside the card (text input keeps its own padding, footer reads as
caption).
Added missing `import SectionView` in AddGroupView.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Contacts" header was a SectionView with empty content lambda, and
the actual contact rows were rendered as separate LazyColumn items
OUTSIDE the SectionView — so they sat on canvas without card chrome.
Move filteredContactChats.forEachIndexed { ContactListNavLinkView }
INSIDE the SectionView lambda in both OneHandLazyColumn and
NonOneHandLazyColumn so the contacts read as a single card matching
the iOS-style facelift.
Same trade-off as GroupChatInfoView members fix (fa29bb7a): lazy
rendering of contact rows replaced with eager composition inside a
Column. For typical contact lists (<100) imperceptible; very long
lists may compose slower on open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same pre-card-chrome pattern as ChatInfoView (fixed in b1a1dad8):
SectionTextFooter("Delete chat messages from your device.") sat inside
the SectionView around ChatTTLOption, so it rendered inside the white
card after PR #6777 added card chrome. Move it out so the caption sits
below the card iOS-style.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The user-selection ExposedDropDownSettingRow at the top of the
servers info screen was rendered loose on the canvas with no card
chrome. Wrap in SectionView so it reads as a card matching the rest
of the screen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four small fixes on the same theme-related screens:
- Move "Customize theme" SectionItemView INSIDE the THEMES SectionView
in AppearanceView so it sits in the same card as Color mode / Dark
mode colors with an auto-divider above it.
- Wrap WallpaperPresetSelector (theme slots + chat preview) and the
conditional Remove-image button in CustomizeThemeView with a
SectionView so they read as a card, matching the Appearance themes
card pattern. Add a SectionDividerSpaced after.
- Drop SectionSpacer() that sat between the Wallpaper tint row and the
Sent message row inside WallpaperSetupView — auto-divider on the
Wallpaper tint SectionItemView already provides separation; the
30dp spacer rendered as extra empty padding inside the card.
- Wrap the Reset colors action in a single-item SectionView so it
reads as its own card, matching the export/import card below.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two layout regressions from earlier facelift commits where slider/item
math assumed DEFAULT_PADDING (20dp) for inner card padding, but the
facelift uses CARD_PADDING (16dp outer) + CARD_ITEM_PADDING (15dp inner)
= 31dp per side instead of 20dp.
- Slider widthIn calc in AppToolbarsSection and MessageShapeSection
used (maxWidth - DEFAULT_PADDING * 2) so the slider was ~22dp wider
than it should be, shrinking the label Box (weight 1f) and clipping
"Transparency" to "Transparenc". Switched to
(CARD_PADDING + CARD_ITEM_PADDING) * 2.
- SettingsActionItemWithContent explicitly passed
PaddingValues(horizontal = DEFAULT_PADDING) to its SectionItemView,
overriding the new CARD_ITEM_PADDING default. That made any row using
SettingsPreferenceItem/SettingsActionItem sit 5dp further inset than
rows using plain SectionItemViewWithoutMinPadding — visible as a left
indent on "Tail" relative to "Corner". Replaced with CARD_ITEM_PADDING
so it matches.
Removed `private` from CARD_PADDING and CARD_ITEM_PADDING in Section.kt
to allow imports from other files (used the same way as SectionView etc.
are imported individually).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- "Review conditions" (ConditionsButton — a bare SectionItemView) was
rendered between the operators card and the messages card on the
canvas without card chrome. Move it inside the operators SectionView
lambda after the operator rows, so it shares the card and gets a
2dp auto-divider above it separating it from the operator list.
- "Save servers" (a bare SectionItemView further down) is now wrapped
in its own SectionView so it reads as a single-item card matching
the iOS-style facelift of the rest of the screen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TextEditor (144dp tall input) sat flush against the top and bottom
edges of its containing SectionView card on the New server screen.
Pass padding = PaddingValues(vertical = DEFAULT_PADDING_HALF) to the
SectionView so the field gets 10dp of breathing room top and bottom
inside the card.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default Compose Material 1 RippleTheme uses hoveredAlpha=0.04 for LIGHT,
producing a Black·0.04 overlay (~#F5F5F5) on white cards — visually 3
units per channel away from the off-white canvas (~#F2F2F2), so the
hover state blends into the canvas and the row looks unfocused.
Add a section-local SectionRippleTheme that mirrors Material's defaults
for everything except hoveredAlpha on LIGHT (raised to 0.08 → ~#EBEBEB
overlay, ~7 units delta from canvas — visibly distinct). Dark themes
keep Material defaults since their hover contrast is already adequate.
Provided via CompositionLocalProvider in all three SectionView variants
alongside LocalInSectionCard, so it scopes only to section card items.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8050676b moved sectionItemDivider() AFTER clickable.padding() in the
modifier chain to make the line paint on top of clickable's hover
indication. Side effect: drawBehind then saw the size of the
padding-reduced content area, not the full row, so dividers rendered
15dp ABOVE the actual row bottom (in the middle of the row's bottom
padding zone) instead of at the row edge between adjacent items.
Fix: keep sectionItemDivider() in the modifier val BEFORE clickable/
padding (so size = full row outer bounds) AND switch from drawBehind
to drawWithContent { drawContent(); drawLine(...) } so the line is
painted AFTER the chain's content + hover indication draw. Both
goals satisfied: divider sits at the true row bottom AND paints on
top of hover overlay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f922d8fc introduced SectionDivider() call in the themes card but forgot
to add the per-symbol import. SectionView/SectionDividerSpaced etc. in
this codebase are imported individually (Section.kt declares them at
top level, not inside a package), so SectionDivider needs its own
import line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SubscriptionsSectionView and SMPSubscriptionsSection both rendered their
InfoRows + control item in a plain Column without card chrome — so on
the Servers info screen the "Message reception" section title sat above
loose rows on the gray canvas (no card), inconsistent with the rest of
the screen. Wrap the inner Column in SectionView so the rows get the
raised iOS-style card look. The custom header Row (title + subscription
status indicator) stays outside the card so the icon stays inline with
the title text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same pre-card-chrome pattern: SectionDividerSpaced was inside the
single-row SectionView around the InfoRow, so after PR #6777 added
card chrome it rendered as a white gap inside the card (under the
auto-divider on the InfoRow), producing the "extra divider + gap"
the user reported.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the members card showed only the current user (owner) and the
add-members button — the actual group members were rendered as separate
LazyColumn items() OUTSIDE the SectionView, so they sat on the gray
canvas without card chrome. Visually inconsistent: owner in a card,
everyone else floating.
Move filteredMembers.value.forEach { ... } INSIDE the SectionView lambda
so every member row is part of the same card as the owner. Drop the
explicit Divider() call (auto-divider handles it now). Move remember
key to member.groupMemberId so per-member state survives reorders.
Trade-off: lazy rendering of member rows is replaced with eager
composition inside a Column. For typical groups (<100 members) this is
imperceptible; very large groups may compose slower on open. Watching
for reports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In ChatInfo > Chat theme screen the wallpaper preset selector, the
wallpaper setup controls, the reset/set-default buttons and the
"Apply to" mode dropdown were rendered as loose composables on a gray
canvas — no card chrome, inconsistent with the rest of Appearance.
Wrap them in SectionView so they read as raised iOS-style cards:
- wallpaper preset selector + setup view → one card
- reset-to-global + set-default buttons → one card
- (advanced mode) Apply-to dropdown → one card
- (collapsed mode) Advanced-settings button → one card
CustomizeThemeColorsSection and ImportExportThemeSection were already
SectionView-wrapped and remain unchanged. UserWallpaperEditor (sister
function with similar layout, lines 28-220) is intentionally left
alone — user reported only the chat-theme entry point.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same pre-card-chrome pattern: SectionTextFooter("Delete chat messages
from your device.") was inside the ChatTTLOption SectionView lambda, so
after PR #6777 added card chrome it rendered inside the card. Move it
out so the caption sits below the card iOS-style.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same pre-card-chrome pattern as NetworkAndServers: SectionTextFooter
("Show: Database IDs and Transport isolation...") and SectionBottomSpacer
were inside SectionView lambdas, so after PR #6777 added card chrome they
rendered inside the white card area — the footer caption sat inside the
first card and the 48dp bottom spacer appeared as an empty row at the
end of the deprecated-options card (after SimpleX links).
Move both out of the SectionView lambda so the footer reads as a caption
below the first card and the bottom spacer adds safe-area room after the
deprecated-options card (not inside it).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before PR #6777 SectionView had no card chrome, so SectionTextFooter and
SectionDividerSpaced placed inside its content lambda rendered as plain
inline content. After the card chrome was added, the same code rendered
the footer caption and the spacer INSIDE the white card area, producing
an unwanted gap (and visible auto-divider tail) under Advanced network
settings.
Move both out of the SectionView lambda so the footer reads as a caption
below the card (iOS-style) and the spacer separates this card from the
next one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ProfileImageSection's Row had Modifier.padding(top = 10.dp), giving 10dp
above the avatar and 0dp below — visibly asymmetric inside the card.
Changed to vertical = 10.dp so top and bottom padding match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SectionDivider() composable had 0 callsites and used Material Divider
with horizontal inset (unused legacy). Repurposed to draw a 2dp
canvas-color Box matching the auto-divider style used by
SectionItemView, gated by LocalInSectionCard so it no-ops outside a
section card.
Use it in Appearance themes card between WallpaperPresetSelector
(custom composable, not a SectionItemView, so no auto-divider) and the
following content (Remove image / Color mode / Dark mode), providing
the visual separator the user expects between the theme grid and the
rows below it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before section facelift the spacer separated the Apply-to row from the
wallpaper preview block visually. With the new 2dp item divider drawing
under the Apply-to row that separation is already provided, and the spacer
leaves an awkward white gap between the divider and the preview.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CARD_PADDING (16dp) still drives outer card margin from screen edge.
Item content inside the card now uses CARD_ITEM_PADDING = CARD_PADDING - 1.dp,
giving the row text a slightly tighter horizontal inset that reads
better at the current card width.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously sectionItemDivider() was inside the modifier val before
clickable, so the hover background drew over it inconsistently — on
hover the row's content area got a tinted overlay while the 2dp
divider area stayed at canvas color, creating visible contrast that
read as a "dark line below hovered row".
Moving the modifier to the end of the chain (after clickable+padding)
makes drawBehind paint after the hover indication, so the divider
color is consistently #F2F2F2-ish regardless of hover state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- LIGHT canvas (themedBackground) now paints the off-white formula
(bg.mixWith(onBackground, 0.95f)) so white cards read as raised.
DARK/BLACK keep palette bg (cards already raised via founder's
formula in Section.kt). SIMPLEX keeps its gradient.
- Section cards in LIGHT switch from formula to pure white via
Color.White. DARK/BLACK keep the formula, unchanged.
- Section card horizontal padding equalized to 16dp on outer + inner
for clean canvas-edge alignment. extraPadding (icon-indented rows)
keeps DEFAULT_PADDING * 1.7f.
- 2dp dividers between rows inside section cards, color matches the
per-theme canvas (SIMPLEX uses gradient bottom stop). Implemented via
Modifier.drawBehind on each SectionItemView, gated by a private
LocalInSectionCard CompositionLocal set true only by SectionView's
inner Column — standalone SectionItemView usage (alerts, pickers)
stays unaffected. Single canvas helper canvasColorForCurrentTheme()
in Theme.kt is the source of truth for both canvas paint and divider
color.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to #6958 (Android/Desktop). Channel-link "Share via chat"
picker showed Saved Messages, which produced `Failed reading: empty`
on tap because `*<id>` has no branch in `sendRefP`. Add `includeLocal`
flag to `filterChatsToForwardTo` (default true) and to
`ChatItemForwardingView`; `shareChannelPicker` passes false.
Bug #2 from #6958 (button on plain groups) is not present on iOS —
`GroupLinkView.swift:110` already gates by `publicGroup != nil`.
* simplex-chat-python: split Client from Bot, add request/response API
Client is now the base class for SimpleX participants that talk TO
services (monitors, probes, automated participants). Bot extends Client
with server features (address, auto-accept, welcome, commands).
New methods on Client (inherited by Bot):
connect_to(link) idempotent contact handshake
send_and_wait(id, text) send a message and await the reply
events() async iterator over chat events
@on_message(contact_id=N) filter by sender in decorators
BotProfile renamed to Profile (alias kept). New ContactAlreadyExistsError
subclass for cleaner error handling.
* simplex-chat-python: narrow event payload type per @on_event tag
@client.on_event("contactConnected") now types the handler's event
parameter as CEvt.ContactConnected instead of the unnarrowed
CEvt.ChatEvent union — mirroring how @on_message narrows by
content_type.
The 50 overloads are generated by the Haskell codegen into _events.py
(as a Protocol class), so new events stay in sync automatically.
Client.on_event is exposed as a property typed as that Protocol; the
runtime implementation is unchanged.
* ui: hide saved messages from share channel picker; hide share via chat on plain groups
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* plans: justify share channel link picker filter and group-link button gate
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "core: forward compatible support for owners/admins/moderations deleting channel and public group messages without limitations (#6962)"
This reverts commit 08108ebabb.
* core, ui: allow indefinite deletion from history for public channel/group owners/moderators
* style
Co-authored-by: Evgeny <evgeny@poberezkin.com>
* refactor
* show error on deletion
* better alerts
* test
* plan
* simplify test
* bot api docs
* refactor
* test that removed from history is not delivered to the new subscribers
* fix, refactor
* fix
* rename
* rename predicate in UI
* rename
* do not forward channel deletions from history
* remove redundant check
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* plans: design and implementation plan for desktop tray icon
Adds a system tray icon to the SimpleX desktop app with first-close
dialog and an Appearance toggle. Covered: minimize-to-tray on Linux,
Windows, and macOS via ComposeNativeTray; tri-state CloseBehavior
preference; unread-dot icon swap; show/quit menu.
The design plan covers the user-facing behavior, library choice
rationale (AWT/Compose Tray is broken on stock GNOME per JDK-8322750;
ComposeNativeTray uses platform-native APIs and is maintained), and
out-of-scope items. The implementation plan splits the work into 8
incremental commits, each leaving the build green.
* desktop: add CloseBehavior preference
* plans: switch tray to built-in Compose Tray with GNOME probe
* desktop: branch close handler on CloseBehavior preference
* desktop: first-close dialog for tray choice
* desktop: tray icon assets and menu strings
* desktop: system tray icon with show/quit menu
* desktop: unread indicator on tray icon
* desktop: Appearance toggle for minimize-to-tray
* desktop: tray feature fixes from audit
- Scope closedByError per application iteration; set before dispatchEvent.
- Short-circuit Ask to Quit when tray is unavailable.
- Sum users.unreadCount (pre-aggregated) instead of iterating chats.
- Replace dialog's captured lambdas with a top-level flag and
ApplicationScope extension; wrap in SimpleXTheme.
- Probe SystemTray with real add/remove off the EDT.
- Drop duplicate ic_simplex_tray.svg; nudge unread dot to cy=34 to
stop the r=6 circle clipping at the viewBox bottom.
- Use mkSafeEnumPreference, PreferenceToggle, SectionTextFooter.
* plans: sync desktop tray plans with implementation
* desktop: render close-behavior popup via AlertManager
Replaces the bespoke DialogWindow with AlertManager.shared.showAlertDialogButtonsColumn —
same in-app surface as e.g. the link-previews opt-in alert. Drops isAskingCloseBehavior,
the CloseBehaviorDialog Composable, the resetAskCloseBehavior helper, and the per-iteration
reset call: the alert's lifecycle is now bounded by AlertManager's single slot, so a crash
mid-dialog gets cleanly overwritten by the crash report.
* desktop: reset closeBehavior with 'Reset all hints'
Generalises AppPreferences.hintPreferences to a heterogeneous List<HintPref>
so non-Boolean prefs can participate. Adds closeBehavior to the list, so
'Reset all hints' brings the first-close dialog back.
* desktop: skip muted profiles in tray unread sum
Active profile still counts so the user can see their own unread; non-active
muted profiles contribute zero.
* desktop: dark-theme tray icons
Adds ic_simplex_tray_light and ic_simplex_tray_dot_light — copies of the
existing tray SVGs with the navy back-X swapped to white. Picks the variant
via isInDarkTheme() so the icon stays visible against dark tray backgrounds.
Detect the substring /team anywhere in Grok's AI reply (per-message and the
initial post-join reply) and run the same escalation as a customer /team:
invite the team members and switch the conversation to TEAM-PENDING. The reply
itself is still posted to the chat.
* android, desktop: constrain image height in layout
Clamp the image preview Box's aspect ratio so it never goes
below 1/2.33 (matching the height cap already enforced by
PriorityLayout). Prevents tall image previews from overflowing
into the caption area below.
Analogous to iOS #6732.
* plans: justify image text overlap fix
Chat errors emitted via the Haskell `eToView` path (e.g. agent errors
on stale connections after a peer deletes a chat) were caught by the
broad `except Exception` arm in the bot receive loop, producing an
ERROR log with a full traceback for routine soft errors. Match the
desktop client policy (SimpleXAPI.kt:3332-3340): catch ChatAPIError
separately, escalate CRITICAL agent errors to ERROR, log the rest at
DEBUG.
* docs: simplex-chat-python design and implementation plan
* bots: Python wire types codegen
* simplex-chat-python: package scaffold
* simplex-chat-python: native libsimplex loader
* simplex-chat-python: async FFI wrappers
* simplex-chat-python: ChatApi with 49 api methods
* simplex-chat-python: Bot class with decorators and dispatch
* simplex-chat-python: install CLI, example bot, README
* simplex-chat-python: audit fixes
* bots: regenerate API docs and types
Catches up the markdown, TypeScript and Python codegen outputs with two
upstream schema changes:
- APIConnectPlan.connectionLink became optional (from sh/python-lib audit
fixes); cmdString and EBNF syntax now reflect optional parameter.
- APIAddGroupRelays command and CRGroupRelaysAdded/CRGroupRelaysAddFailed
responses added in #6917 (relay management). The TS and markdown outputs
were regenerated when #6917 landed but the Python types module only got
the new entries with this regeneration.
* core: refresh SQLite query plans after relay_inactive_at migration
The M20260507_relay_inactive_at migration (#6917 / #6952) shifted the
query plans that 'Save query plans' verifies. Regenerated via the test
that owns those snapshots; no behavioral change.
* bots: keep APIConnectPlan connectionLink as required parameter
The prior audit-fixes commit changed the syntax expression to `Optional ...`
because the Haskell field is `connectionLink :: Maybe AConnectionLink`.
That misrepresents the API contract: the `Maybe` is purely an internal
signal for link-parsing failure (the handler returns `CEInvalidConnReq`
on `Nothing`), not API-level optionality. Callers MUST always pass a
connection link.
Revert the syntax expression to `Param "connectionLink"` and add a
comment so the intent is preserved next time someone audits.
Regenerates COMMANDS.md, commands.ts and _commands.py to match.
* desktop: pick a free port for the call server if 50395 is in use
startServer() bound a hard-coded port (50395); when it was already in use,
NanoWSD threw "BindException: Address already in use: bind" and the call
failed. It now falls back to an OS-assigned free port, and WebRTCController
opens the browser at the actually-bound port (server.listeningPort) -- still
50395 in the normal case, so browser camera/mic permission stays put.
* plans: justify call server port-bind fix
* core: keep whitelisted query parameters when removing link tracking
In safe mode, "remove link tracking" stripped any query parameter whose
name started with a known tracking prefix in qsSafeBlacklist, ignoring
qsWhitelist. So "list" (e.g. YouTube playlist links) was dropped because
"li" (LinkedIn) is a prefix of it, and github's "ref" was dropped too.
Make the safe-mode filter consult the whitelist, like the other branches.
* docs: plan for keeping whitelisted query parameters when removing link tracking
Design doc for the safe-mode sanitizeUri change (PR #6965): why "?list=" was
stripped from YouTube links, the root cause (safe mode ignoring qsWhitelist),
the fix, what it does/doesn't change, and alternatives considered.
ciBotCommand's regex was unanchored, so a customer message like
"follow/read blog posts?" parsed as a /read command and the Grok
handler silently dropped the message. Anchor the regex with ^ so a
command requires `/` at the start of the (trimmed) message.
In the support bot, filter customer command parsing by the registered
keyword set: any unknown `/word` from a customer (e.g. /help) is now
routed as plain text instead of being silently dropped by Grok. This
also makes /grok when Grok is disabled behave consistently as text,
removing the previous ad-hoc workaround.
* plan: delete channel messages
* core: allow deletion of own messages in public groups in channels to moderators-owners
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* support bot: take Grok initial context as messages array
Generalizes GrokApiClient to take a list of seed messages instead of a
single system prompt. Behavior is unchanged.
* support bot: accept YAML transcript in --context-file
Plain text → single system message (existing behavior).
`.yaml`/`.yml` → parsed as harness transcript; only system and
assistant turns are included.
* desktop: keep text selection on the originally selected message when a new message arrives or is sent
Selection stored positional indices into the merged-items list. When a new
message was appended, the reversed list shifted every index by one — but the
stored start/end indices did not — so the highlight slid onto neighboring
messages.
Anchor the selection to ChatItem IDs instead of list positions. Offsets are
already content-relative (character cursors in rendered text), so they stay
valid across list mutations. Positional indices are derived on read via
derivedStateOf, which keeps per-item reads O(1) amortized. If either
anchored item is removed from the list, a SideEffect synchronously clears
the selection so the copy button does not flash at a stale location.
* desktop: minimize selection fix — anchor ids in SelectionRange
Replaces the previous derivedStateOf-based approach with a surgical
diff: SelectionRange carries startItemId/endItemId alongside the
existing positional indices, and a SideEffect calls resyncIndices()
to translate ids back to current positions when the items list mutates.
All existing call sites of range.startIndex / range.endIndex remain
unchanged. Net diff vs master is +19/-2.
* plans: justify desktop text selection id-anchored fix
* android, desktop: open correct image in fullscreen viewer
Fullscreen image viewer occasionally opened a different image than
the one clicked. Root cause: when the LaunchedEffect probe at
ImageFullScreenView.kt:48-55 calls getMedia(initialIndex - 1) to check
whether a previous media item exists, getMedia returns null for both
"no item" and "item found but failed to load" (e.g. undecodable bytes,
missing file, crypto error). The probe treated null as "no previous
item" and called scrollToStart(), which rewrote initialChatId to the
chat's oldest media item - making the viewer display that oldest item
instead of the clicked one.
Fix: scrollToStart() no longer rewrites initialChatId. The pager is
still repositioned to page 0; getMedia(0) resolves against the
already-set initialChatId (the clicked item) and renders it correctly.
* android, desktop: regression test for fullscreen viewer anchor preservation
Drives the public providerForGallery interface: moves the anchor away from
cItemId via currentPageChanged, calls scrollToStart, then reads the anchor
back through onDismiss's scrollTo callback. The pre-fix code rewrote
initialChatId to the chat's oldest showable, which would surface as
scrollTo(2); the fix preserves the anchor and produces scrollTo(1).
* plan: design doc for fullscreen viewer wrong-image fix
Documents the pager state model, the root cause of the wrong-image bug,
why the one-line deletion in scrollToStart fixes it for both call sites,
and why the wider getMedia null-overload refactor is deliberately out of
scope for this fix.