mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-24 16:10:59 -05:00
feat(mobx): migrate auth and config
This commit is contained in:
parent
bc799931a8
commit
f8b8d96d3d
22 changed files with 342 additions and 279 deletions
10
index.html
10
index.html
|
@ -1,10 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" background="#191919">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
<!--App Title-->
|
||||
<title>Revolt</title>
|
||||
<meta name="apple-mobile-web-app-title" content="Revolt" />
|
||||
|
||||
<!--App Scaling-->
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, user-scalable=no"
|
||||
|
@ -74,9 +77,4 @@
|
|||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
<style>
|
||||
html {
|
||||
background-color: #191919;
|
||||
}
|
||||
</style>
|
||||
</html>
|
||||
|
|
|
@ -49,7 +49,10 @@
|
|||
"FunctionExpression": false
|
||||
},
|
||||
"ignore": {
|
||||
"MethodDefinition": ["toJSON", "hydrate"]
|
||||
"MethodDefinition": [
|
||||
"toJSON",
|
||||
"hydrate"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -140,7 +143,7 @@
|
|||
"react-virtuoso": "^1.10.4",
|
||||
"redux": "^4.1.0",
|
||||
"revolt-api": "0.5.3-alpha.10",
|
||||
"revolt.js": "^5.1.0-alpha.10",
|
||||
"revolt.js": "5.1.0-alpha.15",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "^1.35.1",
|
||||
"shade-blend-color": "^1.0.0",
|
||||
|
|
|
@ -276,13 +276,13 @@ export const ServerListSidebar = observer(({ unreads }: Props) => {
|
|||
onClick={() =>
|
||||
homeActive && history.push("/settings")
|
||||
}>
|
||||
<UserHover user={client.user}>
|
||||
<UserHover user={client.user ?? undefined}>
|
||||
<Icon
|
||||
size={42}
|
||||
unread={homeUnread}
|
||||
count={alertCount}>
|
||||
<UserIcon
|
||||
target={client.user}
|
||||
target={client.user ?? undefined}
|
||||
size={32}
|
||||
status
|
||||
hover
|
||||
|
|
|
@ -205,7 +205,6 @@ export const Languages: { [key in Language]: LanguageEntry } = {
|
|||
|
||||
interface Props {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
locale: Language;
|
||||
}
|
||||
|
||||
export interface Dictionary {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Redirect } from "react-router-dom";
|
||||
|
||||
import { useContext } from "preact/hooks";
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import { Children } from "../../types/Preact";
|
||||
import { OperationsContext } from "./RevoltClient";
|
||||
import { useClient } from "./RevoltClient";
|
||||
|
||||
interface Props {
|
||||
auth?: boolean;
|
||||
|
@ -11,11 +11,13 @@ interface Props {
|
|||
}
|
||||
|
||||
export const CheckAuth = (props: Props) => {
|
||||
const operations = useContext(OperationsContext);
|
||||
const auth = useApplicationState().auth;
|
||||
const client = useClient();
|
||||
const ready = auth.isLoggedIn() && typeof client?.user !== "undefined";
|
||||
|
||||
if (props.auth && !operations.ready()) {
|
||||
if (props.auth && !ready) {
|
||||
return <Redirect to="/login" />;
|
||||
} else if (!props.auth && operations.ready()) {
|
||||
} else if (!props.auth && ready) {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { Session } from "revolt-api/types/Auth";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Client } from "revolt.js";
|
||||
import { Route } from "revolt.js/dist/api/routes";
|
||||
|
||||
import { createContext } from "preact";
|
||||
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
import { dispatch } from "../../redux";
|
||||
import { connectState } from "../../redux/connector";
|
||||
import { AuthState } from "../../redux/reducers/auth";
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import Preloader from "../../components/ui/Preloader";
|
||||
|
||||
import { Children } from "../../types/Preact";
|
||||
import { useIntermediate } from "../intermediate/Intermediate";
|
||||
import { registerEvents, setReconnectDisallowed } from "./events";
|
||||
import { registerEvents } from "./events";
|
||||
import { takeError } from "./util";
|
||||
|
||||
export enum ClientStatus {
|
||||
INIT,
|
||||
LOADING,
|
||||
READY,
|
||||
LOADING,
|
||||
OFFLINE,
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
|
@ -29,179 +25,75 @@ export enum ClientStatus {
|
|||
}
|
||||
|
||||
export interface ClientOperations {
|
||||
login: (
|
||||
data: Route<"POST", "/auth/session/login">["data"],
|
||||
) => Promise<void>;
|
||||
logout: (shouldRequest?: boolean) => Promise<void>;
|
||||
loggedIn: () => boolean;
|
||||
ready: () => boolean;
|
||||
}
|
||||
|
||||
// By the time they are used, they should all be initialized.
|
||||
// Currently the app does not render until a client is built and the other two are always initialized on first render.
|
||||
// - insert's words
|
||||
export const AppContext = createContext<Client>(null!);
|
||||
export const StatusContext = createContext<ClientStatus>(null!);
|
||||
export const OperationsContext = createContext<ClientOperations>(null!);
|
||||
export const LogOutContext = createContext(() => {});
|
||||
|
||||
type Props = {
|
||||
auth: AuthState;
|
||||
children: Children;
|
||||
};
|
||||
|
||||
function Context({ auth, children }: Props) {
|
||||
export default observer(({ children }: Props) => {
|
||||
const state = useApplicationState();
|
||||
const { openScreen } = useIntermediate();
|
||||
const [status, setStatus] = useState(ClientStatus.INIT);
|
||||
const [client, setClient] = useState<Client>(
|
||||
undefined as unknown as Client,
|
||||
);
|
||||
const [client, setClient] = useState<Client>(null!);
|
||||
const [status, setStatus] = useState(ClientStatus.LOADING);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
function logout() {
|
||||
setLoaded(false);
|
||||
client.logout(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const client = new Client({
|
||||
autoReconnect: false,
|
||||
apiURL: import.meta.env.VITE_API_URL,
|
||||
debug: import.meta.env.DEV,
|
||||
});
|
||||
|
||||
setClient(client);
|
||||
setStatus(ClientStatus.LOADING);
|
||||
})();
|
||||
if (navigator.onLine) {
|
||||
new Client().req("GET", "/").then(state.config.set);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (status === ClientStatus.INIT) return null;
|
||||
|
||||
const operations: ClientOperations = useMemo(() => {
|
||||
return {
|
||||
login: async (data) => {
|
||||
setReconnectDisallowed(true);
|
||||
|
||||
try {
|
||||
const onboarding = await client.login(data);
|
||||
setReconnectDisallowed(false);
|
||||
const login = () =>
|
||||
dispatch({
|
||||
type: "LOGIN",
|
||||
session: client.session as Session,
|
||||
});
|
||||
|
||||
if (onboarding) {
|
||||
openScreen({
|
||||
id: "onboarding",
|
||||
callback: async (username: string) =>
|
||||
onboarding(username, true).then(login),
|
||||
});
|
||||
} else {
|
||||
login();
|
||||
}
|
||||
} catch (err) {
|
||||
setReconnectDisallowed(false);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
logout: async (shouldRequest) => {
|
||||
dispatch({ type: "LOGOUT" });
|
||||
|
||||
client.reset();
|
||||
dispatch({ 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: () =>
|
||||
operations.loggedIn() && typeof client.user !== "undefined",
|
||||
};
|
||||
}, [client, auth.active, openScreen]);
|
||||
|
||||
useEffect(
|
||||
() => registerEvents({ operations }, setStatus, client),
|
||||
[client, operations],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (auth.active) {
|
||||
dispatch({ type: "QUEUE_FAIL_ALL" });
|
||||
if (state.auth.isLoggedIn()) {
|
||||
const client = state.config.createClient();
|
||||
setClient(client);
|
||||
|
||||
const active = auth.accounts[auth.active];
|
||||
client.user = client.users.get(active.session.user_id);
|
||||
if (!navigator.onLine) {
|
||||
return setStatus(ClientStatus.OFFLINE);
|
||||
}
|
||||
|
||||
if (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 });
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus(ClientStatus.DISCONNECTED);
|
||||
client
|
||||
.useExistingSession(state.auth.getSession()!)
|
||||
.then(() => setLoaded(true))
|
||||
.catch((err) => {
|
||||
const error = takeError(err);
|
||||
if (error === "Forbidden" || error === "Unauthorized") {
|
||||
operations.logout(true);
|
||||
client.logout(true);
|
||||
openScreen({ id: "signed_out" });
|
||||
} else {
|
||||
setStatus(ClientStatus.DISCONNECTED);
|
||||
openScreen({ id: "error", error });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await client.fetchConfiguration();
|
||||
} catch (err) {
|
||||
console.error("Failed to connect to API server.");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setStatus(ClientStatus.READY);
|
||||
setLoaded(true);
|
||||
}
|
||||
}, [state.auth.getSession()]);
|
||||
|
||||
setStatus(ClientStatus.READY);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
useEffect(() => registerEvents(state.auth, setStatus, client), [client]);
|
||||
|
||||
if (status === ClientStatus.LOADING) {
|
||||
if (!loaded || status === ClientStatus.LOADING) {
|
||||
return <Preloader type="spinner" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={client}>
|
||||
<StatusContext.Provider value={status}>
|
||||
<OperationsContext.Provider value={operations}>
|
||||
<LogOutContext.Provider value={logout}>
|
||||
{children}
|
||||
</OperationsContext.Provider>
|
||||
</LogOutContext.Provider>
|
||||
</StatusContext.Provider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectState<{ children: Children }>(Context, (state) => {
|
||||
return {
|
||||
auth: state.auth,
|
||||
sync: state.sync,
|
||||
};
|
||||
});
|
||||
|
||||
export const useClient = () => useContext(AppContext);
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
import { Language } from "../Locale";
|
||||
import { AppContext, ClientStatus, StatusContext } from "./RevoltClient";
|
||||
|
||||
type Props = {
|
||||
/*type Props = {
|
||||
settings: Settings;
|
||||
locale: Language;
|
||||
sync: SyncOptions;
|
||||
|
@ -150,4 +150,8 @@ export default connectState(SyncManager, (state) => {
|
|||
sync: state.sync,
|
||||
notifications: state.notifications,
|
||||
};
|
||||
});
|
||||
});*/
|
||||
|
||||
function SyncManager() {
|
||||
return <></>;
|
||||
}
|
||||
|
|
|
@ -4,9 +4,10 @@ import { ClientboundNotification } from "revolt.js/dist/websocket/notifications"
|
|||
|
||||
import { StateUpdater } from "preact/hooks";
|
||||
|
||||
import Auth from "../../mobx/stores/Auth";
|
||||
import { dispatch } from "../../redux";
|
||||
|
||||
import { ClientOperations, ClientStatus } from "./RevoltClient";
|
||||
import { ClientStatus } from "./RevoltClient";
|
||||
|
||||
export let preventReconnect = false;
|
||||
let preventUntil = 0;
|
||||
|
@ -16,10 +17,12 @@ export function setReconnectDisallowed(allowed: boolean) {
|
|||
}
|
||||
|
||||
export function registerEvents(
|
||||
{ operations }: { operations: ClientOperations },
|
||||
auth: Auth,
|
||||
setStatus: StateUpdater<ClientStatus>,
|
||||
client: Client,
|
||||
) {
|
||||
if (!client) return;
|
||||
|
||||
function attemptReconnect() {
|
||||
if (preventReconnect) return;
|
||||
function reconnect() {
|
||||
|
@ -36,14 +39,11 @@ export function registerEvents(
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let listeners: Record<string, (...args: any[]) => void> = {
|
||||
connecting: () =>
|
||||
operations.ready() && setStatus(ClientStatus.CONNECTING),
|
||||
connecting: () => setStatus(ClientStatus.CONNECTING),
|
||||
|
||||
dropped: () => {
|
||||
if (operations.ready()) {
|
||||
setStatus(ClientStatus.DISCONNECTED);
|
||||
attemptReconnect();
|
||||
}
|
||||
setStatus(ClientStatus.DISCONNECTED);
|
||||
attemptReconnect();
|
||||
},
|
||||
|
||||
packet: (packet: ClientboundNotification) => {
|
||||
|
@ -70,6 +70,11 @@ export function registerEvents(
|
|||
},
|
||||
|
||||
ready: () => setStatus(ClientStatus.ONLINE),
|
||||
|
||||
logout: () => {
|
||||
auth.logout();
|
||||
setStatus(ClientStatus.READY);
|
||||
},
|
||||
};
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
|
@ -89,19 +94,15 @@ export function registerEvents(
|
|||
}
|
||||
|
||||
const online = () => {
|
||||
if (operations.ready()) {
|
||||
setStatus(ClientStatus.RECONNECTING);
|
||||
setReconnectDisallowed(false);
|
||||
attemptReconnect();
|
||||
}
|
||||
setStatus(ClientStatus.RECONNECTING);
|
||||
setReconnectDisallowed(false);
|
||||
attemptReconnect();
|
||||
};
|
||||
|
||||
const offline = () => {
|
||||
if (operations.ready()) {
|
||||
setReconnectDisallowed(true);
|
||||
client.websocket.disconnect();
|
||||
setStatus(ClientStatus.OFFLINE);
|
||||
}
|
||||
setReconnectDisallowed(true);
|
||||
client.websocket.disconnect();
|
||||
setStatus(ClientStatus.OFFLINE);
|
||||
};
|
||||
|
||||
window.addEventListener("online", online);
|
||||
|
|
|
@ -105,14 +105,14 @@ type Action =
|
|||
| { action: "create_channel"; target: Server }
|
||||
| { action: "create_category"; target: Server }
|
||||
| {
|
||||
action: "create_invite";
|
||||
target: Channel;
|
||||
}
|
||||
action: "create_invite";
|
||||
target: Channel;
|
||||
}
|
||||
| { action: "leave_group"; target: Channel }
|
||||
| {
|
||||
action: "delete_channel";
|
||||
target: Channel;
|
||||
}
|
||||
action: "delete_channel";
|
||||
target: Channel;
|
||||
}
|
||||
| { action: "close_dm"; target: Channel }
|
||||
| { action: "leave_server"; target: Server }
|
||||
| { action: "delete_server"; target: Server }
|
||||
|
@ -123,10 +123,10 @@ type Action =
|
|||
| { action: "open_server_settings"; id: string }
|
||||
| { action: "open_server_channel_settings"; server: string; id: string }
|
||||
| {
|
||||
action: "set_notification_state";
|
||||
key: string;
|
||||
state?: NotificationState;
|
||||
};
|
||||
action: "set_notification_state";
|
||||
key: string;
|
||||
state?: NotificationState;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
notifications: Notifications;
|
||||
|
@ -488,8 +488,9 @@ function ContextMenus(props: Props) {
|
|||
elements.push(
|
||||
<MenuItem data={action} disabled={disabled}>
|
||||
<Text
|
||||
id={`app.context_menu.${locale ?? action.action
|
||||
}`}
|
||||
id={`app.context_menu.${
|
||||
locale ?? action.action
|
||||
}`}
|
||||
/>
|
||||
{tip && <div className="tip">{tip}</div>}
|
||||
</MenuItem>,
|
||||
|
@ -545,8 +546,8 @@ function ContextMenus(props: Props) {
|
|||
const user = uid ? client.users.get(uid) : undefined;
|
||||
const serverChannel =
|
||||
targetChannel &&
|
||||
(targetChannel.channel_type === "TextChannel" ||
|
||||
targetChannel.channel_type === "VoiceChannel")
|
||||
(targetChannel.channel_type === "TextChannel" ||
|
||||
targetChannel.channel_type === "VoiceChannel")
|
||||
? targetChannel
|
||||
: undefined;
|
||||
|
||||
|
@ -558,8 +559,8 @@ function ContextMenus(props: Props) {
|
|||
(server
|
||||
? server.permission
|
||||
: serverChannel
|
||||
? serverChannel.server?.permission
|
||||
: 0) || 0;
|
||||
? serverChannel.server?.permission
|
||||
: 0) || 0;
|
||||
const userPermissions = (user ? user.permission : 0) || 0;
|
||||
|
||||
if (unread) {
|
||||
|
@ -705,7 +706,8 @@ function ContextMenus(props: Props) {
|
|||
if (message && !queued) {
|
||||
const sendPermission =
|
||||
message.channel &&
|
||||
message.channel.permission & ChannelPermission.SendMessage
|
||||
message.channel.permission &
|
||||
ChannelPermission.SendMessage;
|
||||
|
||||
if (sendPermission) {
|
||||
generateAction({
|
||||
|
@ -741,7 +743,7 @@ function ContextMenus(props: Props) {
|
|||
if (
|
||||
message.author_id === userId ||
|
||||
channelPermissions &
|
||||
ChannelPermission.ManageMessages
|
||||
ChannelPermission.ManageMessages
|
||||
) {
|
||||
generateAction({
|
||||
action: "delete_message",
|
||||
|
@ -765,8 +767,8 @@ function ContextMenus(props: Props) {
|
|||
type === "Image"
|
||||
? "open_image"
|
||||
: type === "Video"
|
||||
? "open_video"
|
||||
: "open_file",
|
||||
? "open_video"
|
||||
: "open_file",
|
||||
);
|
||||
|
||||
generateAction(
|
||||
|
@ -777,8 +779,8 @@ function ContextMenus(props: Props) {
|
|||
type === "Image"
|
||||
? "save_image"
|
||||
: type === "Video"
|
||||
? "save_video"
|
||||
: "save_file",
|
||||
? "save_video"
|
||||
: "save_file",
|
||||
);
|
||||
|
||||
generateAction(
|
||||
|
@ -930,9 +932,9 @@ function ContextMenus(props: Props) {
|
|||
|
||||
if (
|
||||
serverPermissions &
|
||||
ServerPermission.ChangeNickname ||
|
||||
ServerPermission.ChangeNickname ||
|
||||
serverPermissions &
|
||||
ServerPermission.ChangeAvatar
|
||||
ServerPermission.ChangeAvatar
|
||||
)
|
||||
generateAction(
|
||||
{ action: "edit_identity", target: server },
|
||||
|
@ -976,10 +978,10 @@ function ContextMenus(props: Props) {
|
|||
sid
|
||||
? "copy_sid"
|
||||
: cid
|
||||
? "copy_cid"
|
||||
: message
|
||||
? "copy_mid"
|
||||
: "copy_uid",
|
||||
? "copy_cid"
|
||||
: message
|
||||
? "copy_mid"
|
||||
: "copy_uid",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import Draft from "./stores/Draft";
|
|||
import Experiments from "./stores/Experiments";
|
||||
import Layout from "./stores/Layout";
|
||||
import LocaleOptions from "./stores/LocaleOptions";
|
||||
import ServerConfig from "./stores/ServerConfig";
|
||||
|
||||
/**
|
||||
* Handles global application state.
|
||||
|
@ -20,6 +21,7 @@ export default class State {
|
|||
locale: LocaleOptions;
|
||||
experiments: Experiments;
|
||||
layout: Layout;
|
||||
config: ServerConfig;
|
||||
|
||||
private persistent: [string, Persistent<unknown>][] = [];
|
||||
|
||||
|
@ -32,12 +34,16 @@ export default class State {
|
|||
this.locale = new LocaleOptions();
|
||||
this.experiments = new Experiments();
|
||||
this.layout = new Layout();
|
||||
this.config = new ServerConfig();
|
||||
|
||||
makeAutoObservable(this);
|
||||
this.registerListeners = this.registerListeners.bind(this);
|
||||
this.register();
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorise and register stores referenced on this object.
|
||||
*/
|
||||
private register() {
|
||||
for (const key of Object.keys(this)) {
|
||||
const obj = (
|
||||
|
@ -65,12 +71,22 @@ export default class State {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register reaction listeners for persistent data stores.
|
||||
* @returns Function to dispose of listeners
|
||||
*/
|
||||
registerListeners() {
|
||||
const listeners = this.persistent.map(([id, store]) => {
|
||||
return reaction(
|
||||
() => store.toJSON(),
|
||||
(value) => {
|
||||
localforage.setItem(id, value);
|
||||
async (value) => {
|
||||
try {
|
||||
await localforage.setItem(id, value);
|
||||
} catch (err) {
|
||||
console.error("Failed to serialise!");
|
||||
console.error(err);
|
||||
console.error(value);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -78,6 +94,9 @@ export default class State {
|
|||
return () => listeners.forEach((x) => x());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load data stores from local storage.
|
||||
*/
|
||||
async hydrate() {
|
||||
for (const [id, store] of this.persistent) {
|
||||
const data = await localforage.getItem(id);
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import { makeAutoObservable, ObservableMap } from "mobx";
|
||||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||
import { Session } from "revolt-api/types/Auth";
|
||||
import { Nullable } from "revolt.js/dist/util/null";
|
||||
|
||||
import { mapToRecord } from "../../lib/conversion";
|
||||
|
||||
import Persistent from "../interfaces/Persistent";
|
||||
import Store from "../interfaces/Store";
|
||||
|
||||
interface Account {
|
||||
session: Session;
|
||||
}
|
||||
|
||||
interface Data {
|
||||
sessions: Record<string, Session>;
|
||||
sessions: Record<string, Account> | [string, Account][];
|
||||
current?: string;
|
||||
}
|
||||
|
||||
|
@ -15,7 +21,7 @@ interface Data {
|
|||
* accounts and their sessions.
|
||||
*/
|
||||
export default class Auth implements Store, Persistent<Data> {
|
||||
private sessions: ObservableMap<string, Session>;
|
||||
private sessions: ObservableMap<string, Account>;
|
||||
private current: Nullable<string>;
|
||||
|
||||
/**
|
||||
|
@ -31,17 +37,27 @@ export default class Auth implements Store, Persistent<Data> {
|
|||
return "auth";
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
@action toJSON() {
|
||||
return {
|
||||
sessions: [...this.sessions],
|
||||
sessions: JSON.parse(JSON.stringify(this.sessions)),
|
||||
current: this.current ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
hydrate(data: Data) {
|
||||
Object.keys(data.sessions).forEach((id) =>
|
||||
this.sessions.set(id, data.sessions[id]),
|
||||
);
|
||||
@action hydrate(data: Data) {
|
||||
if (Array.isArray(data.sessions)) {
|
||||
data.sessions.forEach(([key, value]) =>
|
||||
this.sessions.set(key, value),
|
||||
);
|
||||
} else if (
|
||||
typeof data.sessions === "object" &&
|
||||
data.sessions !== null
|
||||
) {
|
||||
let v = data.sessions;
|
||||
Object.keys(data.sessions).forEach((id) =>
|
||||
this.sessions.set(id, v[id]),
|
||||
);
|
||||
}
|
||||
|
||||
if (data.current && this.sessions.has(data.current)) {
|
||||
this.current = data.current;
|
||||
|
@ -52,8 +68,8 @@ export default class Auth implements Store, Persistent<Data> {
|
|||
* Add a new session to the auth manager.
|
||||
* @param session Session
|
||||
*/
|
||||
setSession(session: Session) {
|
||||
this.sessions.set(session.user_id, session);
|
||||
@action setSession(session: Session) {
|
||||
this.sessions.set(session.user_id, { session });
|
||||
this.current = session.user_id;
|
||||
}
|
||||
|
||||
|
@ -61,11 +77,28 @@ export default class Auth implements Store, Persistent<Data> {
|
|||
* Remove existing session by user ID.
|
||||
* @param user_id User ID tied to session
|
||||
*/
|
||||
removeSession(user_id: string) {
|
||||
this.sessions.delete(user_id);
|
||||
|
||||
@action removeSession(user_id: string) {
|
||||
if (user_id == this.current) {
|
||||
this.current = null;
|
||||
}
|
||||
|
||||
this.sessions.delete(user_id);
|
||||
}
|
||||
|
||||
@action logout() {
|
||||
this.current && this.removeSession(this.current);
|
||||
}
|
||||
|
||||
@computed getSession() {
|
||||
if (!this.current) return;
|
||||
return this.sessions.get(this.current)!.session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we are currently logged in.
|
||||
* @returns Whether we are logged in
|
||||
*/
|
||||
@computed isLoggedIn() {
|
||||
return this.current !== null;
|
||||
}
|
||||
}
|
||||
|
|
75
src/mobx/stores/ServerConfig.ts
Normal file
75
src/mobx/stores/ServerConfig.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { action, computed, makeAutoObservable } from "mobx";
|
||||
import { RevoltConfiguration } from "revolt-api/types/Core";
|
||||
import { Client } from "revolt.js";
|
||||
import { Nullable } from "revolt.js/dist/util/null";
|
||||
|
||||
import Persistent from "../interfaces/Persistent";
|
||||
import Store from "../interfaces/Store";
|
||||
|
||||
interface Data {
|
||||
config?: RevoltConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores server configuration data.
|
||||
*/
|
||||
export default class ServerConfig
|
||||
implements Store, Persistent<RevoltConfiguration>
|
||||
{
|
||||
private config: Nullable<RevoltConfiguration>;
|
||||
|
||||
/**
|
||||
* Construct new ServerConfig store.
|
||||
*/
|
||||
constructor() {
|
||||
this.config = null;
|
||||
makeAutoObservable(this);
|
||||
this.set = this.set.bind(this);
|
||||
}
|
||||
|
||||
get id() {
|
||||
return "server_conf";
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return JSON.parse(JSON.stringify(this.config));
|
||||
}
|
||||
|
||||
@action hydrate(data: RevoltConfiguration) {
|
||||
this.config = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Revolt client.
|
||||
* @returns Revolt client
|
||||
*/
|
||||
createClient() {
|
||||
const client = new Client({
|
||||
autoReconnect: false,
|
||||
apiURL: import.meta.env.VITE_API_URL,
|
||||
debug: import.meta.env.DEV,
|
||||
});
|
||||
|
||||
if (this.config !== null) {
|
||||
client.configuration = this.config;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server configuration.
|
||||
* @returns Server configuration
|
||||
*/
|
||||
@computed get() {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set server configuration.
|
||||
* @param config Server configuration
|
||||
*/
|
||||
@action set(config: RevoltConfiguration) {
|
||||
this.config = config;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { LIBRARY_VERSION } from "revolt.js";
|
||||
|
@ -6,22 +7,24 @@ import styles from "./Login.module.scss";
|
|||
import { Text } from "preact-i18n";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import { ThemeContext } from "../../context/Theme";
|
||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import LocaleSelector from "../../components/common/LocaleSelector";
|
||||
import background from "./background.jpg";
|
||||
|
||||
import { Titlebar } from "../../components/native/Titlebar";
|
||||
import { APP_VERSION } from "../../version";
|
||||
import background from "./background.jpg";
|
||||
import { FormCreate } from "./forms/FormCreate";
|
||||
import { FormLogin } from "./forms/FormLogin";
|
||||
import { FormReset, FormSendReset } from "./forms/FormReset";
|
||||
import { FormResend, FormVerify } from "./forms/FormVerify";
|
||||
|
||||
export default function Login() {
|
||||
export default observer(() => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const client = useContext(AppContext);
|
||||
const configuration = useApplicationState().config.get();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -35,8 +38,7 @@ export default function Login() {
|
|||
<div className={styles.content}>
|
||||
<div className={styles.attribution}>
|
||||
<span>
|
||||
API:{" "}
|
||||
<code>{client.configuration?.revolt ?? "???"}</code>{" "}
|
||||
API: <code>{configuration?.revolt ?? "???"}</code>{" "}
|
||||
· revolt.js: <code>{LIBRARY_VERSION}</code>{" "}
|
||||
· App: <code>{APP_VERSION}</code>
|
||||
</span>
|
||||
|
@ -80,4 +82,4 @@ export default function Login() {
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import styles from "../Login.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
import { useEffect } from "preact/hooks";
|
||||
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import Preloader from "../../../components/ui/Preloader";
|
||||
|
||||
|
@ -13,22 +14,22 @@ export interface CaptchaProps {
|
|||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function CaptchaBlock(props: CaptchaProps) {
|
||||
const client = useContext(AppContext);
|
||||
export const CaptchaBlock = observer((props: CaptchaProps) => {
|
||||
const configuration = useApplicationState().config.get();
|
||||
|
||||
useEffect(() => {
|
||||
if (!client.configuration?.features.captcha.enabled) {
|
||||
if (!configuration?.features.captcha.enabled) {
|
||||
props.onSuccess();
|
||||
}
|
||||
}, [client.configuration?.features.captcha.enabled, props]);
|
||||
}, [configuration?.features.captcha.enabled, props]);
|
||||
|
||||
if (!client.configuration?.features.captcha.enabled)
|
||||
if (!configuration?.features.captcha.enabled)
|
||||
return <Preloader type="spinner" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HCaptcha
|
||||
sitekey={client.configuration.features.captcha.key}
|
||||
sitekey={configuration.features.captcha.key}
|
||||
onVerify={(token) => props.onSuccess(token)}
|
||||
/>
|
||||
<div className={styles.footer}>
|
||||
|
@ -38,4 +39,4 @@ export function CaptchaBlock(props: CaptchaProps) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -6,6 +6,8 @@ import styles from "../Login.module.scss";
|
|||
import { Text } from "preact-i18n";
|
||||
import { useContext, useState } from "preact/hooks";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { takeError } from "../../../context/revoltjs/util";
|
||||
|
||||
|
@ -44,7 +46,7 @@ interface FormInputs {
|
|||
}
|
||||
|
||||
export function Form({ page, callback }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const configuration = useApplicationState().config.get();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState<string | undefined>(undefined);
|
||||
|
@ -80,10 +82,7 @@ export function Form({ page, callback }: Props) {
|
|||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
client.configuration?.features.captcha.enabled &&
|
||||
page !== "reset"
|
||||
) {
|
||||
if (configuration?.features.captcha.enabled && page !== "reset") {
|
||||
setCaptcha({
|
||||
onSuccess: async (captcha) => {
|
||||
setCaptcha(undefined);
|
||||
|
@ -111,7 +110,7 @@ export function Form({ page, callback }: Props) {
|
|||
if (typeof success !== "undefined") {
|
||||
return (
|
||||
<div className={styles.success}>
|
||||
{client.configuration?.features.email ? (
|
||||
{configuration?.features.email ? (
|
||||
<>
|
||||
<Envelope size={72} />
|
||||
<h2>
|
||||
|
@ -172,15 +171,14 @@ export function Form({ page, callback }: Props) {
|
|||
error={errors.password?.message}
|
||||
/>
|
||||
)}
|
||||
{client.configuration?.features.invite_only &&
|
||||
page === "create" && (
|
||||
<FormField
|
||||
type="invite"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.invite?.message}
|
||||
/>
|
||||
)}
|
||||
{configuration?.features.invite_only && page === "create" && (
|
||||
<FormField
|
||||
type="invite"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.invite?.message}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<Overline type="error" error={error}>
|
||||
<Text id={`login.error.${page}`} />
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { Form } from "./Form";
|
||||
|
||||
export function FormCreate() {
|
||||
const client = useContext(AppContext);
|
||||
const config = useApplicationState().config;
|
||||
const client = config.createClient();
|
||||
return <Form page="create" callback={(data) => client.register(data)} />;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { detect } from "detect-browser";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Session } from "revolt-api/types/Auth";
|
||||
import { Client } from "revolt.js";
|
||||
|
||||
import { useContext } from "preact/hooks";
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { OperationsContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
import { Form } from "./Form";
|
||||
|
||||
export function FormLogin() {
|
||||
const { login } = useContext(OperationsContext);
|
||||
const history = useHistory();
|
||||
const auth = useApplicationState().auth;
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
return (
|
||||
<Form
|
||||
|
@ -34,8 +35,40 @@ export function FormLogin() {
|
|||
friendly_name = "Unknown Device";
|
||||
}
|
||||
|
||||
await login({ ...data, friendly_name });
|
||||
history.push("/");
|
||||
// ! FIXME: temporary login flow code
|
||||
// This should be replaced in the future.
|
||||
const client = new Client();
|
||||
await client.fetchConfiguration();
|
||||
const session = (await client.req(
|
||||
"POST",
|
||||
"/auth/session/login",
|
||||
{ ...data, friendly_name },
|
||||
)) as unknown as Session;
|
||||
|
||||
client.session = session;
|
||||
(client as any).Axios.defaults.headers = {
|
||||
"x-session-token": session?.token,
|
||||
};
|
||||
|
||||
function login() {
|
||||
auth.setSession(session);
|
||||
}
|
||||
|
||||
const { onboarding } = await client.req(
|
||||
"GET",
|
||||
"/onboard/hello",
|
||||
);
|
||||
if (onboarding) {
|
||||
openScreen({
|
||||
id: "onboarding",
|
||||
callback: async (username: string) =>
|
||||
client
|
||||
.completeOnboarding({ username }, false)
|
||||
.then(login),
|
||||
});
|
||||
} else {
|
||||
login();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -2,12 +2,15 @@ import { useHistory, useParams } from "react-router-dom";
|
|||
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { Form } from "./Form";
|
||||
|
||||
export function FormSendReset() {
|
||||
const client = useContext(AppContext);
|
||||
const config = useApplicationState().config;
|
||||
const client = config.createClient();
|
||||
|
||||
return (
|
||||
<Form
|
||||
|
|
|
@ -2,6 +2,8 @@ import { useHistory, useParams } from "react-router-dom";
|
|||
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { takeError } from "../../../context/revoltjs/util";
|
||||
|
||||
|
@ -11,7 +13,8 @@ import Preloader from "../../../components/ui/Preloader";
|
|||
import { Form } from "./Form";
|
||||
|
||||
export function FormResend() {
|
||||
const client = useContext(AppContext);
|
||||
const config = useApplicationState().config;
|
||||
const client = config.createClient();
|
||||
|
||||
return (
|
||||
<Form
|
||||
|
|
|
@ -32,7 +32,6 @@ function mapMailProvider(email?: string): [string, string] | undefined {
|
|||
case "outlook.com.br":
|
||||
case "outlook.cl":
|
||||
case "outlook.cz":
|
||||
case "outlook.dk":
|
||||
case "outlook.com.gr":
|
||||
case "outlook.co.il":
|
||||
case "outlook.in":
|
||||
|
|
|
@ -29,10 +29,7 @@ import { useContext } from "preact/hooks";
|
|||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import RequiresOnline from "../../context/revoltjs/RequiresOnline";
|
||||
import {
|
||||
AppContext,
|
||||
OperationsContext,
|
||||
} from "../../context/revoltjs/RevoltClient";
|
||||
import { AppContext, LogOutContext } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import LineDivider from "../../components/ui/LineDivider";
|
||||
|
||||
|
@ -57,7 +54,7 @@ import { ThemeShop } from "./panes/ThemeShop";
|
|||
export default observer(() => {
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
const operations = useContext(OperationsContext);
|
||||
const logout = useContext(LogOutContext);
|
||||
const experiments = useApplicationState().experiments;
|
||||
|
||||
function switchPage(to?: string) {
|
||||
|
@ -220,7 +217,7 @@ export default observer(() => {
|
|||
</a>
|
||||
<LineDivider />
|
||||
<ButtonItem
|
||||
onClick={() => operations.logout()}
|
||||
onClick={logout}
|
||||
className={styles.logOut}
|
||||
compact>
|
||||
<LogOut size={20} />
|
||||
|
|
|
@ -3765,10 +3765,10 @@ revolt-api@^0.5.3-alpha.9:
|
|||
resolved "https://registry.yarnpkg.com/revolt-api/-/revolt-api-0.5.3-alpha.9.tgz#46e75b7d8f9c6702df39039b829dddbb7897f237"
|
||||
integrity sha512-L8K9uPV3ME8bLdtWm8L9iPQvFM0GghA+5LzmWFjd6Gbn56u22ZYub2lABi4iHrWgeA2X41dGSsuSBgHSlts9Og==
|
||||
|
||||
revolt.js@^5.1.0-alpha.10:
|
||||
version "5.1.0-alpha.10"
|
||||
resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.1.0-alpha.10.tgz#e393ac8524e629d3359135651b23b044c0cc9b7b"
|
||||
integrity sha512-wEmBMJkZE/oWy6mzVZg1qw5QC9CE+Gb7sTFlJl+C4pbXfTJWAtY311Tjbd2tX8w3ohYDmN338bVfCW4cOQ8GXQ==
|
||||
revolt.js@5.1.0-alpha.15:
|
||||
version "5.1.0-alpha.15"
|
||||
resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.1.0-alpha.15.tgz#a2be1f29de93f1ec18f0e502ecb65ade55c0070d"
|
||||
integrity sha512-1gGcGDv1+J5NlmnX099XafKugCebACg9ke0NA754I4hLTNMMwkZyphyvYWWWkI394qn2mA3NG7WgEmrIoZUtgw==
|
||||
dependencies:
|
||||
axios "^0.21.4"
|
||||
eventemitter3 "^4.0.7"
|
||||
|
|
Loading…
Reference in a new issue