Live video on the AT Protocol
1import { SessionManager } from "@atproto/api/dist/session-manager";
2import { useContext } from "react";
3import { PlaceStreamChatProfile, PlaceStreamLivestream } from "streamplace";
4import { createStore, StoreApi, useStore } from "zustand";
5import { StreamplaceContext } from "../streamplace-provider/context";
6
7// there are three categories of XRPC that we need to handle:
8// 1. Public (probably) OAuth XRPC to the users' PDS for apps that use this API.
9// 2. Confidental OAuth to the Streamplace server for doing things that require
10// server-side authentication. This isn't very much stuff yet, but you need
11// to log into Streamplace to do things like have Streamplace update your
12// activity status.
13// 3. Anonymous XRPC to the Streamplace server for stuff like `getLiveUsers`. This
14// is easy to handle internal to this library.
15// For the Streamplace app itself, all three are the same. For apps that aren't
16// doing OAuth through the Streamplace node, we need to expose an interface that
17// allows them to use atcute or whatever for 1.
18
19export interface StreamplaceState {
20 url: string;
21 liveUsers: PlaceStreamLivestream.LivestreamView[] | null;
22 setLiveUsers: (opts: {
23 liveUsers?: PlaceStreamLivestream.LivestreamView[];
24 liveUsersLoading?: boolean;
25 liveUsersError?: string | null;
26 liveUsersRefresh?: number;
27 }) => void;
28 liveUsersRefresh: number;
29 liveUsersLoading: boolean;
30 liveUsersError: string | null;
31 oauthSession: SessionManager | null;
32 handle: string | null;
33 chatProfile: PlaceStreamChatProfile.Record | null;
34}
35
36export type StreamplaceStore = StoreApi<StreamplaceState>;
37
38export const makeStreamplaceStore = ({
39 url,
40}: {
41 url: string;
42}): StoreApi<StreamplaceState> => {
43 return createStore<StreamplaceState>()((set) => ({
44 url,
45 liveUsers: null,
46 setLiveUsers: (opts: {
47 liveUsers?: PlaceStreamLivestream.LivestreamView[];
48 liveUsersLoading?: boolean;
49 liveUsersError?: string | null;
50 liveUsersRefresh?: number;
51 }) => {
52 set({
53 ...opts,
54 });
55 },
56 liveUsersRefresh: 0,
57 liveUsersLoading: true,
58 liveUsersError: null,
59 oauthSession: null,
60 handle: null,
61 chatProfile: null,
62 }));
63};
64
65export function getStreamplaceStoreFromContext(): StreamplaceStore {
66 const context = useContext(StreamplaceContext);
67 if (!context) {
68 throw new Error(
69 "useStreamplaceStore must be used within a StreamplaceProvider",
70 );
71 }
72 return context.store;
73}
74
75export function useStreamplaceStore<U>(
76 selector: (state: StreamplaceState) => U,
77): U {
78 return useStore(getStreamplaceStoreFromContext(), selector);
79}
80
81export const useUrl = () => useStreamplaceStore((x) => x.url);
82
83export const useDID = () => useStreamplaceStore((x) => x.oauthSession?.did);
84
85export const useHandle = () => useStreamplaceStore((x) => x.handle);
86export const useSetHandle = (): ((handle: string) => void) => {
87 const store = getStreamplaceStoreFromContext();
88 return (handle: string) => store.setState({ handle });
89};