Skip to content
Snippets Groups Projects
RevoltClient.tsx 7.15 KiB
Newer Older
insert's avatar
insert committed
import { openDB } from 'idb';
insert's avatar
insert committed
import { Client } from "revolt.js";
insert's avatar
insert committed
import { takeError } from "./util";
insert's avatar
insert committed
import { createContext } from "preact";
import { Children } from "../../types/Preact";
import { Route } from "revolt.js/dist/api/routes";
import { connectState } from "../../redux/connector";
insert's avatar
insert committed
import Preloader from "../../components/ui/Preloader";
insert's avatar
insert committed
import { WithDispatcher } from "../../redux/reducers";
import { AuthState } from "../../redux/reducers/auth";
import { SyncOptions } from "../../redux/reducers/sync";
import { useEffect, useMemo, useState } from "preact/hooks";
import { useIntermediate } from '../intermediate/Intermediate';
insert's avatar
insert committed
import { registerEvents, setReconnectDisallowed } from "./events";
import { SingletonMessageRenderer } from '../../lib/renderer/Singleton';
insert's avatar
insert committed

export enum ClientStatus {
insert's avatar
insert committed
    INIT,
insert's avatar
insert committed
    LOADING,
    READY,
    OFFLINE,
    DISCONNECTED,
    CONNECTING,
    RECONNECTING,
insert's avatar
insert committed
    ONLINE,
insert's avatar
insert committed
}

insert's avatar
insert committed
export interface ClientOperations {
    login: (data: Route<"POST", "/auth/login">["data"]) => Promise<void>;
    logout: (shouldRequest?: boolean) => Promise<void>;
    loggedIn: () => boolean;
    ready: () => boolean;
}

insert's avatar
insert committed
export const AppContext = createContext<Client>(undefined as any);
export const StatusContext = createContext<ClientStatus>(undefined as any);
export const OperationsContext = createContext<ClientOperations>(undefined as any);
insert's avatar
insert committed

type Props = WithDispatcher & {
    auth: AuthState;
    children: Children;
};

insert's avatar
insert committed
function Context({ auth, children, dispatcher }: Props) {
    const { openScreen } = useIntermediate();
insert's avatar
insert committed
    const [status, setStatus] = useState(ClientStatus.INIT);
    const [client, setClient] = useState<Client>(undefined as unknown as Client);

    useEffect(() => {
        (async () => {
            let db;
            try {
                // Match sw.ts#L23
insert's avatar
insert committed
                db = await openDB('state', 3, {
                    upgrade(db) {
                        for (let store of [ "channels", "servers", "users", "members" ]) {
                            db.createObjectStore(store, {
                                keyPath: '_id'
                            });
                        }
                    },
                });
            } catch (err) {
                console.error('Failed to open IndexedDB store, continuing without.');
            }

            const client = new Client({
insert's avatar
insert committed
                autoReconnect: false,
                apiURL: import.meta.env.VITE_API_URL,
                debug: import.meta.env.DEV,
                db
            setClient(client);
            SingletonMessageRenderer.subscribe(client);
insert's avatar
insert committed
            setStatus(ClientStatus.LOADING);
        })();
    }, [ ]);

    if (status === ClientStatus.INIT) return null;
insert's avatar
insert committed

insert's avatar
insert committed
    const operations: ClientOperations = useMemo(() => {
        return {
insert's avatar
insert committed
            login: async data => {
                setReconnectDisallowed(true);

                try {
                    const onboarding = await client.login(data);
                    setReconnectDisallowed(false);
                    const login = () =>
                        dispatcher({
                            type: "LOGIN",
                            session: client.session as any
                        });

                    if (onboarding) {
                        openScreen({
insert's avatar
insert committed
                            id: "onboarding",
                            callback: async (username: string) => {
                                await (onboarding as any)(username, true);
                                login();
                            }
insert's avatar
insert committed
                    } else {
                        login();
                    }
                } catch (err) {
                    setReconnectDisallowed(false);
                    throw err;
                }
            },
            logout: async shouldRequest => {
                dispatcher({ type: "LOGOUT" });

                client.reset();
insert's avatar
insert committed
                dispatcher({ type: "RESET" });

                openScreen({ id: "none" });
insert's avatar
insert committed
                setStatus(ClientStatus.READY);

                client.websocket.disconnect();

                if (shouldRequest) {
                    try {
                        await client.logout();
                    } catch (err) {
                        console.error(err);
                    }
                }
            },
            loggedIn: () => typeof auth.active !== "undefined",
            ready: () => (
insert's avatar
insert committed
                operations.loggedIn() &&
insert's avatar
insert committed
                typeof client.user !== "undefined"
            )
insert's avatar
insert committed
        }
insert's avatar
insert committed
    }, [ client, auth.active ]);
insert's avatar
insert committed

    useEffect(() => registerEvents({ operations, dispatcher }, setStatus, client), [ client ]);
insert's avatar
insert committed

    useEffect(() => {
        (async () => {
            if (client.db) {
                await client.restore();
            }
insert's avatar
insert committed

            if (auth.active) {
                dispatcher({ type: "QUEUE_FAIL_ALL" });

                const active = auth.accounts[auth.active];
                client.user = client.users.get(active.session.user_id);
                if (!navigator.onLine) {
                    return setStatus(ClientStatus.OFFLINE);
                }

insert's avatar
insert committed
                if (operations.ready())
insert's avatar
insert committed
                    setStatus(ClientStatus.CONNECTING);
                
                if (navigator.onLine) {
                    await client
                        .fetchConfiguration()
                        .catch(() =>
                            console.error("Failed to connect to API server.")
                        );
                }

                try {
                    await client.fetchConfiguration();
                    const callback = await client.useExistingSession(
                        active.session
                    );

                    if (callback) {
                        openScreen({ id: "onboarding", callback });
                    }
insert's avatar
insert committed
                } catch (err) {
                    setStatus(ClientStatus.DISCONNECTED);
                    const error = takeError(err);
                    if (error === "Forbidden" || error === "Unauthorized") {
insert's avatar
insert committed
                        operations.logout(true);
                        openScreen({ id: "signed_out" });
insert's avatar
insert committed
                    } else {
                        openScreen({ id: "error", error });
insert's avatar
insert committed
                    }
                }
            } else {
insert's avatar
insert committed
                try {
                    await client.fetchConfiguration()
                } catch (err) {
                    console.error("Failed to connect to API server.");
                }

insert's avatar
insert committed
                setStatus(ClientStatus.READY);
            }
        })();
    }, []);

    if (status === ClientStatus.LOADING) {
insert's avatar
insert committed
        return <Preloader type="spinner" />;
insert's avatar
insert committed
    return (
insert's avatar
insert committed
        <AppContext.Provider value={client}>
            <StatusContext.Provider value={status}>
                <OperationsContext.Provider value={operations}>
                    { children }
                </OperationsContext.Provider>
            </StatusContext.Provider>
insert's avatar
insert committed
        </AppContext.Provider>
    );
}

export default connectState<{ children: Children }>(
    Context,
    state => {
        return {
            auth: state.auth,
            sync: state.sync
        };
    },
    true
);