···
1
+
import { Loading } from "@/components/primitives/Loading";
2
+
import { Text } from "@/components/primitives/Text";
3
+
import { useFacet } from "@/lib/facet";
4
+
import { lighten } from "@/lib/facet/src/lib/colors";
5
+
import type { AtUri } from "@/lib/types/atproto";
6
+
import { didSchema } from "@/lib/types/atproto";
7
+
import type { SystemsGmstnDevelopmentChannelInvite } from "@/lib/types/lexicon/systems.gmstn.development.channel.invite";
8
+
import { didDocResolver, stringToAtUri } from "@/lib/utils/atproto";
9
+
import { inviteNewUser } from "@/lib/utils/gmstn";
11
+
useOAuthAgentGuaranteed,
12
+
useOAuthSessionGuaranteed,
13
+
} from "@/providers/OAuthProvider";
14
+
import { useCurrentPalette } from "@/providers/ThemeProvider";
15
+
import { getInviteRecordsFromPds } from "@/queries/get-invites-from-pds";
16
+
import { type OAuthSession } from "@atproto/oauth-client";
17
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
18
+
import { format } from "date-fns";
19
+
import { Plus } from "lucide-react-native";
20
+
import { useState, type Dispatch, type SetStateAction } from "react";
21
+
import { FlatList, Pressable, TextInput, View } from "react-native";
23
+
export const InviteUserModalContent = ({
28
+
setShowInviteModal: Dispatch<SetStateAction<boolean>>;
29
+
channelAtUri: string;
32
+
const { semantic } = useCurrentPalette();
33
+
const { atoms, typography } = useFacet();
34
+
const [inputText, setInputText] = useState("");
35
+
const session = useOAuthSessionGuaranteed();
36
+
const agent = useOAuthAgentGuaranteed();
37
+
const queryClient = useQueryClient();
39
+
const { isLoading, data: invites } = useQuery({
40
+
queryKey: ["invites", session.did],
41
+
queryFn: async () => {
42
+
return await invitesQueryFn({ session, channelAtUri });
46
+
const { mutate: inviteUserMutation, isPending: mutationPending } =
48
+
mutationFn: async () => {
53
+
} = didSchema.safeParse(inputText);
54
+
if (!success) throw new Error(error.message);
55
+
const inviteRes = await inviteNewUser({
61
+
$type: "com.atproto.repo.strongRef",
64
+
console.log(inviteRes)
65
+
if (!inviteRes.ok) throw new Error(inviteRes.error);
67
+
onSuccess: async () => {
68
+
await queryClient.invalidateQueries({
69
+
queryKey: ["invites", session.did],
74
+
const disableSubmitButton = (() => {
75
+
const { success } = didSchema.safeParse(inputText);
76
+
if (!success) return true;
77
+
return !inputText.trim();
83
+
backgroundColor: semantic.surface,
84
+
borderRadius: atoms.radii.lg,
90
+
<View style={{ gap: 4 }}>
91
+
<Text>User DID:</Text>
94
+
flexDirection: "row",
95
+
alignItems: "center",
104
+
borderColor: semantic.borderVariant,
105
+
borderRadius: atoms.radii.md,
107
+
color: semantic.text,
109
+
fontFamily: typography.families.primary,
112
+
typography.weights.byName.extralight,
113
+
typography.sizes.sm,
116
+
onChangeText={(text) => {
117
+
setInputText(text);
119
+
placeholder="did:plc:... or did:web:..."
120
+
placeholderTextColor={semantic.textPlaceholder}
124
+
console.log("mutating");
125
+
inviteUserMutation();
127
+
disabled={disableSubmitButton}
130
+
mutationPending ? (
131
+
<Loading size="small" />
137
+
backgroundColor: disableSubmitButton
138
+
? semantic.textPlaceholder
140
+
? lighten(semantic.primary, 7)
141
+
: semantic.primary,
142
+
alignSelf: "flex-start",
144
+
borderRadius: atoms.radii.md,
145
+
borderColor: semantic.borderVariant,
153
+
<View style={{ gap: 4 }}>
154
+
<Text>Invited users:</Text>
156
+
<Loading size="small" />
161
+
data={invites.toReversed()}
162
+
renderItem={({ item }) => (
163
+
<InvitedUser invite={item} />
165
+
keyExtractor={(_, index) => index.toString()}
166
+
contentContainerStyle={{
170
+
showsVerticalScrollIndicator={false}
179
+
const invitesQueryFn = async ({
183
+
session: OAuthSession;
184
+
channelAtUri: string;
186
+
const invites = await getInviteRecordsFromPds({
187
+
pdsEndpoint: session.serverMetadata.issuer,
192
+
console.error("invitesQueryFn error.", invites.error);
194
+
`Something went wrong while getting the user's channel records.}`,
198
+
const results = invites.data
200
+
const convertResult = stringToAtUri(record.uri);
201
+
if (!convertResult.ok) {
203
+
"Could not convert",
205
+
"into at:// URI object.",
206
+
convertResult.error,
210
+
if (!convertResult.data.collection || !convertResult.data.rKey) {
213
+
"did not convert to a full at:// URI with collection and rkey.",
217
+
const uri: Required<AtUri> = {
218
+
authority: convertResult.data.authority,
219
+
collection: convertResult.data.collection,
220
+
rKey: convertResult.data.rKey,
222
+
return { uri, value: record.value };
224
+
.filter((atUri) => atUri !== undefined)
225
+
.filter((atUri) => atUri.value.channel.uri === channelAtUri);
230
+
const InvitedUser = ({
234
+
value: SystemsGmstnDevelopmentChannelInvite;
235
+
uri: Required<AtUri>;
238
+
const { isLoading, data: handle } = useQuery({
239
+
queryKey: ["handle", invite.value.recipient],
240
+
queryFn: async () => {
241
+
const didDoc = await didDocResolver.resolve(invite.value.recipient);
242
+
if (!didDoc.alsoKnownAs)
243
+
throw new Error("DID did not resolve to handle");
244
+
if (didDoc.alsoKnownAs.length === 0)
246
+
"No alsoKnownAs in DID document. It might be malformed.",
248
+
return didDoc.alsoKnownAs[0].slice(5);
255
+
<Loading size="small" />
260
+
flexDirection: "row",
261
+
justifyContent: "space-between",
265
+
<Text>@{handle}</Text>
269
+
invite.value.createdAt,
270
+
"do MMM y, h:mmaaa",