mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-23 22:22:07 -05:00
feat(mobx): add drafts and state context
This commit is contained in:
parent
66bfc658c3
commit
5a41c25e3c
10 changed files with 285 additions and 14 deletions
14
package.json
14
package.json
|
@ -5,7 +5,7 @@
|
|||
"pull": "node scripts/setup_assets.js",
|
||||
"build": "rimraf build && node scripts/setup_assets.js --check && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
||||
"lint": "eslint src/**/*.{js,jsx,ts,tsx}",
|
||||
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"start": "sirv dist --cors --single --host",
|
||||
|
@ -37,6 +37,18 @@
|
|||
{
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"require-jsdoc": [
|
||||
"error",
|
||||
{
|
||||
"require": {
|
||||
"FunctionDeclaration": true,
|
||||
"MethodDefinition": true,
|
||||
"ClassDeclaration": true,
|
||||
"ArrowFunctionExpression": false,
|
||||
"FunctionExpression": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
SMOOTH_SCROLL_ON_RECEIVE,
|
||||
} from "../../../lib/renderer/Singleton";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { dispatch, getState } from "../../../redux";
|
||||
import { Reply } from "../../../redux/reducers/queue";
|
||||
|
||||
|
@ -115,7 +116,7 @@ const RE_SED = new RegExp("^s/([^])*/([^])*$");
|
|||
export const CAN_UPLOAD_AT_ONCE = 4;
|
||||
|
||||
export default observer(({ channel }: Props) => {
|
||||
const [draft, setDraft] = useState(getState().drafts[channel._id] ?? "");
|
||||
const drafts = useApplicationState().draft;
|
||||
|
||||
const [uploadState, setUploadState] = useState<UploadState>({
|
||||
type: "none",
|
||||
|
@ -150,7 +151,7 @@ export default observer(({ channel }: Props) => {
|
|||
|
||||
const setMessage = useCallback(
|
||||
(content?: string) => {
|
||||
setDraft(content ?? "");
|
||||
drafts.set(channel._id, content);
|
||||
|
||||
if (content) {
|
||||
dispatch({
|
||||
|
@ -165,10 +166,15 @@ export default observer(({ channel }: Props) => {
|
|||
});
|
||||
}
|
||||
},
|
||||
[channel._id],
|
||||
[drafts, channel._id],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
*
|
||||
* @param content
|
||||
* @param action
|
||||
*/
|
||||
function append(content: string, action: "quote" | "mention") {
|
||||
const text =
|
||||
action === "quote"
|
||||
|
@ -178,10 +184,10 @@ export default observer(({ channel }: Props) => {
|
|||
.join("\n")}\n\n`
|
||||
: `${content} `;
|
||||
|
||||
if (!draft || draft.length === 0) {
|
||||
if (!drafts.has(channel._id)) {
|
||||
setMessage(text);
|
||||
} else {
|
||||
setMessage(`${draft}\n${text}`);
|
||||
setMessage(`${drafts.get(channel._id)}\n${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,13 +196,16 @@ export default observer(({ channel }: Props) => {
|
|||
"append",
|
||||
append as (...args: unknown[]) => void,
|
||||
);
|
||||
}, [draft, setMessage]);
|
||||
}, [drafts, channel._id, setMessage]);
|
||||
|
||||
/**
|
||||
* Trigger send message.
|
||||
*/
|
||||
async function send() {
|
||||
if (uploadState.type === "uploading" || uploadState.type === "sending")
|
||||
return;
|
||||
|
||||
const content = draft?.trim() ?? "";
|
||||
const content = drafts.get(channel._id)?.trim() ?? "";
|
||||
if (uploadState.type === "attached") return sendFile(content);
|
||||
if (content.length === 0) return;
|
||||
|
||||
|
@ -281,6 +290,11 @@ export default observer(({ channel }: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param content
|
||||
* @returns
|
||||
*/
|
||||
async function sendFile(content: string) {
|
||||
if (uploadState.type !== "attached") return;
|
||||
const attachments: string[] = [];
|
||||
|
@ -372,6 +386,10 @@ export default observer(({ channel }: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
function startTyping() {
|
||||
if (typeof typing === "number" && +new Date() < typing) return;
|
||||
|
||||
|
@ -385,6 +403,10 @@ export default observer(({ channel }: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param force
|
||||
*/
|
||||
function stopTyping(force?: boolean) {
|
||||
if (force || typing) {
|
||||
const ws = client.websocket;
|
||||
|
@ -503,7 +525,7 @@ export default observer(({ channel }: Props) => {
|
|||
id="message"
|
||||
maxLength={2000}
|
||||
onKeyUp={onKeyUp}
|
||||
value={draft ?? ""}
|
||||
value={drafts.get(channel._id) ?? ""}
|
||||
padding="var(--message-box-padding)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.ctrlKey && e.key === "Enter") {
|
||||
|
@ -513,10 +535,7 @@ export default observer(({ channel }: Props) => {
|
|||
|
||||
if (onKeyDown(e)) return;
|
||||
|
||||
if (
|
||||
e.key === "ArrowUp" &&
|
||||
(!draft || draft.length === 0)
|
||||
) {
|
||||
if (e.key === "ArrowUp" && !drafts.has(channel._id)) {
|
||||
e.preventDefault();
|
||||
internalEmit("MessageRenderer", "edit_last");
|
||||
return;
|
||||
|
|
|
@ -9,6 +9,10 @@ import Theme from "./Theme";
|
|||
import Intermediate from "./intermediate/Intermediate";
|
||||
import Client from "./revoltjs/RevoltClient";
|
||||
|
||||
/**
|
||||
* This component provides all of the application's context layers.
|
||||
* @param param0 Provided children
|
||||
*/
|
||||
export default function Context({ children }: { children: Children }) {
|
||||
return (
|
||||
<Router basename={import.meta.env.BASE_URL}>
|
||||
|
|
16
src/mobx/Persistent.ts
Normal file
16
src/mobx/Persistent.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* A data store which is persistent and should cache its data locally.
|
||||
*/
|
||||
export default interface Persistent<T> {
|
||||
/**
|
||||
* Override toJSON to serialise this data store.
|
||||
* This will also force all subclasses to implement this method.
|
||||
*/
|
||||
toJSON(): unknown;
|
||||
|
||||
/**
|
||||
* Hydrate this data store using given data.
|
||||
* @param data Given data
|
||||
*/
|
||||
hydrate(data: T): void;
|
||||
}
|
37
src/mobx/State.ts
Normal file
37
src/mobx/State.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
import { createContext } from "preact";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import Auth from "./stores/Auth";
|
||||
import Draft from "./stores/Draft";
|
||||
|
||||
/**
|
||||
* Handles global application state.
|
||||
*/
|
||||
export default class State {
|
||||
auth: Auth;
|
||||
draft: Draft;
|
||||
|
||||
/**
|
||||
* Construct new State.
|
||||
*/
|
||||
constructor() {
|
||||
this.auth = new Auth();
|
||||
this.draft = new Draft();
|
||||
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
}
|
||||
|
||||
const StateContext = createContext<State>(null!);
|
||||
|
||||
export const StateContextProvider = StateContext.Provider;
|
||||
|
||||
/**
|
||||
* Get the application state
|
||||
* @returns Application state
|
||||
*/
|
||||
export function useApplicationState() {
|
||||
return useContext(StateContext);
|
||||
}
|
14
src/mobx/TODO
Normal file
14
src/mobx/TODO
Normal file
|
@ -0,0 +1,14 @@
|
|||
auth
|
||||
drafts
|
||||
experiments
|
||||
last opened
|
||||
locale
|
||||
notifications
|
||||
queue
|
||||
section toggle
|
||||
serevr config
|
||||
settings
|
||||
sync
|
||||
themes
|
||||
trusted links
|
||||
unreads
|
6
src/mobx/objectUtil.ts
Normal file
6
src/mobx/objectUtil.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function deleteKey(object: any, key: string) {
|
||||
const newObject = { ...object };
|
||||
delete newObject[key];
|
||||
return newObject;
|
||||
}
|
70
src/mobx/stores/Auth.ts
Normal file
70
src/mobx/stores/Auth.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { makeAutoObservable } from "mobx";
|
||||
import { Session } from "revolt-api/types/Auth";
|
||||
import { Nullable } from "revolt.js/dist/util/null";
|
||||
|
||||
import Persistent from "../Persistent";
|
||||
import { deleteKey } from "../objectUtil";
|
||||
|
||||
interface Data {
|
||||
sessions: Record<string, Session>;
|
||||
current?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles account authentication, managing multiple
|
||||
* accounts and their sessions.
|
||||
*/
|
||||
export default class Auth implements Persistent<Data> {
|
||||
private sessions: Record<string, Session>;
|
||||
private current: Nullable<string>;
|
||||
|
||||
/**
|
||||
* Construct new Auth store.
|
||||
*/
|
||||
constructor() {
|
||||
this.sessions = {};
|
||||
this.current = null;
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
toJSON() {
|
||||
return {
|
||||
sessions: this.sessions,
|
||||
current: this.current,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
hydrate(data: Data) {
|
||||
this.sessions = data.sessions;
|
||||
if (data.current && this.sessions[data.current]) {
|
||||
this.current = data.current;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new session to the auth manager.
|
||||
* @param session Session
|
||||
*/
|
||||
setSession(session: Session) {
|
||||
this.sessions = {
|
||||
...this.sessions,
|
||||
[session.user_id]: session,
|
||||
};
|
||||
|
||||
this.current = session.user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove existing session by user ID.
|
||||
* @param user_id User ID tied to session
|
||||
*/
|
||||
removeSession(user_id: string) {
|
||||
this.sessions = deleteKey(this.sessions, user_id);
|
||||
|
||||
if (user_id == this.current) {
|
||||
this.current = null;
|
||||
}
|
||||
}
|
||||
}
|
80
src/mobx/stores/Draft.ts
Normal file
80
src/mobx/stores/Draft.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||
|
||||
import Persistent from "../Persistent";
|
||||
|
||||
interface Data {
|
||||
drafts: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles storing draft (currently being written) messages.
|
||||
*/
|
||||
export default class Draft implements Persistent<Data> {
|
||||
private drafts: ObservableMap<string, string>;
|
||||
|
||||
/**
|
||||
* Construct new Draft store.
|
||||
*/
|
||||
constructor() {
|
||||
this.drafts = new ObservableMap();
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
toJSON() {
|
||||
return {
|
||||
drafts: this.drafts,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
@action hydrate(data: Data) {
|
||||
Object.keys(data.drafts).forEach((key) =>
|
||||
this.drafts.set(key, data.drafts[key]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get draft for a channel.
|
||||
* @param channel Channel ID
|
||||
*/
|
||||
@computed get(channel: string) {
|
||||
return this.drafts.get(channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a channel has a draft.
|
||||
* @param channel Channel ID
|
||||
*/
|
||||
@computed has(channel: string) {
|
||||
return this.drafts.has(channel) && this.drafts.get(channel)!.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set draft for a channel.
|
||||
* @param channel Channel ID
|
||||
* @param content Draft content
|
||||
*/
|
||||
@action set(channel: string, content?: string) {
|
||||
if (typeof content === "undefined") {
|
||||
return this.clear(channel);
|
||||
}
|
||||
|
||||
this.drafts.set(channel, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear draft from a channel.
|
||||
* @param channel Channel ID
|
||||
*/
|
||||
@action clear(channel: string) {
|
||||
this.drafts.delete(channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset and clear all drafts.
|
||||
*/
|
||||
@action reset() {
|
||||
this.drafts.clear();
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ import { Provider } from "react-redux";
|
|||
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import MobXState, { StateContextProvider } from "../mobx/State";
|
||||
|
||||
import { dispatch, State, store } from ".";
|
||||
import { Children } from "../types/Preact";
|
||||
|
||||
|
@ -10,8 +12,13 @@ interface Props {
|
|||
children: Children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for loading application state.
|
||||
* @param props Provided children
|
||||
*/
|
||||
export default function StateLoader(props: Props) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [state] = useState(new MobXState());
|
||||
|
||||
useEffect(() => {
|
||||
localForage.getItem("state").then((state) => {
|
||||
|
@ -24,5 +31,11 @@ export default function StateLoader(props: Props) {
|
|||
}, []);
|
||||
|
||||
if (!loaded) return null;
|
||||
return <Provider store={store}>{props.children}</Provider>;
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<StateContextProvider value={state}>
|
||||
{props.children}
|
||||
</StateContextProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue