mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-06 17:11:55 +00:00
Hide client behind context.
Use idb for saving data. Allow logins.
This commit is contained in:
@@ -1,14 +1,19 @@
|
||||
import { openDB } from 'idb';
|
||||
import { Client } from "revolt.js";
|
||||
import { takeError } from "./error";
|
||||
import { createContext } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { Children } from "../../types/Preact";
|
||||
import { Route } from "revolt.js/dist/api/routes";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { connectState } from "../../redux/connector";
|
||||
import Preloader from "../../components/ui/Preloader";
|
||||
import { WithDispatcher } from "../../redux/reducers";
|
||||
import { AuthState } from "../../redux/reducers/auth";
|
||||
import { SyncOptions } from "../../redux/reducers/sync";
|
||||
import { registerEvents, setReconnectDisallowed } from "./events";
|
||||
|
||||
export enum ClientStatus {
|
||||
INIT,
|
||||
LOADING,
|
||||
READY,
|
||||
OFFLINE,
|
||||
@@ -26,19 +31,13 @@ export interface ClientOperations {
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
client: Client;
|
||||
status: ClientStatus;
|
||||
operations: ClientOperations;
|
||||
}
|
||||
|
||||
export const AppContext = createContext<AppState>(undefined as any);
|
||||
|
||||
export const RevoltClient = new Client({
|
||||
autoReconnect: false,
|
||||
apiURL: import.meta.env.VITE_API_URL,
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
// db: new Db("state", 3, ["channels", "servers", "users", "members"])
|
||||
});
|
||||
|
||||
type Props = WithDispatcher & {
|
||||
auth: AuthState;
|
||||
sync: SyncOptions;
|
||||
@@ -46,18 +45,176 @@ type Props = WithDispatcher & {
|
||||
};
|
||||
|
||||
function Context({ auth, sync, children, dispatcher }: Props) {
|
||||
const [status, setStatus] = useState(ClientStatus.LOADING);
|
||||
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;
|
||||
|
||||
const value: AppState = {
|
||||
client,
|
||||
status,
|
||||
operations: {
|
||||
login: async data => {},
|
||||
logout: async shouldRequest => {},
|
||||
loggedIn: () => false,
|
||||
ready: () => false
|
||||
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"
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
await client
|
||||
.fetchConfiguration()
|
||||
.catch(() =>
|
||||
console.error("Failed to connect to API server.")
|
||||
);
|
||||
setStatus(ClientStatus.READY);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (status === ClientStatus.LOADING) {
|
||||
return <Preloader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={value}>
|
||||
{ children }
|
||||
|
||||
121
src/context/revoltjs/events.ts
Normal file
121
src/context/revoltjs/events.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
||||
import { WithDispatcher } from "../../redux/reducers";
|
||||
import { Client, Message } from "revolt.js/dist";
|
||||
import {
|
||||
AppState,
|
||||
ClientStatus
|
||||
} from "./RevoltClient";
|
||||
import { StateUpdater } from "preact/hooks";
|
||||
|
||||
export var preventReconnect = false;
|
||||
let preventUntil = 0;
|
||||
|
||||
export function setReconnectDisallowed(allowed: boolean) {
|
||||
preventReconnect = allowed;
|
||||
}
|
||||
|
||||
export function registerEvents({
|
||||
operations,
|
||||
dispatcher
|
||||
}: AppState & WithDispatcher, setStatus: StateUpdater<ClientStatus>, client: Client) {
|
||||
const listeners = {
|
||||
connecting: () =>
|
||||
operations.ready() && setStatus(ClientStatus.CONNECTING),
|
||||
|
||||
dropped: () => {
|
||||
operations.ready() && setStatus(ClientStatus.DISCONNECTED);
|
||||
|
||||
if (preventReconnect) return;
|
||||
function reconnect() {
|
||||
preventUntil = +new Date() + 2000;
|
||||
client.websocket.connect().catch(err => console.error(err));
|
||||
}
|
||||
|
||||
if (+new Date() > preventUntil) {
|
||||
setTimeout(reconnect, 2000);
|
||||
} else {
|
||||
reconnect();
|
||||
}
|
||||
},
|
||||
|
||||
packet: (packet: ClientboundNotification) => {
|
||||
switch (packet.type) {
|
||||
case "ChannelStartTyping": {
|
||||
if (packet.user === client.user?._id) return;
|
||||
dispatcher({
|
||||
type: "TYPING_START",
|
||||
channel: packet.id,
|
||||
user: packet.user
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "ChannelStopTyping": {
|
||||
if (packet.user === client.user?._id) return;
|
||||
dispatcher({
|
||||
type: "TYPING_STOP",
|
||||
channel: packet.id,
|
||||
user: packet.user
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "ChannelAck": {
|
||||
dispatcher({
|
||||
type: "UNREADS_MARK_READ",
|
||||
channel: packet.id,
|
||||
message: packet.message_id,
|
||||
request: false
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
message: (message: Message) => {
|
||||
if (message.mentions?.includes(client.user!._id)) {
|
||||
dispatcher({
|
||||
type: "UNREADS_MENTION",
|
||||
channel: message.channel,
|
||||
message: message._id
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ready: () => {
|
||||
setStatus(ClientStatus.ONLINE);
|
||||
}
|
||||
};
|
||||
|
||||
let listenerFunc: { [key: string]: Function };
|
||||
if (import.meta.env.DEV) {
|
||||
listenerFunc = {};
|
||||
for (const listener of Object.keys(listeners)) {
|
||||
listenerFunc[listener] = (...args: any[]) => {
|
||||
console.debug(`Calling ${listener} with`, args);
|
||||
(listeners as any)[listener](...args);
|
||||
};
|
||||
}
|
||||
} else {
|
||||
listenerFunc = listeners;
|
||||
}
|
||||
|
||||
for (const listener of Object.keys(listenerFunc)) {
|
||||
client.addListener(listener, (listenerFunc as any)[listener]);
|
||||
}
|
||||
|
||||
/*const online = () =>
|
||||
operations.ready() && setStatus(ClientStatus.RECONNECTING);
|
||||
const offline = () =>
|
||||
operations.ready() && setStatus(ClientStatus.OFFLINE);
|
||||
|
||||
window.addEventListener("online", online);
|
||||
window.addEventListener("offline", offline);
|
||||
|
||||
return () => {
|
||||
for (const listener of Object.keys(listenerFunc)) {
|
||||
RevoltClient.removeListener(listener, (listenerFunc as any)[listener]);
|
||||
}
|
||||
|
||||
window.removeEventListener("online", online);
|
||||
window.removeEventListener("offline", offline);
|
||||
};*/
|
||||
}
|
||||
108
src/context/revoltjs/hooks.ts
Normal file
108
src/context/revoltjs/hooks.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||
import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
|
||||
import { Client, PermissionCalculator } from 'revolt.js';
|
||||
import { AppContext } from "./RevoltClient";
|
||||
|
||||
interface HookContext {
|
||||
client: Client,
|
||||
forceUpdate: () => void
|
||||
}
|
||||
|
||||
export function useForceUpdate(context?: HookContext): HookContext {
|
||||
const { client } = useContext(AppContext);
|
||||
if (context) return context;
|
||||
const [, updateState] = useState({});
|
||||
return { client, forceUpdate: useCallback(() => updateState({}), []) };
|
||||
}
|
||||
|
||||
function useObject(type: string, id?: string | string[], context?: HookContext) {
|
||||
const ctx = useForceUpdate(context);
|
||||
|
||||
function mutation(target: string) {
|
||||
if (typeof id === 'string' ? target === id :
|
||||
Array.isArray(id) ? id.includes(target) : true) {
|
||||
ctx.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
const map = (ctx.client as any)[type];
|
||||
useEffect(() => {
|
||||
map.addListener("update", mutation);
|
||||
return () => map.removeListener("update", mutation);
|
||||
}, [id]);
|
||||
|
||||
return typeof id === 'string' ? map.get(id)
|
||||
: Array.isArray(id) ? id.map(x => map.get(x))
|
||||
: map.toArray();
|
||||
}
|
||||
|
||||
export function useUser(id?: string, context?: HookContext) {
|
||||
if (typeof id === "undefined") return;
|
||||
return useObject('users', id, context) as Readonly<Users.User> | undefined;
|
||||
}
|
||||
|
||||
export function useSelf(context?: HookContext) {
|
||||
const ctx = useForceUpdate(context);
|
||||
return useUser(ctx.client.user!._id, ctx);
|
||||
}
|
||||
|
||||
export function useUsers(ids?: string[], context?: HookContext) {
|
||||
return useObject('users', ids, context) as (Readonly<Users.User> | undefined)[];
|
||||
}
|
||||
|
||||
export function useChannel(id?: string, context?: HookContext) {
|
||||
if (typeof id === "undefined") return;
|
||||
return useObject('channels', id, context) as Readonly<Channels.Channel> | undefined;
|
||||
}
|
||||
|
||||
export function useChannels(ids?: string[], context?: HookContext) {
|
||||
return useObject('channels', ids, context) as (Readonly<Channels.Channel> | undefined)[];
|
||||
}
|
||||
|
||||
export function useServer(id?: string, context?: HookContext) {
|
||||
if (typeof id === "undefined") return;
|
||||
return useObject('servers', id, context) as Readonly<Servers.Server> | undefined;
|
||||
}
|
||||
|
||||
export function useServers(ids?: string[], context?: HookContext) {
|
||||
return useObject('servers', ids, context) as (Readonly<Servers.Server> | undefined)[];
|
||||
}
|
||||
|
||||
export function useUserPermission(id: string, context?: HookContext) {
|
||||
const ctx = useForceUpdate(context);
|
||||
|
||||
const mutation = (target: string) => (target === id) && ctx.forceUpdate();
|
||||
useEffect(() => {
|
||||
ctx.client.users.addListener("update", mutation);
|
||||
return () => ctx.client.users.removeListener("update", mutation);
|
||||
}, [id]);
|
||||
|
||||
let calculator = new PermissionCalculator(ctx.client);
|
||||
return calculator.forUser(id);
|
||||
}
|
||||
|
||||
export function useChannelPermission(id: string, context?: HookContext) {
|
||||
const ctx = useForceUpdate(context);
|
||||
|
||||
const mutation = (target: string) => (target === id) && ctx.forceUpdate();
|
||||
useEffect(() => {
|
||||
ctx.client.channels.addListener("update", mutation);
|
||||
return () => ctx.client.channels.removeListener("update", mutation);
|
||||
}, [id]);
|
||||
|
||||
let calculator = new PermissionCalculator(ctx.client);
|
||||
return calculator.forChannel(id);
|
||||
}
|
||||
|
||||
export function useServerPermission(id: string, context?: HookContext) {
|
||||
const ctx = useForceUpdate(context);
|
||||
|
||||
const mutation = (target: string) => (target === id) && ctx.forceUpdate();
|
||||
useEffect(() => {
|
||||
ctx.client.servers.addListener("update", mutation);
|
||||
return () => ctx.client.servers.removeListener("update", mutation);
|
||||
}, [id]);
|
||||
|
||||
let calculator = new PermissionCalculator(ctx.client);
|
||||
return calculator.forServer(id);
|
||||
}
|
||||
Reference in New Issue
Block a user