Fixes#22037
The StatusChatInput refactor dropped the ASCII emoticon conversion that
used to run off the old text area's onTextChanged handler, so typing
":)" stayed ":)" instead of becoming 🙂.
Restore it at the layer that owns caret and text structure. ChatTextArea
gains an opt-in asciiEmojiConversion property and a convertAsciiEmoji()
function: it matches the word ending at the caret against the emoticon
table, longest alias first, and requires a whole word at text/line start
or after whitespace. Conversion is triggered from Keys.onPressed on
space and enter, before the key's own insertion, and from the existing
InputMethodEventFilter for on-screen keyboard commits.
Emoji gains a lazily-built alias_ascii lookup, replacing the old linear
scan over the whole emoji list on every keystroke, and the highlighter
gains replaceTextWithEmojis() so a conversion is a single undo step.
StatusChatInput enables the property and force-converts in
tryFinalizeMessage(), so "hello :)" sent without a trailing space is
converted too.
Unlike the previous implementation, emoticons inside code spans and code
blocks are now left verbatim.
* perf(statusq): event-loop-driven QML incubation controller
The QQuickWindow default incubation controller collapses its time budget
when frames are slow, starving async Loaders exactly when load is
heaviest — on low-end devices this stretches section incubation into
multi-second skeletons. Drive incubation from a permanently armed timer
instead: 20ms slices per event-loop tick, a 12ms gentle phase during the
first 300ms of a burst so section-switch animations keep their frame
budget, and a 128ms idle backstop so a missed count callback can never
wedge incubation. Input stays responsive between ticks.
A/B on the community section page (STORYBOOK_INCUBATION_MS=20, gentle
phase off): idle machine 857 -> 441ms for a 210-channel load; CPU-
contended machine (the low-end condition) 5.4-8.1s with the default
controller vs a flat 400-490ms with this one.
* fixup! perf(statusq): event-loop-driven QML incubation controller
Without it the QML test engine runs async Loaders on Qt's render-loop
budget — the configuration the benchmark measured at tens of seconds per
section load, and the one nothing in the app ever uses. Tests were pinning
a configuration that does not ship, and paying for it with 120 s waits.
externc casts to QQmlEngine now: the test harness owns a plain one, not a
QQmlApplicationEngine. The HomePage grid lookup waits for its delegate
instead of assuming it exists on the same tick.
* fixup! perf(statusq): event-loop-driven QML incubation controller
* chore(statusq): env-gated incubation debug HUD
STATUS_INCUBATION_HUD=1 shows a top-right overlay with the live incubator
count (16ms refresh + 1s peak) and the controller pacing phase
(idle/gentle/boost, plus held gentle hints). Debug-only; drop before merge
if unwanted.
- move the `nativeWindowDpr` handling up to AppMain, initialize it with
a small delay when the appMain is ready; also track the current `screen`
in order not to restart when the user just moves the window between
screens
- on Android, add a dedicated DensityListener to emit the needed signal
for us; the Android QPlatformScreen/QPlatformWindow doesn't
(re)implement the needed `devicePixelRatio()` methods
- add a simple ConfirmationDialog (with bottom sheet) asking for restart
when the native/base DPR changes
- adjust SB page, update TS files
Fixes#21915
- using the standard `org.freedesktop.Notifications` DBUS backend/API;
see https://specifications.freedesktop.org/notification/latest-single/
for details
- allows tracking the OS Notifications, including being clicked/closed
by the user and navigating Status to the respective section/chat/channel
like other OSs
- fully async using DBUS pending call/watcher paradigm, no more fire and
forget using a cmd line wrapper (`notify-send`)
Iterates #2520
Iterates #21292
A Tab opened to display a downloaded file is no longer a browsing Tab.
`profileParams` gains an orthogonal `localPreview` flag; it selects a
profile of its own — always off the record, never named — with no
injected scripts, no web channel and no connector.
The local-URL policy splits along the same line: browsing profiles reach
no file:// at all (a local path in the address bar dead-ends), and only
the preview profile reaches the downloads and player-page directories.
The default profile, which backs views whose storage profile could not be
created, carries the browsing policy either way.
WebEngine views drop localContentCanAccessRemoteUrls everywhere and grant
localContentCanAccessFileUrls to previews alone, so the player page can
load the media beside it.
Stack-target: PR 21853
downloadUrl takes an opaque token and downloadRequested hands it back, on
both Backends and through every adapter. Nothing between the two reads it —
it exists so the caller of a host-side re-issue can recognise its own
Download by identity instead of by URL plus a timing window. Downloads the
host did not re-issue carry an empty token.
BrowserProfileUtils keeps the token beside the initiating view in its
per-page map, and that view may now be null: a re-issue no longer needs a
live Web View to start from.
Stack-target: PR 21849
Three facts about opening a downloaded media file were baked in as one global
answer. All three belong to the Backend, so they join
BrowserBackendCapabilities beside inPageMediaPlaybackSupported.
proprietaryCodecsSupported. M4A/AAC and MP4/H.264 were excluded everywhere
because our Qt WebEngine build ships without the licensed codecs — a fact about
that build, not about WebKit or the Android WebView, which decode them through
the OS. On iOS a page's inline player played an mp4 while the same file, once
downloaded, went to the share sheet.
mediaPlayerPageRequired. The generated player page exists only because WebEngine
turns a top-level navigation to local media into a fresh Download. Keeping it
everywhere is what broke iOS playback: the page sits in TempLocation, the media
in the downloads directory, and a plain local load grants the web content
process only the file it was handed — so the page came up and the audio in it
never loaded.
loadFileUrl(fileUrl, readAccessUrl) joins the Web View seam beside loadUrl,
implemented by both adapters and proxied by LazyWebViewAdapter — the
seam member easiest to forget. The grant is left empty, so it resolves to the
file's own directory: the narrowest one that works.
Nothing is deleted — a Backend answering "yes" still gets the page, so falling
back is one line. Verified on an iOS device: mp3, mp4 and webm all open in a Tab
with a native player.
Extend the cross-platform web view seam with the download vocabulary the
Downloads feature builds on (ADR 0006):
- AbstractWebView: DownloadState gains DownloadPaused (WebEngine-shaped
numbering, Paused = 5 matches MobileWebViewDownload), a host-side
downloadUrl() re-issue used for Retry, a linkLongPressed(link, image,
position) signal for mobile Backends, supportsPdfViewer, and a
'retained' flag for Web Views kept alive after their tab closes.
The downloadsStore / isDownloadView coupling is gone: adapters map
Backend downloads into the seam and the UI never branches on Backend.
- WebViewAdapter / MobileWebViewAdapter / LazyWebViewAdapter implement
the seam; the desktop path gains an injectable isBrowsableLocalUrlFn
local-URL guard exception, and LazyWebViewAdapter forwards Retry
through ensureLoaded().
- ProfileManager survives a WebEngine profile data-path collision.
- StatusQ C++: BrowserProfileUtils (QWebEnginePage::download re-issue,
profile plumbing) and SystemUtilsInternal additions, iOS share-sheet
support via NSItemProvider in ios_utils.mm, Android accept path in
StatusQtActivity, mobilewebview pin bump, and Common.mk tracks
CMakeLists.txt so the pin invalidates mobile builds.
- add a signal `closePopupsRequested()` to NIM, and add a helper
function that walks over the potentially closeable Popups, and tries to
close them
- small fix for the Linux OS Notification impl to match the actual
appName now
Fixes#21292
* fix(ios): paste without triggering the system paste permission prompt
On iOS any programmatic read of UIPasteboard shows the "Allow Paste?"
alert. Our Paste buttons read the clipboard directly, so they prompted
every time. Replace them with UIPasteControl, where the system performs
the read and hands over the contents, making the tap itself the consent.
- add NativePasteButtonItem, hosting a native UIPasteControl, and the
StatusPasteButton wrapper (native on iOS, StatusButton elsewhere)
- use it for every Paste button: sync code, WalletConnect URI,
watch-only address, private key, chat key and both send recipient
inputs
- disable AppMain's passive clipboard watcher on iOS: it read the
clipboard with no user action at all, which is what prompted on launch
- drop the now-dead iOS guards left over from #21365
A native view composites above the whole Qt scene, so the control is
clipped to its visible viewport and hidden whenever something is stacked
above it - a popup over the page, or another popup over the one holding
it. That state is recomputed per frame from afterAnimating and pushed to
UIKit only when it changes; it must not call polish(), which requests a
new frame and would keep the window rendering forever.
The paste completion captures the target view rather than a raw C++
pointer, so a control destroyed while the item provider loads - closing
the modal it lives in - cannot deliver into freed memory.
Desktop keeps its existing Paste button styling: the wrapper forwards
size, border, font weight and focus policy, and each site passes what it
used before. Desktop buttons are now disabled rather than always enabled
when the clipboard has no text.
Also track *.mm in STATUS_Q_FILES: the mobile build only watched .cpp
and .h, so no Objective-C++ change in StatusQ ever triggered a rebuild
and stale binaries were shipped to the simulator.
Text editors are unaffected: StatusTextArea is a Controls TextArea, so
long-press gives the OS edit menu, whose Paste the system authorises.
Closes#21438
* fix(StatusPasteButton): apply review feedback
- base the wrapper on Control and forward the font group, so a site can
set the label's family, weight or pixelSize. They are forwarded one by
one on purpose: StatusBaseButton binds each of them itself, and a
whole-group `font:` assignment is silently ignored - verified by
checking that a bigger pixelSize actually widens the button.
- keep the wrapper out of the tab chain. Control brings its own
focusPolicy, so the button's is `buttonFocusPolicy`; otherwise the
composite would add a tab stop in front of the button.
- derive the Qt -> UIKit scale from the window's devicePixelRatio over
the view's contentScaleFactor rather than parsing QT_SCALE_FACTOR. The
ratio is exactly the extra Qt scaling, whatever asked for it.
- mark foregroundColor/backgroundColor REQUIRED: UIKit picks each tint's
default on its own, so setting only one can leave the glyph unreadable
against the fill.
* fix(NativePasteButtonItem): make the zero-scale guard obvious
contentScaleFactor was already checked before the division, but far
enough above it to read like an unguarded divide. Bail out on the null
view and window first, then guard the divisor right where it is used.
The QML engine gets a network access factory that hands out managers with a
disk cache and per-host accounting, so every image, animation and XHR the QML
layer issues can be seen and capped.
HttpStats is a GUI-thread singleton recording per-host totals split by whether
the bytes came from the network or from the cache. Bytes are taken from
downloadProgress rather than Content-Length, which is absent on some responses
and wrong on chunked ones. It is constructed on the GUI thread before any
factory worker can first-touch it with the wrong QObject affinity, and it
reports a finished cache clear on its own cacheCleared() signal — separate from
changed(), which fires on every reply and so cannot be what a reader hangs an
expensive re-measurement on.
The cache takes an explicit budget instead of Qt's 50 MB default, which a
single collectibles page can evict on its own.
The factory also sends Accept: image/webp,image/png,image/jpeg on Cloudinary
delivery URLs, so the f_auto in the rewritten media URLs has something to
resolve against. The list is narrow on purpose: f_auto returns AVIF or JPEG XL
to a client that claims them and Qt decodes neither. It is a statement about
this client's decoders, which is why it lives here and not in QML — see
docs/adr/0006-qt-http-cache.md.
All of it lives in networkaccessfactory.{h,cpp} rather than in externc.cpp,
which exists to be a thin C-ABI shim for Nim: the shim now casts the engine
pointer and delegates. That also lets Storybook install the same factory, so
media traffic measured on a Storybook page is the traffic the application
produces — and it lets the Accept policy be tested, which as a static function
in a .cpp with no header it could not be.
TestHttpStats and TestNetworkAccessFactory cover the thread affinity, both
clear paths, and the Accept policy as a table.
* 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
- use a native JNI call to get the base (native) DPR of the underlying
window/screen, as the Android QPA doesn't implement/overrride the
`QPlatformScreen::devicePixelRatio()` and always returns `1.0f`
- reduce the min/max range to `[0.75,1.5]` of the baseline
Fixes#21543
- move the settings to the popup/model, simplifying and centralizing the
responsibilities when saving/loading the recent emojis
- wrap the `MessageReactionsRow` quick actions bar into a `Loader`,
delaying its construction for after the model and recent emojis have
been fully loaded; there was a race condition between the 2 sides
because the StatusEmojiModel lives inside a QML singleton, and hence
being constructed at startup before anything else
- adapt the Storybook page
Fixes#21490
- tied to the global AC notifications count
- use the `QGuiApplication::setBadgeNumber(int num)` everywhere to set
the actual value
- fixup and move the qApp static initialization to NIM, simplify the
global signal code path/propagation
- bump `vendor/nim-seaqt` to the Qt 6.8 branch
Fixes#20872
Replace the QML clear state machine and addressed signal with a QJSValue
callback and a Mojo-ordered sentinel cookie barrier in C++, so completion
is per-caller without pending-name counters or navigation guards.
Address clearSiteDataCompleted to the initiating adapter, run site and
profile clears through one state machine with navigation/origin guards,
and settle cookieRemoved only for host-matching names.
Qt 6 loadAllCookies no longer re-emits existing cookies, and overwrites
arrive as cookieRemoved only — track cookie names from cookieAdded and
delete by name+URL. After clear, navigate with GET so POST Set-Cookie
is not replayed.
Desktop clear site/browsing data now deletes host cookies (incl. HttpOnly) in
C++, then wipes current-origin DOM storage through injected site_utils.js.
Desktop clearBrowsingData was stuck because QML WebEngineProfile has no
cookie API and the old clearCache path threw before the fallback timer.
Add BrowserProfileUtils (QtWebEngine-only) to clear HTTP cache + cookies,
and align the QML API with the UI (clearBrowsingData / clearSiteData).