src
features
community-membership
community-sharing
event-rsvp
lib
pages
···
6
6
RSVP_STATUS_GOING,
7
7
RSVP_STATUS_NOT_GOING,
8
8
setRsvpStatus,
9
9
+
type RsvpSubjectRef,
9
10
type RsvpStatus,
10
11
} from "./data";
12
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
33
-
function isPermissionError(error: unknown): boolean {
34
34
-
const maybeError = error as {
35
35
-
status?: number;
36
36
-
error?: string;
37
37
-
message?: string;
38
38
-
};
39
39
-
const text = `${maybeError.error ?? ""} ${maybeError.message ?? ""}`.toLowerCase();
40
40
-
return (
41
41
-
maybeError.status === 401 ||
42
42
-
maybeError.status === 403 ||
43
43
-
text.includes("scope")
44
44
-
);
45
45
-
}
46
46
-
47
35
export const rsvpActions = {
48
36
rsvpEvent: defineAction({
49
37
accept: "form",
···
74
62
}
75
63
76
64
try {
65
65
+
const subject: RsvpSubjectRef = {
66
66
+
uri: input.eventUri,
67
67
+
cid: input.eventCid,
68
68
+
};
69
69
+
77
70
await setRsvpStatus(
78
71
loggedInUser,
79
79
-
{ uri: input.eventUri, cid: input.eventCid },
72
72
+
subject,
80
73
FORM_STATUS_TO_RSVP_STATUS[input.status],
81
74
);
82
75
} catch (error) {
···
12
12
| typeof RSVP_STATUS_INTERESTED
13
13
| typeof RSVP_STATUS_NOT_GOING;
14
14
15
15
+
export interface RsvpSubjectRef {
16
16
+
uri: string;
17
17
+
cid: string;
18
18
+
}
19
19
+
15
20
export interface CalendarRsvpRecord {
16
21
$type: "community.lexicon.calendar.rsvp";
17
17
-
subject: { uri: string; cid: string };
22
22
+
subject: RsvpSubjectRef;
18
23
status: RsvpStatus;
19
24
createdAt: string;
20
25
}
···
26
31
record: CalendarRsvpRecord;
27
32
}
28
33
29
29
-
export type LoggedInUser = NonNullable<App.Locals["loggedInUser"]>;
34
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
116
-
subject: { uri: string; cid: string },
121
121
+
subject: RsvpSubjectRef,
117
122
status: RsvpStatus,
118
123
): Promise<void> {
119
124
const agent = await getLoggedInAgent(loggedInUser);
···
1
1
+
import {
2
2
+
getActionErrorNotice,
3
3
+
type ActionResultLike,
4
4
+
} from "../../lib/action-result";
5
5
+
1
6
export type RsvpOutcomeCode = "going" | "notgoing";
2
7
3
8
export interface RsvpNotice {
···
5
10
message: string;
6
11
}
7
12
8
8
-
type RsvpResultLike = {
9
9
-
data?: {
10
10
-
outcome?: RsvpOutcomeCode | null;
11
11
-
eventName?: string | null;
12
12
-
} | null;
13
13
-
error?: {
14
14
-
code?: string;
15
15
-
message?: string;
16
16
-
} | null;
17
17
-
} | null | undefined;
13
13
+
type RsvpResultLike = ActionResultLike<{
14
14
+
outcome?: RsvpOutcomeCode | null;
15
15
+
eventName?: string | null;
16
16
+
}>;
18
17
19
18
type RsvpActionErrorCode =
20
19
| "UNAUTHORIZED"
···
79
78
};
80
79
}
81
80
82
82
-
return getRsvpErrorNotice(result?.error?.code, result?.error?.message);
81
81
+
return getActionErrorNotice(
82
82
+
RSVP_ERROR_NOTICE,
83
83
+
result?.error?.code,
84
84
+
result?.error?.message,
85
85
+
);
83
86
}
84
87
85
88
export function getRsvpErrorMessage(code: RsvpActionErrorCode): string {
86
89
return RSVP_ERROR_NOTICE[code].message;
87
90
}
88
88
-
89
89
-
function getRsvpErrorNotice(
90
90
-
code?: string,
91
91
-
message?: string,
92
92
-
): RsvpNotice | null {
93
93
-
if (!code) {
94
94
-
return message ? { tone: "error", message } : null;
95
95
-
}
96
96
-
97
97
-
const fallback = RSVP_ERROR_NOTICE[code as RsvpActionErrorCode];
98
98
-
if (!fallback) {
99
99
-
return message ? { tone: "error", message } : null;
100
100
-
}
101
101
-
102
102
-
return {
103
103
-
tone: fallback.tone,
104
104
-
message: message || fallback.message,
105
105
-
};
106
106
-
}
···
33
33
hasMeaningfulData(item?.data) || item?.error ? item : null,
34
34
});
35
35
}
36
36
+
37
37
+
export type ActionResultLike<TData extends object> = {
38
38
+
data?: TData | null;
39
39
+
error?: {
40
40
+
code?: string;
41
41
+
message?: string;
42
42
+
} | null;
43
43
+
} | null | undefined;
44
44
+
45
45
+
interface NoticeMessage {
46
46
+
tone: "success" | "info" | "neutral" | "error";
47
47
+
message: string;
48
48
+
}
49
49
+
50
50
+
export function getActionErrorNotice<
51
51
+
TCode extends string,
52
52
+
TNotice extends NoticeMessage,
53
53
+
>(
54
54
+
notices: Record<TCode, TNotice>,
55
55
+
code?: string,
56
56
+
message?: string,
57
57
+
): TNotice | { tone: "error"; message: string } | null {
58
58
+
if (!code) {
59
59
+
return message ? { tone: "error", message } : null;
60
60
+
}
61
61
+
62
62
+
const fallback = notices[code as TCode];
63
63
+
if (!fallback) {
64
64
+
return message ? { tone: "error", message } : null;
65
65
+
}
66
66
+
67
67
+
return {
68
68
+
...fallback,
69
69
+
message: message || fallback.message,
70
70
+
};
71
71
+
}
72
72
+
73
73
+
export function isPermissionError(error: unknown): boolean {
74
74
+
if (typeof error !== "object" || error === null) {
75
75
+
return false;
76
76
+
}
77
77
+
78
78
+
const maybeError = error as {
79
79
+
status?: number;
80
80
+
error?: string;
81
81
+
message?: string;
82
82
+
};
83
83
+
const text =
84
84
+
`${maybeError.error ?? ""} ${maybeError.message ?? ""}`.toLowerCase();
85
85
+
86
86
+
return (
87
87
+
maybeError.status === 401 ||
88
88
+
maybeError.status === 403 ||
89
89
+
text.includes("scope")
90
90
+
);
91
91
+
}
···
6
6
import CommunityCard from "../components/communities/CommunityCard.astro";
7
7
import { getLiveCollection } from "astro:content";
8
8
import { actions } from "astro:actions";
9
9
-
import { getMembership } from "../lib/opensocial/membership";
10
10
-
import { resolveHandleToDid } from "../lib/community/identity";
11
11
-
import { getCommunityListingJoinState } from "../features/community-membership/page";
9
9
+
import {
10
10
+
getCommunityListingJoinState,
11
11
+
getCommunityListingMembershipState,
12
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
25
-
// Resolve which opensocial communities the signed-in viewer already belongs to,
26
26
-
// so members see a "Joined" badge instead of a redundant Join button. Only
27
27
-
// opensocial communities are probed (others have no join endpoint), and each
28
28
-
// lookup is isolated: a failed probe just falls back to showing Join.
29
29
-
const memberHandles = new Set<string>();
30
30
-
const adminHandles = new Set<string>();
31
31
-
if (loggedInUser) {
32
32
-
const openSocial = communities.filter((c) => c.isOpenSocialCommunity);
33
33
-
const results = await Promise.allSettled(
34
34
-
openSocial.map(async (c) => {
35
35
-
const communityDid = await resolveHandleToDid(c.handle);
36
36
-
const membership = await getMembership({
37
37
-
communityDid,
38
38
-
userDid: loggedInUser.did,
39
39
-
});
40
40
-
return {
41
41
-
handle: c.handle,
42
42
-
isMember: membership.isMember,
43
43
-
isAdmin: membership.isAdmin,
44
44
-
};
45
45
-
}),
46
46
-
);
47
47
-
for (const result of results) {
48
48
-
if (result.status !== "fulfilled") continue;
49
49
-
if (result.value.isMember) memberHandles.add(result.value.handle);
50
50
-
if (result.value.isAdmin) adminHandles.add(result.value.handle);
51
51
-
}
52
52
-
}
26
26
+
const { memberHandles, adminHandles } =
27
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>();