Skip to content

Add native notification centre and reliable Log navigation - #79

Open
ricardovfreixo wants to merge 9 commits into
NativePHP:mainfrom
ricardovfreixo:feat/notification-centre
Open

ricardovfreixo wants to merge 9 commits into
NativePHP:mainfrom
ricardovfreixo:feat/notification-centre

Conversation

@ricardovfreixo

@ricardovfreixo ricardovfreixo commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #71.

Native notification centre

  • Account-scoped multi-select filters and More sheet, including legacy filter migration and all/none states.
  • Per-type native rows, optional quote/image/rank previews, accessible selected/new states, and correct actor/commit/quote targets.
  • Thirty-second foreground polling stages arrivals and updated groups behind a pinned button without moving existing rows.
  • Preserves pagination, expanded groups/cursors, read state and follow-back success/rollback, including staged data.
  • Removes the completed notification-centre roadmap entry.

Log navigation and refresh feedback

  • Already on a feed: one Android Log tap scrolls to exact top and then refreshes Following/Latest/Trending, with accessible loading feedback.
  • Inside a Log thread/comment: Log returns to the ORIGINAL feed instance instead of replacing it. The paired SDK retains native saveable pane state for scroll-position restoration.
  • The chip is exclusive to Android root Log-tab reselect. Feed switches, pull-to-refresh, mount, resume and polling stay quiet; pulls use the list's own indicator.
  • Feedback is scoped to the active feed and request generation; reverse completion order, late responses after resume, failures and retries cannot clear another request's indicator.
  • iOS reselect wiring is unchanged. Normal cross-tab URL navigation remains intact.

Complete companion delivery

Component Source / status
NativePHP core NativePHP/mobile-air#588 — ready for review; pinned to cbf2eb466f85ed68dec960d5e727ca61673ddbf6
NativePHP UI NativePHP/mobile-ui#162 — ready for review; pinned to 00e4831be6cf24fa71ce60b0c1699d4060e3966a
Signed notification quote/image previews dev-null-sh/dev-null.sh#1004 — merge/deployment required for full preview parity
Rank/level metadata dev-null-sh/dev-null.sh#836 — existing independent companion; deployment required

The core/UI refs are public fork refs recorded in composer.json and composer.lock; only those two package records changed. No local path repositories, vendor patches or committed generated Android sources are needed. UI release must follow the matching core interface. Replace the temporary fork pins with upstream versions once the companions are released.

Missing backend metadata degrades honestly, but is not full end-to-end preview parity. The runtime lock and ignored free manifests are not committed; the author's existing PHP 8.5.12 runtime pin was preserved byte-for-byte.

Current upstream-main sync and lock resolution

Merge de6896e incorporates app main (4850154) without rewriting public history. Both SDK branches were merged with their current upstream mains: core cbf2eb4 includes b107ed9, UI 00e4831 includes 86ef0e6. This retains main's bounded image gestures, keyboard/chrome/font/runtime changes alongside the SDK companions; keeping the old fork pins would have dropped newly required gesture support. The stack renderer conflict combines upstream's chrome controller with per-mount saveable panes. Independent source review found no P0/P1.

Both tracked/free locks pin the published ZIP refs; only those two SDK package records differ from current app main. The author's existing .env and PHP 8.5.12 runtime lock are unchanged, and the ignored free setup is updated to the same pair. GitHub confirms all three PRs are mergeable. The author subsequently regenerated/rebuilt Android for the captures below; iOS and the remaining native gates are still outstanding.

Moto g24 review follow-up

Addressed the reported extra indicators in 3412c44954ea3cd31e9a15741e22e1b812fc1153: openAfresh() opens quietly, load(bool $feedback = false) is quiet by default, and only refreshLog() opts in. Pull-to-refresh uses the list's own spinner; feed switches and the iOS paths never raise this chip. Existing per-feed/request-generation ownership, retries and forced repeated refreshes are retained. No ListRenderer, styling, dependency or SDK edits were made.

Six additional parameterized regression cases fail before the fix and pass after it. The full app suite passes 1,303 tests (the same 1 skipped / 2 deprecated); after strengthening the cross-feed ownership scenario, both focused suites pass 30 tests / 578 assertions. Independent review and Pint/diff checks pass. Device confirmation of this latest change is still outstanding. The subsequent upstream-main integration is documented above.

Verification

  • App suite after the latest-main merge and PUBLIC pinned ZIP installation: 1,336 passed / 13,091 assertions, 1 skipped, 2 deprecated.
  • Core Feature + Unit after upstream merge: 2,312 passed / 7,080 assertions, 4 upstream-owned risky, 8 skipped; coverage not enabled.
  • UI after upstream merge: 416 passed / 1,227 assertions; previous paired-source harness 23 / 44. UI Kotlin contract checks are source assertions, not native execution.
  • API companion: full composer test previously passed 3,972 tests / 21,963 assertions, 100% code/type coverage, lint and frontend checks.
  • Pint, Composer validation, diff checks and independent reviews passed. Final cross-repo review cleared draft publication with no P0/P1 findings.
  • Temporary local SDK symlinks exposed dependency classes to architecture presets. Normal package directories passed all 5 architecture tests; no rules were weakened.

No native build, JVM tests or Android instrumentation was executed by the agent. The author rebuilt Android; the agent captured the real Pixel 9 UI and the limited interactions documented below. This does not complete the remaining device gates. The pane instrumentation test checks saveable-state ownership, not a real feed's pixel offset. This is ready for review/community testing, not a claim of completed native validation.

Follow-up: reported Kotlin compile failure

The first user build reported four unresolved references to LocalTopBarColor / LocalTopTabRow in the upstream UI tab-strip renderer. The pinned core does not expose those hooks. UI commit 28753b7 removes the unsupported imports and retains the documented transparent container; no core stubs or other tab behavior were added.

Both locks now pin that public fix. Its two source regression tests passed (red before the fix), the UI suite passed 407 tests, and the app's 26 Log navigation/feedback tests passed. The 1,297-test full app run above predates this Kotlin-only follow-up. Native compilation still awaits the user's rerun; PHP checks are not compiler validation.

Follow-up: Onboarding missing-file startup race

The next user build reached PHP but failed including Flysystem's PortableVisibilityConverter.php. The file was verified in the host, staging directory, bundle ZIP, APK's nested bundle and the exact device include path. Device error time was 12:50:30.192 UTC; the file was rewritten at 12:50:30.676 UTC, about 485 ms later.

Core commit f06d13c closes the source-confirmed race: an initializer could check persistent-runtime state before waiting on extractionLock, then re-extract DEBUG's laravel/ directory after another initializer booted PHP. Both foreground and background initializers now recheck under the SAME lock used by persistent boot, before extraction/artisan work. No credentials, persisted data or key-store implementation were changed.

Six source-contract regressions pass; a production-helper two-thread JVM test is included but unrun. Core PHP suites pass 2,157 tests, independent review is clear, and 50 app Onboarding/Log tests pass. Both locks pin the public fix. The guard remains in the latest core merge. The installed Composer packages are updated; after the broader upstream SDK sync, the generated native shell must be regenerated by the author using the backed-up-shell procedure below. Do not clear app data or credentials. Native compile/cold-start verification remains user-run.

Reproduce / Android test

Fresh checkout: use the README's normal or free setup. The normal manifest includes the paid secure-storage plugin. The existing author checkout already has composer.free.json / composer.free.lock updated to the same public refs; do not rerun first-install scripts that regenerate .env/keys on an existing installation.

For the existing free setup:

COMPOSER=composer.free.json composer install

Regenerate the Android shell so the new CORE Kotlin files are included; hot reload is insufficient. Back up any custom files under nativephp/android first: the installer replaces that generated directory. The author must run these commands manually:

COMPOSER=composer.free.json php -d memory_limit=1G artisan native:install android
COMPOSER=composer.free.json php -d memory_limit=1G artisan native:run android

Use Java 21 and the configured Android SDK. Keep host/bundled PHP minor versions aligned and inspect any installer-induced runtime-lock changes; do not commit the author's local runtime pin. The 1G override is required for the host build and does NOT change on-device PHP memory.

Outstanding gates

  • Author-regenerated Android debug build running on Pixel 9; real captures below.
  • iOS native compile; prepared core JVM suites (BottomTabTapDecisionTest, NativeScrollTargetRegistryTest, ScreenTokensTest) and pane instrumentation (ScreenTokenPaneStateTest).
  • On each feed, one Log tap scrolls to top AND refreshes; indicator settles after success/failure, repeated taps and idle.
  • Scroll deep, open a commit, post a reply (view focuses it), then tap Log: return to the same feed at the previous index AND pixel offset. Repeat through multiple nested screens.
  • During scroll-to-top, change feed/tab or push/return a child: no abandoned callback; pager drag/settle availability is correct.
  • Fresh same-URI replacement starts fresh; saved state does not leak between mounts/accounts.
  • Chip/pill layout, tap-through and hidden-state TalkBack behavior.
  • Notification filter persistence, group expansion/older pages, targets, staged arrivals, follow rollback, offline/retry and unread markers.
  • Quote/image/rank previews against a backend with BOTH API companions deployed.
  • Attach screenshot/recording per CONTRIBUTING.md before marking ready for merge.

Explicit non-claims / separate work

  • Idle/runtime behavior: the SDK sync inherits current upstream runtime/session changes as baseline. This PR adds no separate recovery implementation and does not claim the earlier idle symptom is fixed; that diagnosis and device validation remain separate.
  • Latest web/app ordering has no confirmed shared-query defect. Recompare after completed refreshes on the same account/backend before changing ordering.
  • Mention autocomplete is tracked separately in Add @mention user suggestions to the post and reply composer #78.

Screenshots

Partial evidence: Android AFTER only. Captured from the author-built PR on a physical Pixel 9 in light and dark mode. Notification list, More/filter sheet and Log screenshots are genuine, unedited captures. Recordings show the filter sheet and a scroll followed by the Log re-tap. They do not establish all-feed behavior, idle reliability or exact thread-return pixel-offset restoration.

Before-state and iOS screenshots/recordings are still pending, not waived. This Linux host has no iOS simulator. The phone was returned to Notifications and its original dark-mode setting; notification filters were not changed. Media is on the separate orphan screenshots branch, not this code branch.

log

iOS light iOS dark Android light Android dark
After

log-reselect

Recordings: Android dark, after, Android light, after

notification-filters

iOS light iOS dark Android light Android dark
After

Recordings: Android dark, after, Android light, after

notifications

iOS light iOS dark Android light Android dark
After

@ricardovfreixo
ricardovfreixo marked this pull request as ready for review October 23, 2026 13:32

@simonhamp simonhamp left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried this on a Moto g24: a Debug build of this branch merged onto current main, with #588 and #162 merged into main's SDK branches.

The new "Refreshing…" chip shows up in more places than the description says:

  • Switching between Following, Latest and Trending. switchTo() calls openAfresh(), which now calls open(feedback: true). Switching used to be quiet. Now every switch puts the chip up until the fetch lands, which takes a few seconds on Trending because it re-ranks.
  • Pull to refresh. load() passes feedback: true, so a pull shows the chip on top of the list's own pull spinner. That's two spinners for one refresh.
  • Re-tapping the Log tab through refreshLog(). This is the case the PR is about, and the chip makes sense there because nothing else shows that a refresh started.

Could the feedback be limited to the tab re-tap? openAfresh() would go back to plain open(), load() would stop passing feedback, and refreshLog() would ask for it instead, for example with a load(feedback: true). iOS doesn't have the reselect wired, so the chip would never show there, which matches how the Log behaved before.

If the chip is on pulls because Android's pull spinner runs for a fixed 1.5s and doesn't wait for the request, that's better fixed in mobile-ui's ListRenderer than with a second indicator in the app.

It also shows up in the middle of the screen on iOS.

I'm not sure we need it at all on either platform.

@ricardovfreixo

Copy link
Copy Markdown
Contributor Author

Thanks for testing on the Moto g24. Addressed in 3412c44: openAfresh() is quiet again, load() defaults to feedback: false, and only refreshLog() opts in. Feed switches and pulls no longer show the chip; pulls keep the native list spinner, and iOS remains quiet. Per-feed/generation ownership and repeated reselect dispatch are unchanged. No ListRenderer or styling changes.

Added regressions for both platforms and all feed/pull paths. Full PHP suite: 1,303 passed (1 skipped, 2 existing deprecations); focused reselect/feedback suites: 30 passed / 578 assertions. Independent review passed. I have not run a native build/device check of this follow-up. This commit is limited to the feedback fix; newer app/SDK main integration is separate.

@mdeloughry

Copy link
Copy Markdown

Tested the notification centre on a physical iPhone 17 running iOS 27.2, using app revision de6896e58028e01489ff5a002b0b35e346808440 and its pinned core/UI SDK companions.

Manual checks passed:

  • Notification filters and selection persistence.
  • More sheet: remains open while toggling filters and dismisses correctly.
  • Notification tap destinations and back navigation.
  • Group expansion/collapse, pagination and pull-to-refresh.
  • New-arrival button behavior.
  • Background/offline recovery and light/dark appearance checks.
  • With all six categories disabled, the empty state correctly shows Show all notifications; tapping it restores all categories.

No issues found during this manual iOS pass. The Show all notifications control initially appeared to be missing, but it correctly appears only when the filtered list is empty.

This validates the notification-centre checks above. The Android-only Log-tab reselect behavior in core PR #588 was not validated on iOS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bring the native notification centre in line with the web

3 participants