Files
status-app/src/app/modules/main
Alex Jbanca 4483d7eb2d fix(ios): pause/resume services with the app lifecycle and re-point media URLs on rebind (#21687)
* fix(media): re-point cached media URLs when the media server restarts (#47)

iOS suspends the app and kills status-go's local media server; on resume
it rebinds on a NEW ephemeral port and emits mediaserver.started, which
the Nim side never handled — every cached https://localhost:<oldport>/
image URL (chat images, avatars, stickers, link previews, community
icons) went permanently stale.

Key decisions:
- New MediaServerStartedSignal decoded in signals_manager (the enum
  member already existed, so the cheap-triage scan needs no change);
  null-event envelopes decode to port 0 and every rewrite helper treats
  port <= 0 as a no-op.
- One shared helper, withMediaServerPort (app_service/common/
  media_server_url.nim): rewrites the port ONLY for http(s)://
  localhost|127.0.0.1|0.0.0.0:<port> URLs, splicing around the authority
  so path/query stay byte-identical; everything else (remote URLs, data
  URIs, qrc/file paths, port-less URLs) passes through untouched, so
  callers apply it blindly. Desktop stays a no-op: the signal never
  fires there, and an unchanged port rewrites nothing.
- Subscribers rewrite in place + notify: message models (chat + pinned)
  re-emit image-carrying roles via dataChanged; link-preview thumbnails
  are QObjects and emit urlChanged directly; section model refreshes
  community image/banner/icon; contacts service rewrites its
  ContactDetails cache and re-emits SIGNAL_CONTACT_UPDATED per changed
  contact, plus the user-profile singleton's own avatar (its setters
  already no-op on equal values).
- Dropped the contacts service's imageServerUrl field: it was write-only
  dead code (fetched once, never read).

Files: src/app/core/signals/{signals_manager,types}.nim,
src/app/core/signals/remote_signals/mediaserver.nim (new),
src/app_service/common/media_server_url.nim (new),
src/app_service/service/contacts/service.nim,
src/app_service/service/message/dto/{link_preview,link_preview_thumbnail}.nim,
src/app/modules/shared_models/{message_item,message_model,link_preview_model,section_model}.nim,
chat_section chat_content + messages + main module/controller/io_interface
wiring, test/nim/{media_server_url_test (new),message_model_test,
signals_manager_test}.nim

Verified: media_server_url_test 9/9 OK, signals_manager_test 6/6 OK
(incl. 2 new mediaserver.started tests) via make nim-test-run USE_SYSTEM_NIM=1.
message_model_test does NOT link in this arm64 container — pre-existing
nimqml/LTO link failure, reproduced identically on clean HEAD with a
fresh nimcache; the 3 new model tests compile but need CI/another host
to run. Device repro (background 10+ min, resume, images reload) remains
for the human pass.

* fix(build): normalize two 'import Nimqml' casings to the module's real name

discord_message_item and message_transaction_parameters_item imported
Nimqml (capital N) while the vendored module file is nimqml.nim. On a
case-insensitive checkout (Docker-on-Mac bind mount) Nim treats the two
spellings as distinct modules whose nimcache artifacts collide on one
file, so any test pulling in these items (e.g. message_model_test) fails
to link with undefined nimqml symbols. Found while verifying #47.

* refactor(media): deduplicate the media-URL refresh template and drop dead code

Review follow-up to ca5e5c92c, no behavior change:
- Extract the thrice-duplicated compare-rewrite-flag template into
  refreshMediaServerUrl in media_server_url.nim; message_item,
  section_model and contacts/service now share one definition.
- Drop the why-comment copy-pasted verbatim into three controllers;
  the explanation lives on MediaServerStartedSignal and the helper module.
- Remove backend getImageServerURL, dead since its only caller
  (setImageServerUrl) was deleted on this branch.
- Whitespace: trailing newline in signals/types.nim, blank line before
  the appended suite in message_model_test.nim.

Verified: media_server_url_test, message_model_test and
signals_manager_test all pass (nim-test-run recipe); app wiring
compile-checked via app/modules/main/module + contacts service +
section_model with --compileOnly.

* fix(ios): drive PauseServices/ResumeServices from the app lifecycle (#51)

On iOS status-go runs in-process and nothing drove the pausable-services
lifecycle: services never paused on backgrounding (battery cost) and never
resumed on foregrounding, so the media server's listening socket iOS kills
during suspension stayed dead and every cached localhost media URL failed
until app restart (device-confirmed: post-resume image loads get Connection
refused while in-process RPC still works). Resuming now re-runs the full
recovery chain: ResumeServices -> ServiceRegistry -> mediaserver
ToForeground() rebind -> mediaserver.started -> #47/#49's URL refresh.

Key decisions:
- Lifecycle source is StatusQ's UrlSchemeEvent applicationStateChanged
  watcher (already the iOS foreground seam): new appBackgrounded signal
  emitted ONLY on Qt::ApplicationSuspended — Inactive dips from share
  sheets/system alerts never pause; appForegrounded fires on every return
  to Active, so ServicesPauseBridge latches (`paused`) and resumes only
  when it actually paused. Connections are synchronous (AutoConnection,
  same thread): iOS freezes the process right after the state change, a
  queued pause slot might never run.
- Bridge wired only under `when defined(ios)`: Android's service process
  already drives pause/resume from binder visibility (UI process must not
  double-drive), desktop is never suspended.
- Same contract as StatusGoService.java: fetch the service list from
  PausableServices() at each transition (late-registered services picked
  up; empty list — node not running, e.g. login screen — drives nothing),
  then PauseServices/ResumeServices with the JSON name array. The three
  libstatus C exports are bound in new backend/pausable_services.nim
  (vendor/nim-status-go doesn't cover them); errors logged, not fatal.
- Backend calls injected into the bridge as a seam
  (PausableServicesCalls), so the Nim test drives the real StatusQ
  signal emitters against recorders.

Files: src/app/core/services_pause_bridge.nim (new),
src/backend/pausable_services.nim (new), src/nim_status_client.nim,
src/statusq_bridge.nim, ui/StatusQ/{include/StatusQ/urlschemeevent.h,
src/{urlschemeevent,externc}.cpp}, Makefile (test target),
test/nim/services_pause_bridge_test.nim (new)

Verified: services_pause_bridge_test 15/15 OK; full tests-nim-linux 464
OK / 0 failed (chat_section_model_test fails to compile identically on
the clean base — pre-existing, unrelated); nim_status_client Linux path
compile-checked (--compileOnly, exit 0); StatusQ recompiles clean;
libstatus.so exports PausableServices/PauseServices/ResumeServices
(linked by the test). Device criteria (iPhone: photo -> background 10+
min -> resume -> images recover; Android S21: exactly one service-driven
pause/resume, no duplicates) remain for the human pass — iOS paths can't
run in this container.

* fix(tests): reset the fetch counter per case in services_pause_bridge

unittest setup vars are module-scope globals; the int's constant
initializer runs once, so fetches leaked across cases and the two
absolute-count assertions saw the running total.

* fix: Potential fix for pull request finding

* fix(statusq): handle the full application-state enum in watchApplicationState

Inactive is an expected transient dip and stays a no-op; anything else
unhandled now logs a qWarning.

* fix(tests): empty pausable set no longer latches paused, so foregrounding skips the fetch
2026-08-05 14:18:26 +03:00
..