Skip to content
Snippets Groups Projects
RevoltClient.tsx 7.38 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 "./error";
insert's avatar
insert committed
import { createContext } from "preact";
import { Children } from "../../types/Preact";
import { Route } from "revolt.js/dist/api/routes";
insert's avatar
insert committed
import { useEffect, useState } from "preact/hooks";
insert's avatar
insert committed
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";
insert's avatar
insert committed
import { registerEvents, setReconnectDisallowed } from "./events";
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;
}

export interface AppState {
insert's avatar
insert committed
    client: Client;
insert's avatar
insert committed
    status: ClientStatus;
    operations: ClientOperations;
}

export const AppContext = createContext<AppState>(undefined as any);

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

function Context({ auth, sync, children, dispatcher }: Props) {
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 {
                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.');
            }

            setClient(new Client({
                autoReconnect: false,
                apiURL: import.meta.env.VITE_API_URL,
                debug: import.meta.env.DEV,
                db
            }));

            setStatus(ClientStatus.LOADING);
        })();
    }, [ ]);

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

    const value: AppState = {
insert's avatar
insert committed
        client,
insert's avatar
insert committed
        status,
        operations: {
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({
                            id: "onboarding",
                            callback: async (username: string) => {
                                await (onboarding as any)(username, true);
                                login();
                            }
                        });*/
                    } else {
                        login();
                    }
                } catch (err) {
                    setReconnectDisallowed(false);
                    throw err;
                }
            },
            logout: async shouldRequest => {
                dispatcher({ type: "LOGOUT" });

                delete client.user;
                dispatcher({ type: "RESET" });

                // openScreen({ id: "none" });
                setStatus(ClientStatus.READY);

                client.websocket.disconnect();

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

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

    useEffect(() => {
        (async () => {
            await client.restore();

            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);
                }

                if (value.operations.ready())
                    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 });*/
                    //} else {
                        /*
                        // ! FIXME: all this code needs to be re-written
                        (async () => {
                            // ! FIXME: should be included in Ready payload
                            props.dispatcher({
                                type: 'SYNC_UPDATE',
                                // ! FIXME: write a procedure to resolve merge conflicts
                                update: mapSync(
                                    await client.syncFetchSettings(DEFAULT_ENABLED_SYNC.filter(x => !props.sync?.disabled?.includes(x)))
                                )
                            });
                        })()

                        props.dispatcher({ type: 'UNREADS_SET', unreads: await client.syncFetchUnreads() });*/
                    //}
                } catch (err) {
                    setStatus(ClientStatus.DISCONNECTED);
                    const error = takeError(err);
                    if (error === "Forbidden") {
                        value.operations.logout(true);
                        // openScreen({ id: "signed_out" });
                    } else {
                        // openScreen({ id: "error", error });
                    }
                }
            } 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) {
        return <Preloader />;
    }

insert's avatar
insert committed
    return (
        <AppContext.Provider value={value}>
            { children }
        </AppContext.Provider>
    );
}

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