Commit Graph
14 Commits
Author SHA1 Message Date
MarekWoandClaude Opus 5 2a78d3fed3 feat(notifications): name the sender, quote the message, land on the right screen
A notification said "New: 1 channel" and nothing more, because
sendBrowserNotification() was driven by badge-counter deltas and never saw a
message. The socket payload has carried the sender, the text and the channel
all along.

Channel messages now notify as "<channel>" / "<sender>: <text>", DMs as
"<sender>" / "<text>", each conversation on its own tag so one cannot bury
another, and repeats on the same tag collapse with a count in the title.
Notifications carry a deep link in options.data.url; a tap resolves it in place
when the page is already up, and the wrapper hands it to the Intent so a cold
start lands there too. Channel links are new (/?channel=<idx>); DM links reuse
the conversation parameter /dm already accepted.

Two faults that made notifications look unreliable while the app was awake:

- A message on the channel last left on screen never notified at all. A channel
  now counts as open only while the document is visible.
- The first checkAndNotify() compared against zeroed baselines, so a page
  loading in the background announced its whole backlog as new ("New: 492
  channels"). The first pass now only takes the measurement.

The count-only notification stays as the fallback for messages the socket never
delivered; the socket path advances its baselines so the same message is not
announced twice.

dm.js's checkDmNotifications() is removed rather than updated: dm.html never
loads app.js, so its areNotificationsEnabled() guard returned on the first line
in every context it ran in, and it would now be a second, vaguer alert for a
message app.js already announces in full.

Verified against the local container with a Playwright harness (14 checks):
content, collapsing, mute and own-message suppression, taps on a running page,
cold starts on both link kinds, and no stray count-only alerts.

Background delivery on Android is a separate, architectural problem and is
untouched here - the wrapper still has no foreground service, so a frozen
process still stops notifications altogether.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 16:04:44 +02:00
MarekWoandClaude Opus 5 61a268f2cb docs: publish the 2.0 build of the Android app
The wrapper sources moved to targetSdk 36 and versionName 2.0, but the
`.apk` shipped from this repository was still the 1.3 build, so the
download table described a file that no longer matched the code beside it.

Replaces the published binary and the figures that identify it: 5.2 MB,
version 2.0, and a new SHA-256. Verified before committing - apksigner
reports the APK as signed by the same certificate as every release before
it (SHA-256 425857b3...9bd230), so it installs as an update rather than
forcing a reinstall, and aapt2 confirms versionCode 5 against targetSdk 36.

Also adds the release notes for both Android changes in this cycle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 08:25:31 +02:00
MarekWoandClaude Opus 5 a36fe9ebb7 fix(android): follow the mc-webui theme without a restart
The system bar strips took their colour from the page only when a page
finished loading or the app came back to the foreground. mc-webui switches
its theme by flipping data-theme on the document element - no reload, no
trip through onResume - so switching to the dark theme left a white strip
under the status bar until the app was restarted.

A MutationObserver on that attribute closes the gap. It lives entirely on
this side, so mc-webui still needs no changes, and it reports twice: once
immediately and once after 300 ms, because the theme change animates and
an immediate read catches a colour mid-transition.

The callback arrives through a second, separate JavaScript bridge rather
than an extra method on the notification one. That bridge is deliberately
minimal, and a one-way "here is a colour" channel has no business sharing
a surface with notification permissions.

Verified on a real device: the strips now follow the theme both ways
without leaving the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 08:15:40 +02:00
MarekWoandClaude Opus 5 7499930cad chore(android): target API 36 and handle enforced edge-to-edge
Google Play rejects anything below targetSdk 36 submitted after
2026-08-31, so the wrapper had to move off 34 before it can be published
at all.

The toolchain came along for the ride: AGP 8.2.0 -> 8.13.2, Gradle
8.7 -> 8.14.3, Kotlin 1.9.22 -> 2.2.21, and the AndroidX libraries to the
newest releases that still build against Android 16. The very newest ones
(core-ktx 1.19) want AGP 9.1 and compileSdk 37, which is a DSL migration
of its own and buys nothing here.

Targeting 36 means the system draws behind the status and navigation bars
with no way to opt out, so the padding that used to arrive for free is now
applied by hand - system bars, display cutout and the keyboard, all on the
root. Padding the root leaves strips of its background behind the bars,
and a white strip above a dark mc-webui theme reads as a bug, so the page
is asked for its own background colour after every load and the strips are
painted to match, with the bar icons flipped to stay readable.

Verified from the CLI: assembleDebug, assembleRelease and bundleRelease
all pass, and aapt2 confirms targetSdk 36 with the permission set
unchanged. Still needs a look on a real Android 15/16 device.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:42:17 +02:00
MarekWoandClaude Opus 5 daed07a335 feat(android): publish wrapper 1.3 with proxy sign-in
Verified live against the test server with an Nginx Proxy Manager access
list: the wrapper prompts, stores the credentials, and reconnects without
asking again. The proxy log shows 24 websocket upgrades (HTTP 101) from
the WebView user agent, so the credentials reach socket.io and the XHRs
too - which was the open question, since onReceivedHttpAuthRequest fires
for the main frame only.

Also documents what that log made obvious: requests refused in the
background raise no prompt, so switching authentication on under a
running app leaves it stuck until restarted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187QpThw1Y8n7aYDjHtMHax
2026-08-03 08:44:43 +02:00
MarekWoandClaude Opus 5 51f6674d6c feat(android): sign in to servers that ask for a password
A reverse proxy can be told to require HTTP Basic authentication - an
Access List in Nginx Proxy Manager, say - which is what makes publishing
an instance to the internet reasonable. The wrapper had no answer for
that: WebViewClient cancels the request by default, so the app showed
nginx's bare 401 page and stopped there.

Handle onReceivedHttpAuthRequest instead, asking for the username and
password once and keeping them per host, so later launches go straight
in. WebView only calls it after a 401 carrying WWW-Authenticate, so an
instance that asks for no password never reaches any of this and behaves
exactly as before.

Credentials are stored before the server has accepted them; a wrong one
simply comes back on the next 401 with the username filled in, since
useHttpAuthUsernamePassword() tells a first ask from a retry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187QpThw1Y8n7aYDjHtMHax
2026-08-03 08:17:32 +02:00
MarekWoandClaude Opus 5 a3fde55704 feat(android): publish wrapper 1.2 with the text-size fix
Signed release build of the textZoom change from 09dcf26, plus the
download table it invalidates.

Verified on the artifact rather than on trust: classes.dex references
setTextZoom, the manifest reports versionName 1.2, and the signing
certificate is SHA-256 42:58:57:B3:...:D2:30 -- the same key as 1.1, so
it installs over the previous version instead of forcing a reinstall.
User confirmed on-device that nothing wraps any more.

Size is unchanged at 4.6 MB (4.61 MiB), so only the version and hash
rows move. android/README.md documents the release steps but pins no
version of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:39:03 +02:00
MarekWoandClaude Opus 5 09dcf26f87 fix(android): match the browser's text size, and let the status bar wrap
Android's WebView scales text by the system font-size setting; Chrome
ignores it and uses its own accessibility preference. With the system
font one step above default, the wrapper rendered every string ~20%
larger than the website at an identical layout width -- the brand
truncated to "mc-web...", message bodies wrapped early, and the footer
broke apart. Pin textZoom to 100 so the wrapper matches the browser.

The footer was fragile independently of that. Status, region badge and
the "Updated:" time sat in a justify-content-between row with no wrap,
so once they exceeded the width each item broke internally instead of
the row reflowing -- a dot with "Polaczono" stranded below it, a time
split from its own label. Longer translated labels ("Aktualizacja:" vs
"Updated:") hit this well before English did. Adding flex-wrap plus
text-nowrap on the items makes them reflow as whole units; .badge
already carries white-space: nowrap, so it needs nothing.

Verified in Chrome against the local container, pl locale, region badge
forced visible, at 412/360/320 CSS px and with a 1.2x text-only override
that reproduces textZoom: before/after screenshots show the reported
breakage and a clean two-line reflow respectively, with no horizontal
overflow. At full width the change is a no-op.

Rides along in the already-pending, never-built wrapper 1.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:46:18 +02:00
MarekWoandClaude Opus 5 ebd2e95fe1 fix: refill the message gap left by a backgrounded app
The chat view only ever grew by socket push, so anything that arrived
while the connection was down was never drawn. Android tears the
connection down behind a locked screen, and the wrapper keeps the same
page alive for days, so the list stopped at the last message that got
through until the app was force-stopped. A browser tab hid the same bug
by reloading the page on resume.

Every way back from a gap now re-reads the list from the server: the
socket reconnecting, the page becoming visible after more than a glance
away, a heartbeat noticing its own tick arrived far too late (the page
was frozen), a new Refresh item in the menu, and window.__mcAppResumed,
which the wrapper calls from onResume since a WebView is not guaranteed
to report the page as hidden at all. Direct messages get the same
treatment.

Verified in Chrome against the local container: all five triggers fire a
resync, with no page errors and the list intact afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:19:52 +02:00
MarekWoandClaude Opus 5 80eba5bbc1 feat(android): publish wrapper 1.1 with notification support
The signed release build of the notification work from 954e99b, signed
with the same key as 1.0 (certificate SHA-256 425857b3...d230), so it
installs straight over the previous version and users keep their saved
server address.

Verified against the packaged APK: versionCode 2 / versionName 1.1,
POST_NOTIFICATIONS present, and assets/notification_shim.js included.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:50:52 +02:00
MarekWoandClaude Opus 5 954e99b42c feat(android): give the wrapper working notifications
Android's WebView ships no Web Notifications API, so window.Notification
was undefined, mc-webui detected that and greyed its toggle out as
"Unavailable". The app also declared no POST_NOTIFICATIONS and created no
channel, so it never even appeared in Android's notification settings.

A shim injected at document start puts window.Notification back and
forwards it to a @JavascriptInterface bridge that posts through Android's
NotificationManager. mc-webui itself is untouched - the page keeps using
the standard API.

- onPageStarted is early enough: mc-webui reads the permission on
  DOMContentLoaded, a whole parse and script pass later
- web permission states map onto Android's, with
  shouldShowRequestPermissionRationale separating "ask again" from
  "blocked for good" after a refusal
- tags replace notifications the way the web API expects; a tap returns to
  the running app (singleTop) and fires the page's onclick
- notifications only arrive while the process is alive, same as the PWA

versionCode 2 / versionName 1.1. Verified: debug and release both build
clean (lintVitalRelease included), shim and drawable land in the APK, and
the shim's contract is covered by a Node harness against a fake bridge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 08:48:01 +02:00
MarekWoandClaude Opus 5 28d74476e7 feat(android): publish the signed release build of the wrapper
Replaces the debug APK with a release build signed with the project key
(CN=Marek Wojtaszek, valid to 2081, v1+v2 schemes, not debuggable), so
every later version installs over this one instead of forcing a reinstall.

Docs catch up with what the app can now do: QR scanning works on https
instances, downloads land in the phone's Downloads folder, and the address
form is a non-destructive screen you can reach deliberately. New SHA-256,
size, permission list and the signing fingerprint to verify against.

Also silences the Kotlin warning for the deprecated shouldOverrideUrlLoading
overload kept for Android 5.x - source-only, the APK is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 21:17:57 +02:00
MarekWoandClaude Opus 5 0cbab03ab8 feat(android): add wrapper sources and fix the address, camera and downloads
The wrapper is what users sideload, so its source belongs next to the APK -
they can read what they install, or build it themselves.

Behaviour fixes on the way in (APK rebuild pending):

- The saved server address survives. Back on the first page and connection
  errors used to delete it, so a stray tap or a moment without signal meant
  typing the address again; the form now opens pre-filled and only a save
  replaces what is stored. Back at the top level asks: exit, change server,
  or cancel
- QR scanning works: the page's camera request is mirrored to an Android
  permission request (CAMERA, on an https instance - getUserMedia needs a
  secure context, as in any browser)
- Downloads work: a DownloadListener hands database backups and other files
  to DownloadManager, which puts them in the phone's Downloads folder
- Links to other hosts and non-http schemes open in the system browser, so
  a URL in a message no longer navigates the app away from the instance
- Rotating the screen no longer reloads the page

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 21:05:42 +02:00
MarekWoandClaude Opus 5 f71cac8b2e feat(android): ship the Android companion app with an install guide
A thin WebView wrapper that opens a user's own mc-webui instance full
screen, without the browser address bar. It asks once for the server
address and remembers it; all logic stays on the server.

- android/mc-webui-wrapper.apk (1.0, it.wojtaszek.mc.wrapper, minSdk 21)
- docs/android-app.md: download + checksum, "unknown sources" permission,
  the Play Protect notice, first connection, and the limitations that come
  with a WebView (no notifications, no QR camera, no file downloads)
- README, user guide and whatsnew entries, incl. an app-vs-PWA comparison
- *.apk marked binary so the text=auto rule can never touch it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 20:55:04 +02:00