frontend client for gemstone. decentralised workplace app

refactor: split up provider

serenity c9fa122d b3c041dd

+106
src/providers/authed/ChannelsProvider.tsx
···
+
import { DEFAULT_STALE_TIME } from "@/lib/consts";
+
import type { AtUri } from "@/lib/types/atproto";
+
import {
+
systemsGmstnDevelopmentChannelRecordSchema,
+
type SystemsGmstnDevelopmentChannel,
+
} from "@/lib/types/lexicon/systems.gmstn.development.channels";
+
import { getRecordFromFullAtUri, stringToAtUri } from "@/lib/utils/atproto";
+
import { useMemberships } from "@/providers/authed/MembershipsProvider";
+
import { useQueries } from "@tanstack/react-query";
+
import type { ReactNode } from "react";
+
import { createContext, useContext } from "react";
+
+
interface ChannelsContextValue {
+
channels: Array<{
+
channel: SystemsGmstnDevelopmentChannel;
+
channelAtUri: AtUri;
+
}>;
+
isInitialising: boolean;
+
error: Error | null;
+
}
+
+
const ChannelsContext = createContext<ChannelsContextValue | null>(null);
+
+
export const useChannelRecords = () => {
+
const channelsValue = useContext(ChannelsContext);
+
if (!channelsValue)
+
throw new Error(
+
"Channels context was null. Did you try to access this outside of the authed providers? ChannelsProvider must be below OAuth provider.",
+
);
+
return channelsValue;
+
};
+
+
export const useChannelRecordByAtUriObject = (channelAtUri: AtUri) => {
+
const { channels } = useChannelRecords();
+
return channels.find((channel) => channel.channelAtUri === channelAtUri);
+
};
+
+
export const useChannelRecordByAtUriString = (channelAtUriString: string) => {
+
const convertResult = stringToAtUri(channelAtUriString);
+
const { channels } = useChannelRecords();
+
if (!convertResult.ok) {
+
console.error(
+
"Something went wrong getting membership value from context.",
+
);
+
console.error(
+
"Provided string",
+
channelAtUriString,
+
"was not a valid at:// URI",
+
);
+
return;
+
}
+
const { data: atUri } = convertResult;
+
return channels.find((channel) => channel.channelAtUri === atUri);
+
};
+
+
export const ChannelsProvider = ({ children }: { children: ReactNode }) => {
+
const { memberships, isInitialising: membershipsInitialising } =
+
useMemberships();
+
+
// TODO: group channel memberships by
+
+
const channelsQueries = useQueries({
+
queries: memberships.map((membershipObjects) => ({
+
enabled: membershipsInitialising,
+
queryKey: ["channel", membershipObjects.membership.channel.uri],
+
queryFn: () => channelQueryFn(membershipObjects.channelAtUri),
+
staleTime: DEFAULT_STALE_TIME,
+
})),
+
});
+
+
const isInitialising = channelsQueries.some((q) => q.isLoading);
+
+
const value: ChannelsContextValue = {
+
isInitialising,
+
channels: channelsQueries
+
.map((q) => q.data)
+
.filter((c) => c !== undefined),
+
error: channelsQueries.find((q) => q.error)?.error ?? null,
+
};
+
+
return <ChannelsContext value={value}>{children}</ChannelsContext>;
+
};
+
+
// FIXME: holy shit don't do this. we will build prism and use that to resolve
+
// our memberships into channels. for now we resolve the at-uri manually.
+
const channelQueryFn = async (channelAtUri: AtUri) => {
+
const res = await getRecordFromFullAtUri(channelAtUri);
+
if (!res.ok) {
+
console.error("Could not retrieve record from full at uri", res.error);
+
throw new Error("Something went wrong while fetching channel records");
+
}
+
const {
+
success,
+
error,
+
data: channel,
+
} = systemsGmstnDevelopmentChannelRecordSchema.safeParse(res.data);
+
if (!success) {
+
console.error(
+
res.data,
+
"did not resolve to a valid channel record",
+
error,
+
);
+
throw new Error("Something went wrong while fetching channel records");
+
}
+
return { channel, channelAtUri };
+
};
+103 -144
src/providers/authed/LatticeSessionsProvider.tsx
···
import { DEFAULT_STALE_TIME } from "@/lib/consts";
-
import type { AtUri, Did } from "@/lib/types/atproto";
+
import type { Did } from "@/lib/types/atproto";
import type { LatticeSessionInfo } from "@/lib/types/handshake";
-
import { systemsGmstnDevelopmentChannelRecordSchema } from "@/lib/types/lexicon/systems.gmstn.development.channels";
-
import { getRecordFromFullAtUri, stringToAtUri } from "@/lib/utils/atproto";
+
import type { SystemsGmstnDevelopmentChannelMembership } from "@/lib/types/lexicon/systems.gmstn.development.channel.membership";
+
import type { SystemsGmstnDevelopmentChannel } from "@/lib/types/lexicon/systems.gmstn.development.channels";
+
import { stringToAtUri } from "@/lib/utils/atproto";
+
import {
+
ChannelsProvider,
+
useChannelRecords,
+
} from "@/providers/authed/ChannelsProvider";
+
import {
+
MembershipsProvider,
+
useMemberships,
+
} from "@/providers/authed/MembershipsProvider";
+
import type { OAuth } from "@/providers/OAuthProvider";
import { useOAuthValue } from "@/providers/OAuthProvider";
-
import { getMembershipRecordsFromPds } from "@/queries/get-membership-from-pds";
import { initiateHandshakeTo } from "@/queries/initiate-handshake-to";
-
import type { OAuthSession } from "@atproto/oauth-client";
-
import { useQueries, useQuery } from "@tanstack/react-query";
+
import { useQueries } from "@tanstack/react-query";
import type { ReactNode } from "react";
import { createContext, useContext, useMemo } from "react";
···
null,
);
-
export const LatticeSessionsProvider = ({
+
export const useLatticeSession = () => {
+
const value = useContext(LatticeSessionsContext);
+
if (value === null)
+
throw new Error(
+
"Lattice session context not inited. Or ordering of providers was wrong.",
+
);
+
return value.sessions;
+
};
+
+
const LatticeSessionsProviderInner = ({
children,
}: {
children: ReactNode;
}) => {
+
const {
+
memberships,
+
isInitialising: membershipsInitialising,
+
error: membershipError,
+
} = useMemberships();
+
const {
+
channels,
+
isInitialising: channelsInitialising,
+
error: channelsError,
+
} = useChannelRecords();
const oauth = useOAuthValue();
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Explicit guard
···
const { session, isLoading, agent, client } = oauth;
const isOAuthReady = !isLoading && !!agent && !!client && !!session;
-
const membershipsQuery = useQuery({
-
queryKey: ["membership", session?.did],
-
enabled: isOAuthReady,
-
queryFn: async () => {
-
if (!session) throw new Error("We need an OAuth session");
-
return await membershipQueryFn(session);
-
},
-
staleTime: DEFAULT_STALE_TIME,
-
});
-
-
// TODO: group channel memberships by
-
-
const channelsQueries = useQueries({
-
queries: !membershipsQuery.data
-
? []
-
: membershipsQuery.data.map((membershipQueryResult) => ({
-
enabled: membershipsQuery.isSuccess,
-
queryKey: [
-
"channel",
-
membershipQueryResult.membership.channel.uri,
-
],
-
queryFn: () => channelQueryFn(membershipQueryResult.atUri),
-
staleTime: DEFAULT_STALE_TIME,
-
})),
-
});
-
const handshakeQueries = useQueries({
-
queries: channelsQueries
-
.map((queryResult) =>
-
queryResult.data
-
? {
-
queryKey: ["handshakes", queryResult.data.name],
-
queryFn: async () => {
-
const { routeThrough } = queryResult.data;
-
const latticeAtUri = stringToAtUri(
-
routeThrough.uri,
-
);
-
if (!latticeAtUri.ok) {
-
console.error(
-
"Lattice AT URI did not resolve properly",
-
routeThrough,
-
latticeAtUri.error,
-
);
-
throw new Error(
-
"Something went wrong while initiating handshakes",
-
);
-
}
-
// TODO: unfuck this.
-
const did =
-
`did:web:${encodeURIComponent(latticeAtUri.data.rKey ?? "")}` as Did;
-
const handshakeResult = await initiateHandshakeTo(
-
{
-
did,
-
memberships:
-
membershipsQuery.data?.map(
-
(queryResult) =>
-
queryResult.membership,
-
) ?? [],
-
oauth,
-
},
-
);
-
-
if (!handshakeResult.ok) {
-
console.error(
-
"Handshake to",
-
did,
-
"failed. Reason:",
-
handshakeResult.error,
-
);
-
throw new Error("Handshake failed.");
-
}
-
-
return {
-
did,
-
sessionInfo: handshakeResult.data,
-
};
-
},
-
staleTime: DEFAULT_STALE_TIME,
-
}
-
: undefined,
-
)
-
.filter((query) => query !== undefined),
+
queries: channels.map((channelObj) => ({
+
queryKey: ["handshakes", channelObj.channel.name],
+
queryFn: () =>
+
handshakesQueryFn({
+
channel: channelObj.channel,
+
memberships: memberships.map(
+
({ membership }) => membership,
+
),
+
oauth,
+
}),
+
staleTime: DEFAULT_STALE_TIME,
+
})),
});
const isInitialising =
-
membershipsQuery.isLoading ||
-
channelsQueries.some((q) => q.isLoading) ||
+
isOAuthReady ||
+
membershipsInitialising ||
+
channelsInitialising ||
handshakeQueries.some((q) => q.isLoading);
const error =
-
membershipsQuery.error ??
-
channelsQueries.find((q) => q.error)?.error ??
+
membershipError ??
+
channelsError ??
handshakeQueries.find((q) => q.error)?.error ??
null;
···
);
};
-
const membershipQueryFn = async (session: OAuthSession) => {
-
const memberships = await getMembershipRecordsFromPds({
-
pdsEndpoint: session.serverMetadata.issuer,
-
did: session.did,
-
});
-
-
if (!memberships.ok) {
-
console.error("getMembershipRecordsFromPds error.", memberships.error);
-
throw new Error(
-
`Something went wrong while getting the user's membership records.}`,
+
const handshakesQueryFn = async ({
+
channel,
+
memberships,
+
oauth,
+
}: {
+
channel: SystemsGmstnDevelopmentChannel;
+
memberships: Array<SystemsGmstnDevelopmentChannelMembership>;
+
oauth: OAuth;
+
}) => {
+
const { routeThrough } = channel;
+
const latticeAtUri = stringToAtUri(routeThrough.uri);
+
if (!latticeAtUri.ok) {
+
console.error(
+
"Lattice AT URI did not resolve properly",
+
routeThrough,
+
latticeAtUri.error,
);
+
throw new Error("Something went wrong while initiating handshakes");
}
-
const { data } = memberships;
-
const membershipAtUris = data.map((membership) => {
-
const convertResult = stringToAtUri(membership.channel.uri);
-
if (!convertResult.ok) {
-
console.error(
-
"Could not convert",
-
membership,
-
"into at:// URI object.",
-
convertResult.error,
-
);
-
return;
-
}
-
return { membership, atUri: convertResult.data };
+
// TODO: unfuck this.
+
const did =
+
`did:web:${encodeURIComponent(latticeAtUri.data.rKey ?? "")}` as Did;
+
const handshakeResult = await initiateHandshakeTo({
+
did,
+
memberships,
+
oauth,
});
-
return membershipAtUris.filter((membership) => membership !== undefined);
-
};
-
-
// FIXME: holy shit don't do this. we will build prism and use that to resolve
-
// our memberships into channels. for now we resolve the at-uri manually.
-
const channelQueryFn = async (membership: AtUri) => {
-
const res = await getRecordFromFullAtUri(membership);
-
if (!res.ok) {
-
console.error("Could not retrieve record from full at uri", res.error);
-
throw new Error("Something went wrong while fetching channel records");
-
}
-
const {
-
success,
-
error,
-
data: channel,
-
} = systemsGmstnDevelopmentChannelRecordSchema.safeParse(res.data);
-
if (!success) {
+
if (!handshakeResult.ok) {
console.error(
-
res.data,
-
"did not resolve to a valid channel record",
-
error,
+
"Handshake to",
+
did,
+
"failed. Reason:",
+
handshakeResult.error,
);
-
throw new Error("Something went wrong while fetching channel records");
+
throw new Error("Handshake failed.");
}
-
return channel;
+
+
return {
+
did,
+
sessionInfo: handshakeResult.data,
+
};
};
-
export const useLatticeSession = () => {
-
const value = useContext(LatticeSessionsContext);
-
if (value === null)
-
throw new Error(
-
"Lattice session context not inited. Or ordering of providers was wrong.",
-
);
-
return value.sessions;
+
export const LatticeSessionsProvider = ({
+
children,
+
}: {
+
children: ReactNode;
+
}) => {
+
// Memberships must be above channels
+
// channels must be above lattice sessions inner
+
// do it this way to preserve the order if we need to move them around some day
+
return (
+
<MembershipsProvider>
+
<ChannelsProvider>
+
<LatticeSessionsProviderInner>
+
{children}
+
</LatticeSessionsProviderInner>
+
</ChannelsProvider>
+
</MembershipsProvider>
+
);
};
+145
src/providers/authed/MembershipsProvider.tsx
···
+
import { DEFAULT_STALE_TIME } from "@/lib/consts";
+
import type { AtUri } from "@/lib/types/atproto";
+
import type { SystemsGmstnDevelopmentChannelMembership } from "@/lib/types/lexicon/systems.gmstn.development.channel.membership";
+
import { stringToAtUri } from "@/lib/utils/atproto";
+
import { useOAuthValue } from "@/providers/OAuthProvider";
+
import { getMembershipRecordsFromPds } from "@/queries/get-membership-from-pds";
+
import type { OAuthSession } from "@atproto/oauth-client";
+
import { useQuery } from "@tanstack/react-query";
+
import type { ReactNode } from "react";
+
import { createContext, useContext } from "react";
+
+
interface MembershipsContextValue {
+
memberships: Array<{
+
membership: SystemsGmstnDevelopmentChannelMembership;
+
channelAtUri: AtUri;
+
}>;
+
isInitialising: boolean;
+
error: Error | null;
+
}
+
+
const MembershipsContext = createContext<MembershipsContextValue | null>(null);
+
+
export const useMemberships = () => {
+
const membershipsValue = useContext(MembershipsContext);
+
if (!membershipsValue)
+
throw new Error(
+
"Memberships context was null. Did you try to access this outside of the authed providers? MembershipsProvider must be below OAuth provider.",
+
);
+
return membershipsValue;
+
};
+
+
export const useMembershipByAtUriObject = (atUri: AtUri) => {
+
const { memberships } = useMemberships();
+
return memberships.find((membership) => membership.channelAtUri === atUri);
+
};
+
+
export const useMembershipByAtUriString = (atUriString: string) => {
+
const convertResult = stringToAtUri(atUriString);
+
const { memberships } = useMemberships();
+
if (!convertResult.ok) {
+
console.error(
+
"Something went wrong getting membership value from context.",
+
);
+
console.error(
+
"Provided string",
+
atUriString,
+
"was not a valid at:// URI",
+
);
+
return;
+
}
+
const { data: atUri } = convertResult;
+
return memberships.find((membership) => membership.channelAtUri === atUri);
+
};
+
+
export const useMembershipByInviteCid = (inviteCid: string) => {
+
const { memberships } = useMemberships();
+
return memberships.find(
+
({ membership }) => (membership.invite.cid = inviteCid),
+
);
+
};
+
+
export const useMembershipByInviteAtUriString = (inviteAtUriString: string) => {
+
const { memberships } = useMemberships();
+
return memberships.find(
+
({ membership }) => (membership.invite.uri = inviteAtUriString),
+
);
+
};
+
+
export const useMembershipByChannelCid = (channelCid: string) => {
+
const { memberships } = useMemberships();
+
return memberships.find(
+
({ membership }) => membership.channel.cid === channelCid,
+
);
+
};
+
+
export const useMembershipByChannetAtUriString = (
+
channelAtUriString: string,
+
) => {
+
const { memberships } = useMemberships();
+
return memberships.find(
+
({ membership }) => membership.channel.uri === channelAtUriString,
+
);
+
};
+
+
export const MembershipsProvider = ({ children }: { children: ReactNode }) => {
+
const oauth = useOAuthValue();
+
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Explicit guard
+
if (!oauth)
+
throw new Error(
+
"LatticeSessionsProvider must be used within an OAuth provider.",
+
);
+
+
const { session, isLoading, agent, client } = oauth;
+
const isOAuthReady = !isLoading && !!agent && !!client && !!session;
+
+
const membershipsQuery = useQuery({
+
queryKey: ["membership", session?.did],
+
enabled: isOAuthReady,
+
queryFn: async () => {
+
if (!session) throw new Error("We need an OAuth session");
+
return await membershipQueryFn(session);
+
},
+
staleTime: DEFAULT_STALE_TIME,
+
});
+
const isInitialising = membershipsQuery.isLoading;
+
+
const value: MembershipsContextValue = {
+
isInitialising,
+
memberships: membershipsQuery.data ?? [],
+
error: membershipsQuery.error,
+
};
+
+
return <MembershipsContext value={value}>{children}</MembershipsContext>;
+
};
+
+
const membershipQueryFn = async (session: OAuthSession) => {
+
const memberships = await getMembershipRecordsFromPds({
+
pdsEndpoint: session.serverMetadata.issuer,
+
did: session.did,
+
});
+
+
if (!memberships.ok) {
+
console.error("getMembershipRecordsFromPds error.", memberships.error);
+
throw new Error(
+
`Something went wrong while getting the user's membership records.}`,
+
);
+
}
+
const { data } = memberships;
+
const membershipAtUris = data.map((membership) => {
+
const convertResult = stringToAtUri(membership.channel.uri);
+
if (!convertResult.ok) {
+
console.error(
+
"Could not convert",
+
membership,
+
"into at:// URI object.",
+
convertResult.error,
+
);
+
return;
+
}
+
return { membership, channelAtUri: convertResult.data };
+
});
+
+
return membershipAtUris.filter((membership) => membership !== undefined);
+
};