Website hub for the atmosphere community, aggregating posts, events, regional, and more
0

Configure Feed

Select the types of activity you want to include in your feed.

Cleanup even more

+270 -263
+11 -27
src/features/community-membership/action.ts
··· 4 4 import { getAtmosphereCommunityDid } from "../../lib/community/atmosphere"; 5 5 import { resolveHandleToDid } from "../../lib/community/identity"; 6 6 import { OpenSocialCommunityError } from "../../lib/opensocial/client"; 7 + import { isPermissionError } from "../../lib/action-result"; 7 8 import { runJoin, runLeave } from "./mutations"; 8 9 import { getJoinErrorMessage, type JoinOutcomeCode } from "./notice"; 9 - 10 - interface AtmosphereJoinResult { 11 - outcome: JoinOutcomeCode; 12 - } 13 - 14 - interface ListingJoinResult { 15 - outcome: JoinOutcomeCode; 16 - community: string; 17 - } 18 10 19 11 export const membershipActions = { 20 12 joinAtmosphereCommunity: defineAction({ 21 13 accept: "form", 22 - handler: async (_input, ctx): Promise<AtmosphereJoinResult> => { 14 + handler: async (_input, ctx): Promise<{ outcome: JoinOutcomeCode }> => { 23 15 const loggedInUser = ctx.locals.loggedInUser; 24 16 if (!loggedInUser) { 25 17 throw new ActionError({ ··· 43 35 joinOpenSocialCommunity: defineAction({ 44 36 accept: "form", 45 37 input: z.object({ handle: z.string().min(1) }), 46 - handler: async (input, ctx): Promise<ListingJoinResult> => { 38 + handler: async ( 39 + input, 40 + ctx, 41 + ): Promise<{ outcome: JoinOutcomeCode; community: string }> => { 47 42 const loggedInUser = ctx.locals.loggedInUser; 48 43 if (!loggedInUser) { 49 44 throw new ActionError({ ··· 74 69 leaveOpenSocialCommunity: defineAction({ 75 70 accept: "form", 76 71 input: z.object({ handle: z.string().min(1) }), 77 - handler: async (input, ctx): Promise<ListingJoinResult> => { 72 + handler: async ( 73 + input, 74 + ctx, 75 + ): Promise<{ outcome: JoinOutcomeCode; community: string }> => { 78 76 const loggedInUser = ctx.locals.loggedInUser; 79 77 if (!loggedInUser) { 80 78 throw new ActionError({ ··· 104 102 105 103 leaveAtmosphereCommunity: defineAction({ 106 104 accept: "form", 107 - handler: async (_input, ctx): Promise<AtmosphereJoinResult> => { 105 + handler: async (_input, ctx): Promise<{ outcome: JoinOutcomeCode }> => { 108 106 const loggedInUser = ctx.locals.loggedInUser; 109 107 if (!loggedInUser) { 110 108 throw new ActionError({ ··· 153 151 message: getJoinErrorMessage("INTERNAL_SERVER_ERROR"), 154 152 }); 155 153 } 156 - 157 - function isPermissionError(error: unknown): boolean { 158 - const maybeError = error as { 159 - status?: number; 160 - error?: string; 161 - message?: string; 162 - }; 163 - const text = `${maybeError.error ?? ""} ${maybeError.message ?? ""}`.toLowerCase(); 164 - return ( 165 - maybeError.status === 401 || 166 - maybeError.status === 403 || 167 - text.includes("scope") 168 - ); 169 - }
+13 -29
src/features/community-membership/notice.ts
··· 1 + import { 2 + getActionErrorNotice, 3 + type ActionResultLike, 4 + } from "../../lib/action-result"; 5 + 1 6 export type JoinOutcomeCode = 2 7 | "ok" 3 8 | "pending" ··· 11 16 message: string; 12 17 } 13 18 14 - type JoinResultLike = { 15 - data?: { 16 - outcome?: JoinOutcomeCode | null; 17 - } | null; 18 - error?: { 19 - code?: string; 20 - message?: string; 21 - } | null; 22 - } | null | undefined; 19 + type JoinResultLike = ActionResultLike<{ 20 + outcome?: JoinOutcomeCode | null; 21 + }>; 23 22 24 23 type JoinActionErrorCode = 25 24 | "UNAUTHORIZED" ··· 76 75 return JOIN_OUTCOME_NOTICE[outcome]; 77 76 } 78 77 79 - return getJoinErrorNotice(result?.error?.code, result?.error?.message); 78 + return getActionErrorNotice( 79 + JOIN_ERROR_NOTICE, 80 + result?.error?.code, 81 + result?.error?.message, 82 + ); 80 83 } 81 84 82 85 export function getJoinErrorMessage(code: JoinActionErrorCode): string { 83 86 return JOIN_ERROR_NOTICE[code].message; 84 87 } 85 - 86 - function getJoinErrorNotice( 87 - code?: string, 88 - message?: string, 89 - ): JoinNotice | null { 90 - if (!code) { 91 - return message ? { tone: "error", message } : null; 92 - } 93 - 94 - const fallback = JOIN_ERROR_NOTICE[code as JoinActionErrorCode]; 95 - if (!fallback) { 96 - return message ? { tone: "error", message } : null; 97 - } 98 - 99 - return { 100 - tone: fallback.tone, 101 - message: message || fallback.message, 102 - }; 103 - }
+54 -20
src/features/community-membership/page.ts
··· 1 1 import { getAtmosphereCommunityDid } from "../../lib/community/atmosphere"; 2 + import { resolveHandleToDid } from "../../lib/community/identity"; 2 3 import { getMembership } from "../../lib/opensocial/membership"; 3 4 import { pickFirstActionResult } from "../../lib/action-result"; 4 5 import { getJoinNotice, type JoinNotice, type JoinOutcomeCode } from "./notice"; 6 + import { type ActionResultLike } from "../../lib/action-result"; 5 7 6 - type JoinResultLike = { 7 - data?: { 8 - outcome?: JoinOutcomeCode | null; 9 - community?: string | null; 10 - } | null; 11 - error?: { 12 - code?: string; 13 - message?: string; 14 - } | null; 15 - } | null | undefined; 8 + type JoinResultLike = ActionResultLike<{ 9 + outcome?: JoinOutcomeCode | null; 10 + community?: string | null; 11 + }>; 16 12 17 13 interface CommunitySummary { 18 14 handle: string; 19 15 name: string; 20 16 } 21 17 22 - interface CommunityListingJoinState { 23 - notice: JoinNotice | null; 24 - communityName?: string; 25 - } 26 - 27 - interface CommunityViewerMembershipState { 28 - isMember: boolean; 29 - isAdmin: boolean; 18 + interface OpenSocialCommunitySummary { 19 + handle: string; 20 + isOpenSocialCommunity?: boolean; 30 21 } 31 22 32 23 export function getCommunityListingJoinState( ··· 39 30 joinResult: JoinResultLike; 40 31 leaveResult: JoinResultLike; 41 32 }, 42 - ): CommunityListingJoinState { 33 + ) { 43 34 const result = pickFirstActionResult({ 44 35 items: [joinResult, leaveResult], 45 36 hasMeaningfulData: (data) => Boolean(data?.outcome), ··· 69 60 70 61 export async function getAtmosphereViewerMembershipState( 71 62 loggedInUser: App.Locals["loggedInUser"], 72 - ): Promise<CommunityViewerMembershipState> { 63 + ) { 73 64 if (!loggedInUser) { 74 65 return { 75 66 isMember: false, ··· 95 86 }; 96 87 } 97 88 } 89 + 90 + export async function getCommunityListingMembershipState( 91 + loggedInUser: App.Locals["loggedInUser"], 92 + communities: OpenSocialCommunitySummary[], 93 + ) { 94 + const memberHandles = new Set<string>(); 95 + const adminHandles = new Set<string>(); 96 + 97 + if (!loggedInUser) { 98 + return { memberHandles, adminHandles }; 99 + } 100 + 101 + const results = await Promise.allSettled( 102 + communities 103 + .filter((community) => community.isOpenSocialCommunity) 104 + .map(async (community) => { 105 + const communityDid = await resolveHandleToDid(community.handle); 106 + const membership = await getMembership({ 107 + communityDid, 108 + userDid: loggedInUser.did, 109 + }); 110 + return { 111 + handle: community.handle, 112 + isMember: membership.isMember, 113 + isAdmin: membership.isAdmin, 114 + }; 115 + }), 116 + ); 117 + 118 + for (const result of results) { 119 + if (result.status !== "fulfilled") { 120 + continue; 121 + } 122 + if (result.value.isMember) { 123 + memberHandles.add(result.value.handle); 124 + } 125 + if (result.value.isAdmin) { 126 + adminHandles.add(result.value.handle); 127 + } 128 + } 129 + 130 + return { memberHandles, adminHandles }; 131 + }
+84 -55
src/features/community-sharing/action.ts
··· 7 7 getRepoRecordByUri, 8 8 getShareCandidateByUri, 9 9 } from "../../lib/community/share-candidates"; 10 - import { parseSharedDocumentRef } from "../../lib/community/shared-content"; 10 + import { 11 + parseSharedDocumentRef, 12 + type SharedDocumentRef, 13 + } from "../../lib/community/shared-content"; 11 14 import { 12 15 SHARED_CONTENT_COLLECTION, 13 16 shareContentWithCommunity, ··· 15 18 } from "../../lib/opensocial/content-sharing"; 16 19 import { OpenSocialCommunityError } from "../../lib/opensocial/client"; 17 20 import { getMembership } from "../../lib/opensocial/membership"; 21 + import { isPermissionError } from "../../lib/action-result"; 18 22 import { 19 23 getShareErrorMessage, 20 24 type ShareOutcomeCode, 21 25 type UnshareOutcomeCode, 22 26 } from "./notice"; 23 27 24 - function parseSharedContentRecordUri( 25 - uri: string, 26 - communityDid: string, 27 - ): AtUri | null { 28 + type LoggedInUser = NonNullable<App.Locals["loggedInUser"]>; 29 + 30 + function parseSharedContentRecordUri({ 31 + uri, 32 + communityDid, 33 + }: { 34 + uri: string; 35 + communityDid: string; 36 + }): AtUri | null { 28 37 try { 29 38 const parsed = new AtUri(uri); 30 39 return parsed.host === communityDid && ··· 37 46 } 38 47 } 39 48 40 - function isPermissionError(error: unknown): boolean { 41 - const maybeError = error as { 42 - status?: number; 43 - error?: string; 44 - message?: string; 45 - }; 46 - const text = `${maybeError.error ?? ""} ${maybeError.message ?? ""}`.toLowerCase(); 47 - return ( 48 - maybeError.status === 401 || 49 - maybeError.status === 403 || 50 - text.includes("scope") 49 + async function loadSharedDocumentRef({ 50 + shareRecordUri, 51 + communityDid, 52 + shareRecordRkey, 53 + }: { 54 + shareRecordUri: string; 55 + communityDid: string; 56 + shareRecordRkey: string; 57 + }): Promise<SharedDocumentRef> { 58 + const response = await getRepoRecordByUri(shareRecordUri); 59 + if (!response || typeof response.value !== "object" || response.value === null) { 60 + throw new ActionError({ 61 + code: "NOT_FOUND", 62 + message: "That item couldn't be removed. Refresh the page and try again.", 63 + }); 64 + } 65 + 66 + const sharedRecord = parseSharedDocumentRef( 67 + response.value as Record<string, unknown>, 68 + { 69 + source: communityDid, 70 + shareRecordUri: response.uri, 71 + shareRecordRkey, 72 + }, 51 73 ); 74 + if (!sharedRecord) { 75 + throw new ActionError({ 76 + code: "NOT_FOUND", 77 + message: "That item couldn't be removed. Refresh the page and try again.", 78 + }); 79 + } 80 + 81 + return sharedRecord; 82 + } 83 + 84 + async function assertCanUnshare({ 85 + sharedRecord, 86 + loggedInUser, 87 + communityDid, 88 + }: { 89 + sharedRecord: SharedDocumentRef; 90 + loggedInUser: LoggedInUser; 91 + communityDid: string; 92 + }): Promise<void> { 93 + if (sharedRecord.sharedBy === loggedInUser.did) { 94 + return; 95 + } 96 + 97 + const membership = await getMembership({ 98 + communityDid, 99 + userDid: loggedInUser.did, 100 + }); 101 + if (!membership.isAdmin) { 102 + throw new ActionError({ 103 + code: "FORBIDDEN", 104 + message: "Only the original sharer can remove that item.", 105 + }); 106 + } 52 107 } 53 108 54 109 export const sharingActions = { ··· 141 196 142 197 try { 143 198 const communityDid = await getAtmosphereCommunityDid(); 144 - const parsedShareRecordUri = parseSharedContentRecordUri( 145 - input.shareRecordUri, 199 + const parsedShareRecordUri = parseSharedContentRecordUri({ 200 + uri: input.shareRecordUri, 146 201 communityDid, 147 - ); 202 + }); 148 203 if (!parsedShareRecordUri) { 149 204 throw new ActionError({ 150 205 code: "BAD_REQUEST", ··· 152 207 }); 153 208 } 154 209 155 - const response = await getRepoRecordByUri(input.shareRecordUri); 156 - if (!response || typeof response.value !== "object" || response.value === null) { 157 - throw new ActionError({ 158 - code: "NOT_FOUND", 159 - message: "That item couldn't be removed. Refresh the page and try again.", 160 - }); 161 - } 162 - 163 - const sharedRecord = parseSharedDocumentRef( 164 - response.value as Record<string, unknown>, 165 - { 166 - source: communityDid, 167 - shareRecordUri: response.uri, 168 - shareRecordRkey: parsedShareRecordUri.rkey, 169 - }, 170 - ); 171 - if (!sharedRecord) { 172 - throw new ActionError({ 173 - code: "NOT_FOUND", 174 - message: "That item couldn't be removed. Refresh the page and try again.", 175 - }); 176 - } 177 - 178 - const isOriginalSharer = sharedRecord.sharedBy === loggedInUser.did; 179 - if (!isOriginalSharer) { 180 - const membership = await getMembership({ 181 - communityDid, 182 - userDid: loggedInUser.did, 183 - }); 184 - if (!membership.isAdmin) { 185 - throw new ActionError({ 186 - code: "FORBIDDEN", 187 - message: "Only the original sharer can remove that item.", 188 - }); 189 - } 190 - } 210 + const sharedRecord = await loadSharedDocumentRef({ 211 + shareRecordUri: input.shareRecordUri, 212 + communityDid, 213 + shareRecordRkey: parsedShareRecordUri.rkey, 214 + }); 215 + await assertCanUnshare({ 216 + sharedRecord, 217 + loggedInUser, 218 + communityDid, 219 + }); 191 220 192 221 await unshareContentWithCommunity({ 193 222 communityDid,
+15 -38
src/features/community-sharing/notice.ts
··· 1 1 import { pickFirstActionResult } from "../../lib/action-result"; 2 + import { 3 + getActionErrorNotice, 4 + type ActionResultLike, 5 + } from "../../lib/action-result"; 2 6 3 7 export type ShareOutcomeCode = "ok" | "not-member"; 4 8 ··· 9 13 message: string; 10 14 } 11 15 12 - type ShareResultLike = { 13 - data?: { 14 - outcome?: ShareOutcomeCode | null; 15 - } | null; 16 - error?: { 17 - code?: string; 18 - message?: string; 19 - } | null; 20 - } | null | undefined; 16 + type ShareResultLike = ActionResultLike<{ 17 + outcome?: ShareOutcomeCode | null; 18 + }>; 21 19 22 - type UnshareResultLike = { 23 - data?: { 24 - outcome?: UnshareOutcomeCode | null; 25 - } | null; 26 - error?: { 27 - code?: string; 28 - message?: string; 29 - } | null; 30 - } | null | undefined; 20 + type UnshareResultLike = ActionResultLike<{ 21 + outcome?: UnshareOutcomeCode | null; 22 + }>; 31 23 32 24 type ShareActionErrorCode = 33 25 | "UNAUTHORIZED" ··· 94 86 return UNSHARE_OUTCOME_NOTICE[result.data.outcome]; 95 87 } 96 88 97 - return getShareErrorNotice(result?.error?.code, result?.error?.message); 89 + return getActionErrorNotice( 90 + SHARE_ERROR_NOTICE, 91 + result?.error?.code, 92 + result?.error?.message, 93 + ); 98 94 } 99 95 100 96 export function getShareErrorMessage(code: ShareActionErrorCode): string { 101 97 return SHARE_ERROR_NOTICE[code].message; 102 98 } 103 - 104 - function getShareErrorNotice( 105 - code?: string, 106 - message?: string, 107 - ): ShareNotice | null { 108 - if (!code) { 109 - return message ? { tone: "error", message } : null; 110 - } 111 - 112 - const fallback = SHARE_ERROR_NOTICE[code as ShareActionErrorCode]; 113 - if (!fallback) { 114 - return message ? { tone: "error", message } : null; 115 - } 116 - 117 - return { 118 - tone: fallback.tone, 119 - message: message || fallback.message, 120 - }; 121 - }
+1 -15
src/features/community-sharing/page.ts
··· 2 2 3 3 import { getShareCandidates } from "../../lib/community/share-candidates"; 4 4 5 - type ShareCandidates = Awaited<ReturnType<typeof getShareCandidates>>; 6 - 7 - interface ShareSourceProfile { 8 - displayName?: string; 9 - avatar?: string; 10 - } 11 - 12 - interface SharePanelState { 13 - shareRepo: string; 14 - shareSourceProfile: ShareSourceProfile | null; 15 - shareCandidates: ShareCandidates | null; 16 - shareCandidatesError: boolean; 17 - } 18 - 19 5 export async function getCommunitySharePanelState({ 20 6 sourceParam, 21 7 fallbackRepo, ··· 24 10 sourceParam: string | null | undefined; 25 11 fallbackRepo: string | null | undefined; 26 12 canShareContent: boolean; 27 - }): Promise<SharePanelState> { 13 + }) { 28 14 const shareRepo = sourceParam?.trim() || fallbackRepo || ""; 29 15 if (!shareRepo || !canShareContent) { 30 16 return {
+8 -15
src/features/event-rsvp/action.ts
··· 6 6 RSVP_STATUS_GOING, 7 7 RSVP_STATUS_NOT_GOING, 8 8 setRsvpStatus, 9 + type RsvpSubjectRef, 9 10 type RsvpStatus, 10 11 } from "./data"; 12 + import { isPermissionError } from "../../lib/action-result"; 11 13 import { getRsvpErrorMessage, type RsvpOutcomeCode } from "./notice"; 12 14 13 15 const EVENT_COLLECTION = "community.lexicon.calendar.event"; ··· 30 32 notgoing: RSVP_STATUS_NOT_GOING, 31 33 }; 32 34 33 - function isPermissionError(error: unknown): boolean { 34 - const maybeError = error as { 35 - status?: number; 36 - error?: string; 37 - message?: string; 38 - }; 39 - const text = `${maybeError.error ?? ""} ${maybeError.message ?? ""}`.toLowerCase(); 40 - return ( 41 - maybeError.status === 401 || 42 - maybeError.status === 403 || 43 - text.includes("scope") 44 - ); 45 - } 46 - 47 35 export const rsvpActions = { 48 36 rsvpEvent: defineAction({ 49 37 accept: "form", ··· 74 62 } 75 63 76 64 try { 65 + const subject: RsvpSubjectRef = { 66 + uri: input.eventUri, 67 + cid: input.eventCid, 68 + }; 69 + 77 70 await setRsvpStatus( 78 71 loggedInUser, 79 - { uri: input.eventUri, cid: input.eventCid }, 72 + subject, 80 73 FORM_STATUS_TO_RSVP_STATUS[input.status], 81 74 ); 82 75 } catch (error) {
+8 -3
src/features/event-rsvp/data.ts
··· 12 12 | typeof RSVP_STATUS_INTERESTED 13 13 | typeof RSVP_STATUS_NOT_GOING; 14 14 15 + export interface RsvpSubjectRef { 16 + uri: string; 17 + cid: string; 18 + } 19 + 15 20 export interface CalendarRsvpRecord { 16 21 $type: "community.lexicon.calendar.rsvp"; 17 - subject: { uri: string; cid: string }; 22 + subject: RsvpSubjectRef; 18 23 status: RsvpStatus; 19 24 createdAt: string; 20 25 } ··· 26 31 record: CalendarRsvpRecord; 27 32 } 28 33 29 - export type LoggedInUser = NonNullable<App.Locals["loggedInUser"]>; 34 + type LoggedInUser = NonNullable<App.Locals["loggedInUser"]>; 30 35 31 36 interface EventLike { 32 37 uri: string; ··· 113 118 114 119 export async function setRsvpStatus( 115 120 loggedInUser: LoggedInUser, 116 - subject: { uri: string; cid: string }, 121 + subject: RsvpSubjectRef, 117 122 status: RsvpStatus, 118 123 ): Promise<void> { 119 124 const agent = await getLoggedInAgent(loggedInUser);
+14 -30
src/features/event-rsvp/notice.ts
··· 1 + import { 2 + getActionErrorNotice, 3 + type ActionResultLike, 4 + } from "../../lib/action-result"; 5 + 1 6 export type RsvpOutcomeCode = "going" | "notgoing"; 2 7 3 8 export interface RsvpNotice { ··· 5 10 message: string; 6 11 } 7 12 8 - type RsvpResultLike = { 9 - data?: { 10 - outcome?: RsvpOutcomeCode | null; 11 - eventName?: string | null; 12 - } | null; 13 - error?: { 14 - code?: string; 15 - message?: string; 16 - } | null; 17 - } | null | undefined; 13 + type RsvpResultLike = ActionResultLike<{ 14 + outcome?: RsvpOutcomeCode | null; 15 + eventName?: string | null; 16 + }>; 18 17 19 18 type RsvpActionErrorCode = 20 19 | "UNAUTHORIZED" ··· 79 78 }; 80 79 } 81 80 82 - return getRsvpErrorNotice(result?.error?.code, result?.error?.message); 81 + return getActionErrorNotice( 82 + RSVP_ERROR_NOTICE, 83 + result?.error?.code, 84 + result?.error?.message, 85 + ); 83 86 } 84 87 85 88 export function getRsvpErrorMessage(code: RsvpActionErrorCode): string { 86 89 return RSVP_ERROR_NOTICE[code].message; 87 90 } 88 - 89 - function getRsvpErrorNotice( 90 - code?: string, 91 - message?: string, 92 - ): RsvpNotice | null { 93 - if (!code) { 94 - return message ? { tone: "error", message } : null; 95 - } 96 - 97 - const fallback = RSVP_ERROR_NOTICE[code as RsvpActionErrorCode]; 98 - if (!fallback) { 99 - return message ? { tone: "error", message } : null; 100 - } 101 - 102 - return { 103 - tone: fallback.tone, 104 - message: message || fallback.message, 105 - }; 106 - }
+56
src/lib/action-result.ts
··· 33 33 hasMeaningfulData(item?.data) || item?.error ? item : null, 34 34 }); 35 35 } 36 + 37 + export type ActionResultLike<TData extends object> = { 38 + data?: TData | null; 39 + error?: { 40 + code?: string; 41 + message?: string; 42 + } | null; 43 + } | null | undefined; 44 + 45 + interface NoticeMessage { 46 + tone: "success" | "info" | "neutral" | "error"; 47 + message: string; 48 + } 49 + 50 + export function getActionErrorNotice< 51 + TCode extends string, 52 + TNotice extends NoticeMessage, 53 + >( 54 + notices: Record<TCode, TNotice>, 55 + code?: string, 56 + message?: string, 57 + ): TNotice | { tone: "error"; message: string } | null { 58 + if (!code) { 59 + return message ? { tone: "error", message } : null; 60 + } 61 + 62 + const fallback = notices[code as TCode]; 63 + if (!fallback) { 64 + return message ? { tone: "error", message } : null; 65 + } 66 + 67 + return { 68 + ...fallback, 69 + message: message || fallback.message, 70 + }; 71 + } 72 + 73 + export function isPermissionError(error: unknown): boolean { 74 + if (typeof error !== "object" || error === null) { 75 + return false; 76 + } 77 + 78 + const maybeError = error as { 79 + status?: number; 80 + error?: string; 81 + message?: string; 82 + }; 83 + const text = 84 + `${maybeError.error ?? ""} ${maybeError.message ?? ""}`.toLowerCase(); 85 + 86 + return ( 87 + maybeError.status === 401 || 88 + maybeError.status === 403 || 89 + text.includes("scope") 90 + ); 91 + }
+6 -31
src/pages/communities.astro
··· 6 6 import CommunityCard from "../components/communities/CommunityCard.astro"; 7 7 import { getLiveCollection } from "astro:content"; 8 8 import { actions } from "astro:actions"; 9 - import { getMembership } from "../lib/opensocial/membership"; 10 - import { resolveHandleToDid } from "../lib/community/identity"; 11 - import { getCommunityListingJoinState } from "../features/community-membership/page"; 9 + import { 10 + getCommunityListingJoinState, 11 + getCommunityListingMembershipState, 12 + } from "../features/community-membership/page"; 12 13 13 14 const communitiesResult = await getLiveCollection("communities"); 14 15 const communities = communitiesResult.entries?.map((entry) => entry.data) ?? []; ··· 22 23 23 24 const loggedInUser = Astro.locals.loggedInUser; 24 25 25 - // Resolve which opensocial communities the signed-in viewer already belongs to, 26 - // so members see a "Joined" badge instead of a redundant Join button. Only 27 - // opensocial communities are probed (others have no join endpoint), and each 28 - // lookup is isolated: a failed probe just falls back to showing Join. 29 - const memberHandles = new Set<string>(); 30 - const adminHandles = new Set<string>(); 31 - if (loggedInUser) { 32 - const openSocial = communities.filter((c) => c.isOpenSocialCommunity); 33 - const results = await Promise.allSettled( 34 - openSocial.map(async (c) => { 35 - const communityDid = await resolveHandleToDid(c.handle); 36 - const membership = await getMembership({ 37 - communityDid, 38 - userDid: loggedInUser.did, 39 - }); 40 - return { 41 - handle: c.handle, 42 - isMember: membership.isMember, 43 - isAdmin: membership.isAdmin, 44 - }; 45 - }), 46 - ); 47 - for (const result of results) { 48 - if (result.status !== "fulfilled") continue; 49 - if (result.value.isMember) memberHandles.add(result.value.handle); 50 - if (result.value.isAdmin) adminHandles.add(result.value.handle); 51 - } 52 - } 26 + const { memberHandles, adminHandles } = 27 + await getCommunityListingMembershipState(loggedInUser, communities); 53 28 54 29 const REGION_ORDER = ["North America — USA", "North America — Canada", "South America", "Europe", "Online", "Other"]; 55 30 const grouped = new Map<string, typeof communities>();