Route event notifications to the event, not the actor (1.2.35)

Audited the RSVP / event-update notification flow end to end. The data
layer was already solid and secure — RSVPs p-tag the host, updates
p-tag every RSVPer, and useActivityNotifications guards event-update
spoofing by requiring the author to be the event's host (from the `a`
coordinate) or an admin. The gap was on the rendering side.

Fixes:

- ActivityNotificationCard hardwired every notification to link to the
  ACTOR's profile. For an RSVP ("Alice is going to your event") the host
  wants the event + attendee list, and for an event update ("moved to
  Sunday") the attendee wants to READ the update — not visit a profile.
  Both events carry the event's `a` coordinate (31923:pubkey:identifier),
  so these two types now route to /event/:pubkey/:identifier. Everything
  else still lands on the actor's profile. d-tags containing ':' are
  reconstructed correctly.

- useRsvp only invalidated the single-event RSVP query, so the grid /
  calendar "going" counts lagged behind by up to the 30s staleTime after
  RSVPing. Now also invalidates the batch-event-rsvps query so counts
  update promptly.

- Social-tab empty-state copy omitted RSVPs and host updates even though
  they land there; made it honest.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-07 22:56:20 +07:00
parent f253fc55fa
commit 719dd2c4a8
4 changed files with 28 additions and 5 deletions

View file

@ -1,7 +1,7 @@
{
"name": "skate-spots",
"private": true,
"version": "1.2.34",
"version": "1.2.35",
"type": "module",
"homepage": "https://skate-spots.com",
"scripts": {

View file

@ -58,6 +58,23 @@ export function ActivityNotificationCard({
addSuffix: true,
});
// Type-aware destination. For event RSVPs (a host is notified) and event
// updates (an attendee is notified), the useful place to land is the EVENT —
// not the actor's profile. Both carry the event's addressable coordinate in
// an `a` tag ("31923:pubkey:identifier"), which maps to /event/:pubkey/:id.
const eventCoordTag =
notification.type === 'rsvp' || notification.type === 'event-update'
? notification.event.tags.find(([t]) => t === 'a')?.[1]
: undefined;
const eventPath = (() => {
if (!eventCoordTag) return null;
const [kind, pubkey, ...idParts] = eventCoordTag.split(':');
const identifier = idParts.join(':'); // d-tags may contain ':'
if (kind !== '31923' || !pubkey || !identifier) return null;
return `/event/${pubkey}/${identifier}`;
})();
const snippet =
(notification.type === 'reply' ||
notification.type === 'mention' ||
@ -110,11 +127,13 @@ export function ActivityNotificationCard({
</div>
);
// Link to the actor's profile (the always-valid in-app destination).
if (!npub) return body;
// Prefer landing on the event for RSVP / event-update notifications; fall back
// to the actor's profile for everything else (the always-valid destination).
const destination = eventPath ?? (npub ? `/${npub}` : null);
if (!destination) return body;
return (
<Link to={`/${npub}`} onClick={onNavigate} className="block">
<Link to={destination} onClick={onNavigate} className="block">
{body}
</Link>
);

View file

@ -413,7 +413,7 @@ export function NotificationBell({ className }: { className?: string }) {
</div>
)}
{!activityLoading && activity.length === 0 && (
<EmptyState label="When people zap, react, repost, mention or reply to you, it lands here." />
<EmptyState label="Zaps, reactions, reposts, mentions, replies, event RSVPs and host updates all land here." />
)}
<div className="space-y-2.5">
{activity.map((n) => (

View file

@ -199,6 +199,10 @@ export function useRsvp() {
onSuccess: (_data, variables) => {
const coord = eventCoord(variables.event);
queryClient.invalidateQueries({ queryKey: ['event-rsvps', coord] });
// Refresh the grid/calendar "going" counts too (batch query keyed by the
// full coordinate set), so numbers update right after RSVPing rather than
// waiting out the 30s staleTime.
queryClient.invalidateQueries({ queryKey: ['batch-event-rsvps'] });
},
});