Merge /follows/requests into /notifications as a tabbed inbox

Folds the actionable follow-requests surface into the Notifications page
as a Requests tab (alongside the existing Activity tab), so the navbar
exposes a single bell instead of two adjacent inboxes. The Requests tab
shows a count badge for pending rows regardless of read state, while the
bell badge keeps reflecting the unread-notifications count (which already
covers `follow_request_received` rows). The standalone /follows/requests
URL is preserved as a 301 redirect so prior notification deep-links,
emails, and bookmarks still resolve.

Driven by the IA review captured in docs/information-architecture.md.
Specs (notifications, social-follows, journal-landing) are updated in
the same change to reflect the new structure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-26 08:24:59 +02:00
parent 3cd01fe9c0
commit 0306d90de8
15 changed files with 715 additions and 200 deletions

View file

@ -0,0 +1,438 @@
# Information Architecture Review
*Snapshot date: 2026-04-26.* If the navbar, route table, or feed model has
shifted since then, treat this doc as stale and refresh against
`apps/journal/app/routes.ts` + `apps/journal/app/root.tsx`.
A snapshot of where every page lives, who sees it, and how visitors navigate
between them. Intended for review — flag anything that doesn't make sense or
should change.
## Apps
trails.cool ships two front-ends:
- **Journal** (`trails.cool`) — user accounts, social, content. Most of the IA
question lives here.
- **Planner** (`planner.trails.cool`) — anonymous, ephemeral. Five routes
total; not a real IA concern. Listed at the bottom for completeness.
---
## Journal sitemap
### Public surface (logged-out)
```
/ Anonymous home (hero + marketing + public feed)
/users/:username Public profile (full or locked stub)
/users/:username/followers Followers list (404 on private profile)
/users/:username/following Following list (404 on private profile)
/activities/:id Public activity (or 404)
/routes/:id Public route (or 404)
/auth/register Sign-up
/auth/login Sign-in
/auth/verify Magic-link click-through landing
/auth/accept-terms Re-accept gate (used after a Terms version bump)
/legal/imprint Imprint (German required)
/legal/privacy Privacy policy
/legal/terms Terms of service
/privacy 301 → /legal/privacy
```
### Authenticated surface (logged-in)
```
/ Personal dashboard ("your activities" stream)
/feed Social feed (people you follow, accepted)
/notifications Notifications log (4 event types, cursor paginated)
/follows/requests Incoming Pending follow requests inbox
/users/:username Any profile (own, followed, or locked stub)
/users/:username/followers Gated by locked-account rule
/users/:username/following Gated by locked-account rule
/routes Your routes
/routes/new Create route
/routes/:id View
/routes/:id/edit Edit (handoff to Planner via JWT callback)
/activities Your activities
/activities/new Create activity
/activities/:id View
/settings Profile + email + passkeys + sync + danger zone
/sync/import/:provider Wahoo import flow
/auth/logout Logout
```
### Navigation surfaces
**Top navbar (logged-in)** — in document order:
```
[trails.cool] Feed Routes Activities ... 🔔 Follow requests <username> Settings Logout
└─ unread badge └─ pending badge
```
**Top navbar (logged-out):**
```
[trails.cool] ... Login [Register]
```
**Footer (everywhere):** Imprint · Privacy · Terms · Source (GitHub) · "Alpha".
**Profile self-link in nav** points to `/users/<self>`. There is no separate
"My profile" route; you reach your own profile via your own username.
---
## Logged-in vs logged-out home
`/` does double duty:
| Visitor | What `/` shows |
|---|---|
| Anonymous | Hero · Sign-up CTAs · "Try the Planner" · marketing cards (flagship only) · **public instance feed** |
| Signed-in | "Welcome, X" · Feed button · "New activity" CTA · **personal stream of your own activities** |
The two surfaces share no layout — they're effectively two pages behind one
URL. `home.tsx` branches on `user`.
---
## Two feed surfaces, three views
**Decision (2026-04-26):** merge Social and Public into a single `/feed`
page with a Followed / Public toggle. `/` stays "your content," `/feed`
becomes "everyone else's content."
| Surface | View | Audience |
|---|---|---|
| `/` (logged-in) | **Personal** — your own activities | You |
| `/feed` | **Followed** (default) — accepted-followed users' public activities | Signed-in |
| `/feed?view=public` | **Public** — instance-wide public activities | Signed-in |
| `/` (logged-out) | Hero + marketing + public feed (visitor entry point) | Anonymous |
Signed-in users can now see the public instance feed without logging out.
The logged-out `/` keeps its public feed as the anonymous visitor's first
impression.
Implementation notes (for whoever picks this up):
- `listSocialFeed` and `listRecentPublicActivities` already exist in
`apps/journal/app/lib/activities.server.ts` — the toggle just picks one.
- URL shape: `?view=followed` (default, can omit) | `?view=public`. Keeps the
toggle bookmarkable and avoids two routes.
- Empty-state in the Followed view today links to `/` ("see the public feed
there") — that link should become the in-page Public toggle.
- Anonymous request to `/feed` keeps redirecting to `/auth/login`; the
public feed remains reachable for them on `/`.
---
## Notifications vs follow requests
Two adjacent inboxes, two navbar entries:
| | `/notifications` | `/follows/requests` |
|---|---|---|
| Purpose | Read-only event log | Actionable Approve/Reject |
| Types covered | follow_received, follow_request_received, follow_request_approved, activity_published | Only Pending follows targeting you |
| Navbar surface | 🔔 icon + red unread badge | "Follow requests" text + red pending count |
| State | `read_at` per row | None — Approve/Reject mutates the follow row |
A `follow_request_received` notification points its "Review request" link at
`/follows/requests`, so the two surfaces are explicitly cross-linked rather
than merged. Mastodon makes the same split.
---
## Settings
`/settings` is a single scrollable page with five concerns stacked top to
bottom:
1. Profile (display name, bio, profile visibility)
2. Email change (with re-verification)
3. Passkeys (list, add, delete)
4. Connected services (Wahoo today, Strava/Garmin later)
5. Danger zone (delete account)
Spec was recently split into three (`profile-settings`, `account-management`,
`connected-services`) but the UI is still one page.
---
## Cross-app linking
- **Journal → Planner:** "Edit in Planner" on a saved route generates a JWT
callback URL and opens `planner.trails.cool/session/<id>`. Logged-out home
also has a "Try the Planner" link.
- **Planner → Journal:** the post-edit "Save" flow POSTs back to
`/api/routes/:id/callback` on the Journal that issued the JWT.
- The Planner has no link back to a specific Journal otherwise — it's
intentionally instance-agnostic.
---
## Planner sitemap
```
/ Anonymous home (CTA: "Plan a route")
/new Create a fresh anonymous session
/session/:id Collaborative editor
```
No accounts, no profiles, no settings. IA is essentially trivial.
---
## Observations worth discussing
These are tensions or surprises I noticed while mapping. None are bugs — but
each is a deliberate IA choice that's worth confirming.
1. **`/` is two different products.** Logged-in `/` is "your stuff," logged-out
`/` is "the instance." Discoverable? Or should logged-in `/` keep showing
*something* of the public surface (e.g., a "Discover" tab)?
2. ~~**Signed-in users can't see the public instance feed.**~~ *Resolved:
merged into `/feed` with a Followed / Public toggle (see above).*
3. **The navbar's account cluster is busy.** `<username>` + `Settings` + `Logout`
is three controls for the same concept. Already on the redesign list —
collapsing into an avatar dropdown is the natural fix.
4. **`/feed` is reachable from both the navbar AND a button on the home page.**
The button on logged-in `/` (currently labelled "Feed") is mildly redundant
with the navbar entry. Worth keeping if Personal and Followed feel like
separate destinations; worth dropping if the navbar's "Feed" link is
obvious enough on its own.
5. **🔔 vs "Follow requests" are visually inconsistent.** One is an icon, the
other is a text link. Either both icons (consistent) or both text
(discoverable) would be more legible. Mobile wrap is already going to be a
problem.
6. **Routes and Activities are siblings, not nested.** That's correct today —
an activity can exist without a route, and vice versa. Worth flagging only
because some apps merge them into a single "history" timeline.
7. **`/settings` is one page.** Fine for now (5 sections, ~6 controls per
section). At ~10 sections it becomes a tab strip; at ~15 it becomes a left
nav. Not urgent.
8. **No `/explore`, no `/users` directory, no search.** A signed-in user who
wants to *find* people to follow has no in-app path — they need a username
from outside. Federation (Phase 2) makes this worse before it gets better.
9. **No mobile breakpoint for the navbar.** The cluster of 7 links on the
right will wrap on phones today. Tied to the navbar redesign.
10. **Profile vs identity.** Your own profile is at `/users/<you>` not
`/me` or `/profile`. Reachable via the navbar self-link. Fine, but means
"view as logged-out visitor" is non-trivial — opening incognito is the
only way to see your locked stub.
---
## Open IA questions
If the answer to any of these is "I don't know yet," that's a signal it's
worth discussing before the navbar redesign locks in shapes:
- ~~Should `/` and `/feed` merge for signed-in users?~~ *Resolved:
no — `/` stays "you," `/feed` becomes "everyone else" with a
Followed / Public toggle.*
- ~~Where does an `/explore` or `/discover` page live?~~ *Resolved: it's
the Public view inside `/feed`, no new top-level destination needed.*
---
## Implementation backlog
Decisions captured above translate into two work-streams. Items marked
*needs decision* are blockers — confirm before starting that stream.
### Stream A — Merge Social and Public feeds into `/feed`
**Code changes:**
1. **`apps/journal/app/routes/feed.tsx`**
- Loader reads `?view=` query (`"followed"` default, `"public"` accepted;
anything else falls back to default).
- Branch the fetch: `listSocialFeed(user.id, 50)` for Followed,
`listRecentPublicActivities(50)` for Public — both already exist in
`apps/journal/app/lib/activities.server.ts`.
- Render a toggle (two pills/tabs) at the top of the page; active state
highlighted; toggle uses `<Link to="?view=public">` so it's plain HTTP,
SSR-friendly, bookmarkable, and works without JS.
- Per-view `<meta>` title: "Following — trails.cool" / "Public — trails.cool".
- Per-view empty state: Followed view's existing "see the public feed"
escape now links to `?view=public` instead of `/`.
2. **Translation keys (`apps/journal/app/locales/{en,de}/journal.json`)**
- `social.feed.toggle.followed`, `social.feed.toggle.public`
- `social.feed.public.heading`, `social.feed.public.empty`
- Drop `social.feed.publicFeedLink` once the empty-state link is rewired.
3. **No change** to `apps/journal/app/routes/home.tsx` — logged-in `/` keeps
showing the personal stream; the "Feed" button still targets `/feed`
(defaults to Followed view).
4. **No change** to anonymous `/feed` behavior — it continues to redirect to
`/auth/login`. The public feed remains visible to anonymous visitors on `/`.
**Spec updates after Stream A ships:**
- `openspec/specs/social-follows/spec.md` — the **Social activity feed**
requirement currently scopes `/feed` to followed users only. Update it (or
split into a new "Feed views" requirement) to add scenarios for the Public
view and the toggle behavior.
- `openspec/specs/activity-feed/spec.md` — the **Instance-wide public
activity feed** requirement says it powers the home page. Add a scenario
noting it now also powers `/feed?view=public` for signed-in users.
- `openspec/specs/journal-landing/spec.md` — no change. Logged-in `/`
already shows the personal stream; this is unchanged.
### Stream B — Merge Follow requests into Notifications
**Decision (2026-04-26):** fold `/follows/requests` into `/notifications`
as a tabbed sub-page (Activity / Requests). The bell icon stays the
single inbox surface; the Requests tab shows a dot when there are pending
follows still needing Approve/Reject. The Notifications spec's existing
"no inline Approve/Reject on the activity log" rule is preserved — the
Requests tab is the actionable surface, the Activity tab is the log.
**Code changes:**
1. **`apps/journal/app/routes/notifications.tsx`**
- Loader reads `?tab=` (`"activity"` default, `"requests"` accepted).
- For Activity tab: existing behavior (paginated rows, `?before=` cursor).
- For Requests tab: load `listPendingFollowRequests(user.id)` and
`countPendingFollowRequests(user.id)` (already exist).
- Page renders a tab strip at the top with both labels and an unread/
pending dot per tab; active tab highlighted.
- "Mark all read" button only appears on the Activity tab.
2. **`apps/journal/app/routes.ts`**
- `route("follows/requests", ...)` becomes a redirect to
`/notifications?tab=requests` (301). New file
`routes/follows.requests.tsx` reduces to one `loader` returning a
redirect, mirroring the existing `routes/privacy.tsx` pattern.
3. **`apps/journal/app/lib/notifications/link-for.ts`**
- `follow_request_received` now resolves to
`/notifications?tab=requests` instead of `/follows/requests`. Update
the unit test in `link-for.test.ts`.
4. **`apps/journal/app/root.tsx`**
- Drop the `Follow requests` navbar entry entirely.
- Drop `pendingFollowRequests` from the root loader (no longer needed
for navbar). The Requests tab loader inside `/notifications`
fetches it on-demand.
- Bell unread badge logic unchanged — `follow_request_received`
already creates an unread row, so the existing unread count
implicitly covers pending requests.
5. **i18n updates** (`packages/i18n/src/locales/{en,de}.ts`)
- New keys: `notifications.tabs.activity`, `notifications.tabs.requests`.
- The `settings.profile.visibility.privateHelp` string mentions
`/follows/requests` — update to point at the new surface.
6. **e2e tests**
- `e2e/social.test.ts`: the `/follows/requests redirects anonymous
visitors to login` test changes to verify the new redirect target,
and the "B sees the request in /follows/requests" step navigates
to `/notifications?tab=requests` instead.
- `e2e/notifications.test.ts`: same — replace `bPage.goto("/follows/requests")`
with `bPage.goto("/notifications?tab=requests")`.
**Spec updates after Stream B ships:**
- `openspec/specs/notifications/spec.md` — extend the **Notifications
page and unread count** requirement to describe the tabbed structure;
refine the "follow_request_received card links to /follows/requests,
not inline Approve/Reject" scenario to reflect the new URL and the tab
separation. The `linkFor` scenarios need the updated path.
- `openspec/specs/social-follows/spec.md` — update the **Pending follow
request management** requirement: navigation moves to
`/notifications?tab=requests`; the navbar count badge requirement is
retired (it's now a dot inside the Requests tab).
- `openspec/specs/journal-landing/spec.md` — the **Notifications entry
in the navbar** requirement should clarify that this is the only
inbox-style entry (no separate Follow requests entry).
### Stream C — Navbar redesign
*Pending tomorrow. Principles agreed in the IA review; specifics still
need decisions before implementation.*
**Needs decision:**
- **Avatar dropdown content.** Profile · Settings · Logout — any others?
(Theme toggle? Language toggle? Account switcher when federation lands?)
- **Mobile target.** Hamburger? Bottom tab bar? "Phase 2, just don't break"?
- **Self-link vs avatar.** Today the navbar has a `<username>` text link to
your profile. After the avatar dropdown, does the avatar take that role,
or does Profile live only inside the dropdown?
**Code changes (assuming avatar dropdown + icon-only Follow requests + no
mobile work yet):**
1. **`apps/journal/app/root.tsx`**
- Add `displayName` to the loader's `user` payload (currently only
`id` + `username`).
- Replace the `<username> · Settings · Logout` cluster with a single
avatar trigger + dropdown menu.
- Replace the "Follow requests" text link with an icon-only button +
badge (matching the bell's visual treatment).
- Confirm bell + Follow requests sit on the same baseline / same gap.
2. **New components in `apps/journal/app/components/`**
- `Avatar.tsx` — initials fallback when no image (none of our 4 prod
users have an avatar field today, so this is initials-only initially).
- `NavDropdown.tsx` — click-outside + Escape-to-close. Simple
headless impl, no library.
3. **Loader query**
- `getSessionUser` already returns `displayName`; just expose it in the
loader return shape.
**Spec updates after Stream B ships:**
- `openspec/specs/journal-landing/spec.md` — currently has only the
**Notifications entry in the navbar** requirement. Add a new
requirement (or extend that one) for the account dropdown shape and the
Follow-requests icon. Or factor the navbar out into its own spec —
plausible if the cluster keeps growing.
- `openspec/specs/social-follows/spec.md` — the **Pending follow request
management** requirement says "the 'Follow requests' link in the navbar
renders with a small red count badge". Update the wording to reflect
the icon treatment.
- `openspec/specs/notifications/spec.md` — the **Notifications page and
unread count** requirement already says "navbar entry renders with a
count badge"; verify the wording still applies cleanly to the icon-only
treatment.
### Out of scope (deliberately)
These came up in the IA review but aren't part of this round:
- **Search / `/explore` directory.** No way to find users in-app. Defer
until federation (Phase 2) makes it more painful.
- **Settings sectioning.** Single scrollable page is fine at 5 sections.
- **`/me` alias for own profile.** Marginal value; navbar self-link is
enough.
- **Mobile breakpoints across the app.** Bigger than just the navbar.
- **Is "Follow requests" a sub-page of Notifications or a sibling?** Today
it's a sibling (separate navbar item). Could be a tab inside `/notifications`.
- **Avatar dropdown content.** Profile · Settings · Logout — anything else?
(Theme toggle? Language toggle? Account switcher when federation lands?)
- **Mobile.** Hamburger menu? Bottom tab bar? Nothing yet — what's the target?
- **Search.** None today. Adding it changes the navbar. When does it land?