Skip to content

fix(web): a friend request announces itself once and then leaves no trace #1010

Description

@janlauber

A friend request announces itself once and then vanishes. If you were riding, or the tab was closed, or browser notifications are off, nothing anywhere tells you someone is waiting — you find out the next time you happen to open /friends.

Every other waiting thing has a persistent mark. This one does not.

what persistent indicator
room unread a count badge — Sidebar.svelte:256, unreadCount(room.unread)
DM unread the head goes bold — Sidebar.svelte:393
friend request nothing<a href="/friends" class="hover:text-ink ml-auto normal-case">friends</a> (Sidebar.svelte:376)

There is no notification centre in this app, and this issue does not add one. The sidebar is the notification surface, and friend requests are the one announceable event with no home in it.

What #876 built is a one-shot announcement: friendEvent() (friends.svelte.ts:46) returns "X wants to be friends" on the single refresh where pending_in first appears, diffed against the previous list. Correct as far as it goes — it is an announcement, and an announcement is not a state.

Two changes

1. A count on the sidebar's friends link

  MESSAGES                    friends ②
  ● David Kneubühler
  ○ Captain-SVXN

The count is friends.list.filter(f => f.status === 'pending_in').length. Use the existing unreadCount() from $lib/messages/unread-marks so the badge looks like every other badge, and the same visual language carries.

+layout.svelte:60 already calls friends.reload() app-wide, so the count is live on every page without new plumbing.

2. /friends separates what needs you from what does not

FriendsPanel.svelte:111 is:

const pending = $derived((list ?? []).filter((f) => f.status !== 'accepted'));

— one list holding both pending_in (someone is waiting on you) and pending_out (you are waiting on them), rendered below your accepted friends. The two have opposite meanings and only one is actionable.

/friends
┌ wants to be friends ─────────────────┐   ← pending_in, first, its own section
│ Mike Frei          [Accept] [Dismiss]│
│ Ana Rüegg          [Accept] [Dismiss]│
└──────────────────────────────────────┘
  your friends
    David Kneubühler · riding
    Captain-SVXN
  waiting on them                          ← pending_out, quiet, last
    Uelischmueli · asked 2 days ago

Incoming requests move to the top as their own section; outgoing stay quiet at the bottom. Accept and Dismiss already exist (FriendsPanel.svelte:219) — this only regroups and reorders.

The badge counts open requests, not unseen ones

Visiting /friends does not clear it. Accepting or dismissing does.

2 open requests    badge ②
you visit /friends badge ②   ← still true: they are still waiting
you accept one     badge ①
you dismiss other  badge gone

This is deliberately not the unread semantics the other badges use, and the difference is the point: seeing a message is the whole job, seeing a request is not. The badge means "this many people are waiting on you", which stays true until they aren't.

Because it is derived from pending_in and never stored, there is no read state to persist — no table, no endpoint, no migration. The count is a $derived over a list the layout already refreshes.

Acceptance criteria

  • The sidebar's friends link carries a count of incoming requests, styled like the room and DM badges.
  • The count updates without a reload when a request arrives or is answered.
  • Visiting /friends does not change the count; accepting or dismissing does.
  • Zero open requests shows no badge at all — never a 0.
  • /friends shows incoming requests first, in their own section, above accepted friends; outgoing sit last and quiet.
  • pending_in and pending_out are never rendered in the same list again.
  • The A friend request, and its acceptance, announce themselves #876 announcement still fires — this adds a persistent state beside it, it does not replace it.
  • No new table, endpoint or migration.
  • Vitest: the count derives correctly, ignores pending_out, and the badge disappears at zero.
  • make ci green.

Verify it for real

The verify skill with two accounts (?as=): send a request from B, confirm A's sidebar badges without a reload and while A is on a page other than /friends. Visit /friends and confirm the badge stays. Accept, and confirm it clears.

Deliberately not in scope, and why

  • A notification centre. Deferred on purpose. If one is ever built, this badge is one of its inputs, not a thing it replaces.
  • Room invites. There is no such object: rooms.go:188"members only — the code IS the invite". A room invite is a share link, so nothing is addressed to you and nothing is pending. Nothing to badge.
  • Session RSVPs. They cannot be badged without a schema change — session_rsvps stores only the affirmative, so "hasn't answered" and "said no" are the same row-less state. See docs(sessions): a planned session cannot tell "hasn't answered" from "said no" — should declining be recorded? #1011, which poses the decision.
  • Trophies, medals, category changes, acceptances. These are news, not tasks. They have no action, so "clears when you act" cannot apply to them, and mixing news into a task count is how a badge stops being read. They keep the announcement path A friend request, and its acceptance, announce themselves #876 gave them.

Related

#1017 (moves this badge onto a real nav item), #1016 (the status these rows show), #1011 (the RSVP decision this excludes), #876 (the announcement this makes persistent), #568 (one announcement path, and unread that reads the same everywhere), #824 (the badge rules), ADR-0020 (the sidebar as a place), ux.md.

Activity

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdesignDesign related stuff

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions