Show bookmarked communities in My Communities via NIP-51 kind 10004
Bookmarking a kind 34550 community now writes to the NIP-51 Communities list (kind 10004) keyed by the addressable coordinate, so the reference stays valid across community updates. My Communities merges bookmarked communities as a third discovery source alongside founded and member-of, with Founder/Member/Bookmarked badges on each card. Bookmark toasts live on the mutation itself so they survive the more-menu dialog unmounting between .mutate() and publish resolution.
This commit is contained in:
@@ -791,6 +791,22 @@ Both kind `34550` and kind `30009` are addressable events. To add or remove rank
|
||||
2. Extract badge `a` tags from results.
|
||||
3. `{ "kinds": [34550], "#a": ["30009:...", "..."] }`
|
||||
|
||||
**Communities a user has bookmarked:**
|
||||
|
||||
Agora uses [NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md) kind `10004` ("Communities") to let users save communities they want quick access to without requiring membership. Bookmarked communities are surfaced in the "My Communities" view alongside founded and member-of communities.
|
||||
|
||||
1. `{ "kinds": [10004], "authors": ["<user-pubkey>"], "limit": 1 }`
|
||||
2. Extract `a` tags whose value begins with `34550:` from the result.
|
||||
3. For each coordinate `34550:<author-pubkey>:<d-tag>`, query the community definition with both `authors` and `#d` filters to prevent spoofing:
|
||||
|
||||
```jsonc
|
||||
{ "kinds": [34550], "authors": ["<author-pubkey>"], "#d": ["<d-tag>"], "limit": 1 }
|
||||
```
|
||||
|
||||
Clients toggling a bookmark MUST perform a read-modify-write cycle on the replaceable kind `10004` event: fetch the freshest version from relays, add or remove the matching `["a", "34550:<pubkey>:<d-tag>"]` tag, and republish the full tag list. Appending new entries to the end preserves chronological bookmark order per NIP-51.
|
||||
|
||||
When the same community appears in multiple discovery sources, clients SHOULD display a single card but MAY indicate all applicable relationships (e.g. a member who has also bookmarked a community).
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- **Author filtering**: Clients MUST filter community definitions by `authors` to prevent impersonation.
|
||||
@@ -804,6 +820,7 @@ Both kind `34550` and kind `30009` are addressable events. To add or remove rank
|
||||
- [NIP-22](https://github.com/nostr-protocol/nips/blob/master/22.md) -- Comment
|
||||
- [NIP-31](https://github.com/nostr-protocol/nips/blob/master/31.md) -- Unknown Event Kinds (`alt` tag)
|
||||
- [NIP-32](https://github.com/nostr-protocol/nips/blob/master/32.md) -- Labeling (moderation `ban` label)
|
||||
- [NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md) -- Lists (kind `10004` Communities list for bookmarks)
|
||||
- [NIP-56](https://github.com/nostr-protocol/nips/blob/master/56.md) -- Reporting
|
||||
- [NIP-58](https://github.com/nostr-protocol/nips/blob/master/58.md) -- Badges
|
||||
- [NIP-72](https://github.com/nostr-protocol/nips/blob/master/72.md) -- Moderated Communities
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Crown, Shield, Users } from 'lucide-react';
|
||||
import { Bookmark, Crown, Shield, Users } from 'lucide-react';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
|
||||
@@ -18,6 +18,10 @@ interface CommunityCardProps {
|
||||
event: NostrEvent;
|
||||
/** Whether the current user founded this community. */
|
||||
isFounded?: boolean;
|
||||
/** Whether the current user is a validated member (rank > 0 via badge chain). */
|
||||
isMember?: boolean;
|
||||
/** Whether the current user has bookmarked this community (NIP-51 kind 10004). */
|
||||
isBookmarked?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -25,7 +29,13 @@ interface CommunityCardProps {
|
||||
* Compact card for displaying a community in a list.
|
||||
* Shows image, name, description snippet, founder info, and moderator count.
|
||||
*/
|
||||
export function CommunityCard({ event, isFounded, className }: CommunityCardProps) {
|
||||
export function CommunityCard({
|
||||
event,
|
||||
isFounded,
|
||||
isMember,
|
||||
isBookmarked,
|
||||
className,
|
||||
}: CommunityCardProps) {
|
||||
const community = useMemo(() => parseCommunityEvent(event), [event]);
|
||||
|
||||
const founderAuthor = useAuthor(event.pubkey);
|
||||
@@ -75,12 +85,22 @@ export function CommunityCard({ event, isFounded, className }: CommunityCardProp
|
||||
<h3 className="text-sm font-semibold truncate flex-1 group-hover:text-primary transition-colors">
|
||||
{community.name}
|
||||
</h3>
|
||||
{isFounded && (
|
||||
{isFounded ? (
|
||||
<Badge variant="secondary" className="text-[10px] gap-1 shrink-0 bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20">
|
||||
<Crown className="size-2.5" />
|
||||
Founder
|
||||
</Badge>
|
||||
)}
|
||||
) : isMember ? (
|
||||
<Badge variant="secondary" className="text-[10px] gap-1 shrink-0 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20">
|
||||
<Shield className="size-2.5" />
|
||||
Member
|
||||
</Badge>
|
||||
) : isBookmarked ? (
|
||||
<Badge variant="secondary" className="text-[10px] gap-1 shrink-0 bg-primary/10 text-primary border-primary/20">
|
||||
<Bookmark className="size-2.5 fill-current" />
|
||||
Bookmarked
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
|
||||
@@ -47,6 +47,7 @@ import { CommunityReportDialog } from '@/components/CommunityReportDialog';
|
||||
import { AddToListDialog } from '@/components/AddToListDialog';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useBookmarks } from '@/hooks/useBookmarks';
|
||||
import { useCommunityBookmarks } from '@/hooks/useCommunityBookmarks';
|
||||
import { usePinnedNotes } from '@/hooks/usePinnedNotes';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
@@ -57,7 +58,7 @@ import { useOrganizers } from '@/hooks/useOrganizers';
|
||||
import { usePinnedPosts } from '@/hooks/usePinnedPosts';
|
||||
import { useCountryFeed } from '@/contexts/CountryFeedContext';
|
||||
import { useCommunityModerationContext } from '@/contexts/CommunityModerationContext';
|
||||
import { type CommunityMenuContext, canBanTarget, getViewerAuthority } from '@/lib/communityUtils';
|
||||
import { type CommunityMenuContext, canBanTarget, getViewerAuthority, COMMUNITY_DEFINITION_KIND } from '@/lib/communityUtils';
|
||||
// NOTE: `CommunityMenuContext` is derived automatically from
|
||||
// `useCommunityModerationContext()`. Parents install a
|
||||
// `CommunityModerationContext.Provider` to enable community-aware menu items.
|
||||
@@ -368,7 +369,26 @@ function NoteMoreMenuContent({ event, open, onOpenChange, communityContext, onRe
|
||||
const navigate = useNavigate();
|
||||
const { user } = useCurrentUser();
|
||||
const { isBookmarked, toggleBookmark } = useBookmarks();
|
||||
const bookmarked = isBookmarked(event.id);
|
||||
const {
|
||||
isBookmarked: isCommunityBookmarked,
|
||||
toggleBookmark: toggleCommunityBookmark,
|
||||
} = useCommunityBookmarks();
|
||||
|
||||
// Community events (kind 34550) are bookmarked via NIP-51 kind 10004
|
||||
// using their addressable `a` tag coordinate, so the reference stays
|
||||
// valid across community updates. Non-community events use the standard
|
||||
// kind 10003 bookmark list keyed by event id.
|
||||
const isCommunityEvent = event.kind === COMMUNITY_DEFINITION_KIND;
|
||||
const communityATag = useMemo(() => {
|
||||
if (!isCommunityEvent) return undefined;
|
||||
const dTag = event.tags.find(([n]) => n === 'd')?.[1];
|
||||
if (!dTag) return undefined;
|
||||
return `${COMMUNITY_DEFINITION_KIND}:${event.pubkey}:${dTag}`;
|
||||
}, [isCommunityEvent, event.pubkey, event.tags]);
|
||||
|
||||
const bookmarked = isCommunityEvent
|
||||
? !!communityATag && isCommunityBookmarked(communityATag)
|
||||
: isBookmarked(event.id);
|
||||
const { isPinned, togglePin } = usePinnedNotes(user?.pubkey);
|
||||
const pinned = isPinned(event.id);
|
||||
const isOwnPost = user?.pubkey === event.pubkey;
|
||||
@@ -411,7 +431,15 @@ function NoteMoreMenuContent({ event, open, onOpenChange, communityContext, onRe
|
||||
|
||||
const handleBookmark = () => {
|
||||
impactLight();
|
||||
toggleBookmark.mutate(event.id);
|
||||
if (isCommunityEvent) {
|
||||
if (!communityATag) return;
|
||||
// Success/error toasts are fired from the mutation itself in
|
||||
// useCommunityBookmarks so they survive this dialog unmounting
|
||||
// before the publish resolves.
|
||||
toggleCommunityBookmark.mutate({ aTag: communityATag });
|
||||
} else {
|
||||
toggleBookmark.mutate(event.id);
|
||||
}
|
||||
close();
|
||||
};
|
||||
|
||||
@@ -548,7 +576,11 @@ function NoteMoreMenuContent({ event, open, onOpenChange, communityContext, onRe
|
||||
/>
|
||||
<MenuItem
|
||||
icon={<Bookmark className={cn("size-5", bookmarked && "fill-current")} />}
|
||||
label={bookmarked ? 'Remove Bookmark' : 'Bookmark'}
|
||||
label={
|
||||
isCommunityEvent
|
||||
? (bookmarked ? 'Remove community bookmark' : 'Bookmark community')
|
||||
: (bookmarked ? 'Remove Bookmark' : 'Bookmark')
|
||||
}
|
||||
onClick={handleBookmark}
|
||||
/>
|
||||
{user && (
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useCurrentUser } from './useCurrentUser';
|
||||
import { useNostrPublish } from './useNostrPublish';
|
||||
import { fetchFreshEvent } from '@/lib/fetchFreshEvent';
|
||||
import { COMMUNITY_DEFINITION_KIND } from '@/lib/communityUtils';
|
||||
import { toast } from '@/hooks/useToast';
|
||||
|
||||
/** NIP-51 Communities list — kind 10004. */
|
||||
export const COMMUNITIES_LIST_KIND = 10004;
|
||||
|
||||
/**
|
||||
* Hook to manage the user's NIP-51 Communities list (kind 10004).
|
||||
*
|
||||
* This list stores `a` tag coordinates for kind 34550 community definitions
|
||||
* that the user has bookmarked / "saved". Unlike `useBookmarks` (kind 10003)
|
||||
* which targets event IDs, this list targets addressable coordinates so the
|
||||
* reference remains stable across community updates.
|
||||
*/
|
||||
export function useCommunityBookmarks() {
|
||||
const { nostr } = useNostr();
|
||||
const { user } = useCurrentUser();
|
||||
const queryClient = useQueryClient();
|
||||
const { mutateAsync: publishEvent } = useNostrPublish();
|
||||
|
||||
// Query the user's communities list (kind 10004 — replaceable event)
|
||||
const listQuery = useQuery({
|
||||
queryKey: ['community-bookmarks', user?.pubkey],
|
||||
queryFn: async () => {
|
||||
if (!user) return null;
|
||||
const events = await nostr.query([{
|
||||
kinds: [COMMUNITIES_LIST_KIND],
|
||||
authors: [user.pubkey],
|
||||
limit: 1,
|
||||
}]);
|
||||
return events[0] ?? null;
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
// Extract bookmarked community a-tags (only `34550:` coordinates)
|
||||
const bookmarkedATags: string[] = (listQuery.data?.tags ?? [])
|
||||
.filter(([name, value]) =>
|
||||
name === 'a' && typeof value === 'string' && value.startsWith(`${COMMUNITY_DEFINITION_KIND}:`),
|
||||
)
|
||||
.map(([, value]) => value);
|
||||
|
||||
/** Check if a community `a` tag coordinate is bookmarked. */
|
||||
function isBookmarked(aTag: string): boolean {
|
||||
return bookmarkedATags.includes(aTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle bookmark for a given community coordinate.
|
||||
* `aTag` is expected to be a `34550:<pubkey>:<d-tag>` string.
|
||||
* `relayHint` is optional — appended to the tag per NIP-51 when provided.
|
||||
*/
|
||||
const toggleBookmark = useMutation({
|
||||
mutationFn: async ({ aTag, relayHint }: { aTag: string; relayHint?: string }) => {
|
||||
if (!user) throw new Error('User is not logged in');
|
||||
|
||||
// Fetch the freshest kind 10004 from relays before mutating
|
||||
const prev = await fetchFreshEvent(nostr, {
|
||||
kinds: [COMMUNITIES_LIST_KIND],
|
||||
authors: [user.pubkey],
|
||||
});
|
||||
|
||||
const currentTags = prev?.tags ?? [];
|
||||
const currentlyBookmarked = currentTags.some(
|
||||
([name, value]) => name === 'a' && value === aTag,
|
||||
);
|
||||
|
||||
let newTags: string[][];
|
||||
|
||||
if (currentlyBookmarked) {
|
||||
// Remove all matching a-tags for this coordinate
|
||||
newTags = currentTags.filter(
|
||||
([name, value]) => !(name === 'a' && value === aTag),
|
||||
);
|
||||
} else {
|
||||
// Append the new bookmark per NIP-51 recommendation
|
||||
const newTag: string[] = relayHint ? ['a', aTag, relayHint] : ['a', aTag];
|
||||
newTags = [...currentTags, newTag];
|
||||
}
|
||||
|
||||
await publishEvent({
|
||||
kind: COMMUNITIES_LIST_KIND,
|
||||
content: prev?.content ?? '',
|
||||
tags: newTags,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
prev: prev ?? undefined,
|
||||
});
|
||||
|
||||
// Return whether this was a remove or add so onSuccess can pick the
|
||||
// right toast wording. Callbacks live on the mutation (not per-call)
|
||||
// so they still fire when the triggering UI (e.g. a dialog) unmounts
|
||||
// before the publish resolves.
|
||||
return { removed: currentlyBookmarked };
|
||||
},
|
||||
onSuccess: ({ removed }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['community-bookmarks', user?.pubkey] });
|
||||
queryClient.invalidateQueries({ queryKey: ['my-communities'] });
|
||||
toast({
|
||||
title: removed ? 'Community removed from bookmarks' : 'Community bookmarked',
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: 'Failed to update bookmark',
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
/** The kind 10004 list event itself. */
|
||||
listEvent: listQuery.data,
|
||||
/** Array of bookmarked community `a` tag coordinates. */
|
||||
bookmarkedATags,
|
||||
/** Whether the list query is still loading. */
|
||||
isLoading: listQuery.isLoading,
|
||||
/** Check whether a given `a` tag coordinate is bookmarked. */
|
||||
isBookmarked,
|
||||
/** Toggle a community bookmark on/off. */
|
||||
toggleBookmark,
|
||||
};
|
||||
}
|
||||
+133
-17
@@ -3,6 +3,7 @@ import { useNostr } from '@nostrify/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useCurrentUser } from './useCurrentUser';
|
||||
import { COMMUNITIES_LIST_KIND } from './useCommunityBookmarks';
|
||||
import {
|
||||
COMMUNITY_DEFINITION_KIND,
|
||||
BADGE_AWARD_KIND,
|
||||
@@ -17,15 +18,26 @@ export interface MyCommunityEntry {
|
||||
event: NostrEvent;
|
||||
/** Whether the current user is the founder. */
|
||||
isFounded: boolean;
|
||||
/** Whether the current user is a validated (chain-derived) member. */
|
||||
isMember: boolean;
|
||||
/** Whether the current user has bookmarked the community via kind 10004. */
|
||||
isBookmarked: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch communities the logged-in user has founded or been recruited into.
|
||||
* Fetch communities the logged-in user has founded, been recruited into,
|
||||
* or bookmarked via their NIP-51 Communities list (kind 10004).
|
||||
*
|
||||
* Discovery follows the NIP:
|
||||
* 1. Founded: `{ kinds: [34550], authors: [<user-pubkey>] }`
|
||||
* 2. Member-of: query kind 8 awards targeting the user, extract badge `a` tags,
|
||||
* Discovery:
|
||||
*
|
||||
* 1. Founded -- `{ kinds: [34550], authors: [user.pubkey] }`
|
||||
* 2. Member-of -- kind 8 awards targeting the user, extract badge `a` tags,
|
||||
* then find the community definitions referencing those badges.
|
||||
* 3. Bookmarked -- read kind 10004 authored by user, extract `a` tags
|
||||
* pointing at kind 34550 events, and fetch those community definitions.
|
||||
*
|
||||
* Priority when the same community appears in multiple sources:
|
||||
* founded > member > bookmarked.
|
||||
*/
|
||||
export function useMyCommunities() {
|
||||
const { nostr } = useNostr();
|
||||
@@ -39,17 +51,27 @@ export function useMyCommunities() {
|
||||
const timeout = AbortSignal.timeout(10_000);
|
||||
const combinedSignal = AbortSignal.any([signal, timeout]);
|
||||
|
||||
// Step 1: Communities founded by the user
|
||||
// ── Step 1: Communities founded by the user ───────────────────────────
|
||||
const foundedEvents = await nostr.query(
|
||||
[{ kinds: [COMMUNITY_DEFINITION_KIND], authors: [user.pubkey], limit: 50 }],
|
||||
{ signal: combinedSignal },
|
||||
);
|
||||
|
||||
// Step 2: Badge awards targeting the user
|
||||
const awards = await nostr.query(
|
||||
[{ kinds: [BADGE_AWARD_KIND], '#p': [user.pubkey], limit: 200 }],
|
||||
{ signal: combinedSignal },
|
||||
);
|
||||
// ── Step 2: Badge awards targeting the user + Bookmarks list ──────────
|
||||
//
|
||||
// Batched into a single relay round-trip. The kind 10004 list is a
|
||||
// replaceable event, so pulling it here keeps the read path tight and
|
||||
// reuses the same connection.
|
||||
const [awards, bookmarkListEvents] = await Promise.all([
|
||||
nostr.query(
|
||||
[{ kinds: [BADGE_AWARD_KIND], '#p': [user.pubkey], limit: 200 }],
|
||||
{ signal: combinedSignal },
|
||||
),
|
||||
nostr.query(
|
||||
[{ kinds: [COMMUNITIES_LIST_KIND], authors: [user.pubkey], limit: 1 }],
|
||||
{ signal: combinedSignal },
|
||||
),
|
||||
]);
|
||||
|
||||
// Extract badge a-tag coordinates from awards
|
||||
const badgeATags = new Set<string>();
|
||||
@@ -70,26 +92,120 @@ export function useMyCommunities() {
|
||||
);
|
||||
}
|
||||
|
||||
// Merge and deduplicate (founded takes priority)
|
||||
// ── Step 4: Resolve bookmarked community coordinates ──────────────────
|
||||
//
|
||||
// NIP-51 kind 10004 stores community definitions as `a` tags like
|
||||
// `34550:<pubkey>:<d-tag>`. For each bookmarked coordinate we query
|
||||
// with both `authors` and `#d` so relays return a single authentic
|
||||
// event per bookmark (per AGENTS.md security guidance on addressable
|
||||
// events).
|
||||
//
|
||||
// Multiple coordinates with the same author are grouped to minimise
|
||||
// the number of relay queries while keeping the author filter intact.
|
||||
|
||||
const bookmarkListEvent = bookmarkListEvents[0];
|
||||
const bookmarkedCoords: string[] = (bookmarkListEvent?.tags ?? [])
|
||||
.filter(([n, v]) =>
|
||||
n === 'a'
|
||||
&& typeof v === 'string'
|
||||
&& v.startsWith(`${COMMUNITY_DEFINITION_KIND}:`),
|
||||
)
|
||||
.map(([, v]) => v);
|
||||
|
||||
// Group bookmarked coords by author pubkey: author -> Set<d-tag>
|
||||
const coordsByAuthor = new Map<string, Set<string>>();
|
||||
for (const coord of bookmarkedCoords) {
|
||||
// Format: "34550:<pubkey>:<d-tag>" -- d-tag may itself contain ":"
|
||||
const firstColon = coord.indexOf(':');
|
||||
const secondColon = coord.indexOf(':', firstColon + 1);
|
||||
if (firstColon === -1 || secondColon === -1) continue;
|
||||
const authorPubkey = coord.slice(firstColon + 1, secondColon);
|
||||
const dTag = coord.slice(secondColon + 1);
|
||||
if (!authorPubkey || !dTag) continue;
|
||||
const existing = coordsByAuthor.get(authorPubkey);
|
||||
if (existing) {
|
||||
existing.add(dTag);
|
||||
} else {
|
||||
coordsByAuthor.set(authorPubkey, new Set([dTag]));
|
||||
}
|
||||
}
|
||||
|
||||
let bookmarkedCommunityEvents: NostrEvent[] = [];
|
||||
if (coordsByAuthor.size > 0) {
|
||||
const bookmarkQueries = await Promise.all(
|
||||
Array.from(coordsByAuthor.entries()).map(([authorPubkey, dTags]) =>
|
||||
nostr.query(
|
||||
[{
|
||||
kinds: [COMMUNITY_DEFINITION_KIND],
|
||||
authors: [authorPubkey],
|
||||
'#d': [...dTags],
|
||||
limit: dTags.size,
|
||||
}],
|
||||
{ signal: combinedSignal },
|
||||
),
|
||||
),
|
||||
);
|
||||
bookmarkedCommunityEvents = bookmarkQueries.flat();
|
||||
}
|
||||
|
||||
const bookmarkedATagSet = new Set(bookmarkedCoords);
|
||||
|
||||
// ── Merge & deduplicate ──────────────────────────────────────────────
|
||||
// Priority: founded > member > bookmarked. `isBookmarked` is resolved
|
||||
// from the bookmark list irrespective of which bucket produced the
|
||||
// entry, so founders/members who have also bookmarked see both flags.
|
||||
const seen = new Map<string, MyCommunityEntry>();
|
||||
|
||||
for (const event of foundedEvents) {
|
||||
const community = parseCommunityEvent(event);
|
||||
if (!community) continue;
|
||||
seen.set(community.aTag, { community, event, isFounded: true });
|
||||
seen.set(community.aTag, {
|
||||
community,
|
||||
event,
|
||||
isFounded: true,
|
||||
isMember: false,
|
||||
isBookmarked: bookmarkedATagSet.has(community.aTag),
|
||||
});
|
||||
}
|
||||
|
||||
for (const event of memberCommunityEvents) {
|
||||
const community = parseCommunityEvent(event);
|
||||
if (!community) continue;
|
||||
if (!seen.has(community.aTag)) {
|
||||
seen.set(community.aTag, { community, event, isFounded: false });
|
||||
}
|
||||
if (seen.has(community.aTag)) continue;
|
||||
seen.set(community.aTag, {
|
||||
community,
|
||||
event,
|
||||
isFounded: false,
|
||||
isMember: true,
|
||||
isBookmarked: bookmarkedATagSet.has(community.aTag),
|
||||
});
|
||||
}
|
||||
|
||||
// Sort: founded first, then by created_at descending
|
||||
for (const event of bookmarkedCommunityEvents) {
|
||||
const community = parseCommunityEvent(event);
|
||||
if (!community) continue;
|
||||
if (!bookmarkedATagSet.has(community.aTag)) continue;
|
||||
if (seen.has(community.aTag)) continue;
|
||||
seen.set(community.aTag, {
|
||||
community,
|
||||
event,
|
||||
isFounded: false,
|
||||
isMember: false,
|
||||
isBookmarked: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort: founded first, then member, then bookmarked-only;
|
||||
// tie-break by created_at descending.
|
||||
const sortRank = (entry: MyCommunityEntry): number => {
|
||||
if (entry.isFounded) return 0;
|
||||
if (entry.isMember) return 1;
|
||||
return 2;
|
||||
};
|
||||
|
||||
return Array.from(seen.values()).sort((a, b) => {
|
||||
if (a.isFounded !== b.isFounded) return a.isFounded ? -1 : 1;
|
||||
const rankDiff = sortRank(a) - sortRank(b);
|
||||
if (rankDiff !== 0) return rankDiff;
|
||||
return b.event.created_at - a.event.created_at;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -215,6 +215,8 @@ function MyCommunitiesContent() {
|
||||
key={entry.community.aTag}
|
||||
event={entry.event}
|
||||
isFounded={entry.isFounded}
|
||||
isMember={entry.isMember}
|
||||
isBookmarked={entry.isBookmarked}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user