mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-24 22:52:09 -05:00
feat(mobx): refactor and remove (react-)redux
This commit is contained in:
parent
6e1bcab92b
commit
cc0e45526c
55 changed files with 249 additions and 1522 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -10,3 +10,5 @@ dist-ssr
|
|||
public/assets
|
||||
public/assets_*
|
||||
!public/assets_default
|
||||
|
||||
.vscode/vscode-chrome-debug-userdatadir
|
||||
|
|
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://local.revolt.chat:3000",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"runtimeExecutable": "/usr/bin/chromium",
|
||||
"userDataDir": "${workspaceFolder}/.vscode/vscode-chrome-debug-userdatadir"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -136,14 +136,12 @@
|
|||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "6.3.0",
|
||||
"react-overlapping-panels": "1.2.2",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scroll": "^1.8.2",
|
||||
"react-virtualized-auto-sizer": "^1.0.5",
|
||||
"react-virtuoso": "^1.10.4",
|
||||
"redux": "^4.1.0",
|
||||
"revolt-api": "0.5.3-alpha.10",
|
||||
"revolt.js": "5.2.0-patch.0",
|
||||
"revolt.js": "5.2.1-patch.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "^1.35.1",
|
||||
"shade-blend-color": "^1.0.0",
|
||||
|
|
|
@ -6,7 +6,8 @@ import styled from "styled-components";
|
|||
import { Text } from "preact-i18n";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import { dispatch, getState } from "../../redux";
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import { SECTION_NSFW } from "../../mobx/stores/Layout";
|
||||
|
||||
import Button from "../ui/Button";
|
||||
import Checkbox from "../ui/Checkbox";
|
||||
|
@ -49,9 +50,7 @@ type Props = {
|
|||
|
||||
export default observer((props: Props) => {
|
||||
const history = useHistory();
|
||||
const [consent, setConsent] = useState(
|
||||
getState().sectionToggle["nsfw"] ?? false,
|
||||
);
|
||||
const layout = useApplicationState().layout;
|
||||
const [ageGate, setAgeGate] = useState(false);
|
||||
|
||||
if (ageGate || !props.gated) {
|
||||
|
@ -81,26 +80,19 @@ export default observer((props: Props) => {
|
|||
</span>
|
||||
|
||||
<Checkbox
|
||||
checked={consent}
|
||||
onChange={(v) => {
|
||||
setConsent(v);
|
||||
if (v) {
|
||||
dispatch({
|
||||
type: "SECTION_TOGGLE_SET",
|
||||
id: "nsfw",
|
||||
state: true,
|
||||
});
|
||||
} else {
|
||||
dispatch({ type: "SECTION_TOGGLE_UNSET", id: "nsfw" });
|
||||
}
|
||||
}}>
|
||||
checked={layout.getSectionState(SECTION_NSFW, false)}
|
||||
onChange={() => layout.toggleSectionState(SECTION_NSFW, false)}>
|
||||
<Text id="app.main.channel.nsfw.confirm" />
|
||||
</Checkbox>
|
||||
<div className="actions">
|
||||
<Button contrast onClick={() => history.goBack()}>
|
||||
<Text id="app.special.modals.actions.back" />
|
||||
</Button>
|
||||
<Button contrast onClick={() => consent && setAgeGate(true)}>
|
||||
<Button
|
||||
contrast
|
||||
onClick={() =>
|
||||
layout.getSectionState(SECTION_NSFW) && setAgeGate(true)
|
||||
}>
|
||||
<Text id={`app.main.channel.nsfw.${props.type}.confirm`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useState } from "preact/hooks";
|
|||
|
||||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
|
||||
import { QueuedMessage } from "../../../redux/reducers/queue";
|
||||
import { QueuedMessage } from "../../../mobx/stores/MessageQueue";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from "../../../lib/renderer/Singleton";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { Reply } from "../../../redux/reducers/queue";
|
||||
import { Reply } from "../../../mobx/stores/MessageQueue";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import {
|
||||
|
@ -111,7 +111,7 @@ const Action = styled.div`
|
|||
const RE_SED = new RegExp("^s/([^])*/([^])*$");
|
||||
|
||||
// ! FIXME: add to app config and load from app config
|
||||
export const CAN_UPLOAD_AT_ONCE = 4;
|
||||
export const CAN_UPLOAD_AT_ONCE = 5;
|
||||
|
||||
export default observer(({ channel }: Props) => {
|
||||
const state = useApplicationState();
|
||||
|
|
|
@ -10,8 +10,9 @@ import { StateUpdater, useEffect } from "preact/hooks";
|
|||
|
||||
import { internalSubscribe } from "../../../../lib/eventEmitter";
|
||||
|
||||
import { dispatch, getState } from "../../../../redux";
|
||||
import { Reply } from "../../../../redux/reducers/queue";
|
||||
import { useApplicationState } from "../../../../mobx/State";
|
||||
import { SECTION_MENTION } from "../../../../mobx/stores/Layout";
|
||||
import { Reply } from "../../../../mobx/stores/MessageQueue";
|
||||
|
||||
import IconButton from "../../../ui/IconButton";
|
||||
|
||||
|
@ -81,6 +82,7 @@ const Base = styled.div`
|
|||
const MAX_REPLIES = 5;
|
||||
export default observer(({ channel, replies, setReplies }: Props) => {
|
||||
const client = channel.client;
|
||||
const layout = useApplicationState().layout;
|
||||
|
||||
// Event listener for adding new messages to reply bar.
|
||||
useEffect(() => {
|
||||
|
@ -99,7 +101,7 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||
mention:
|
||||
message.author_id === client.user!._id
|
||||
? false
|
||||
: getState().sectionToggle.mention ?? false,
|
||||
: layout.getSectionState("SECTION_MENTION", false),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -181,11 +183,11 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
|||
}),
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: "SECTION_TOGGLE_SET",
|
||||
id: "mention",
|
||||
layout.setSectionState(
|
||||
SECTION_MENTION,
|
||||
state,
|
||||
});
|
||||
false,
|
||||
);
|
||||
}}>
|
||||
<span class="toggle">
|
||||
<At size={15} />
|
||||
|
|
|
@ -10,8 +10,6 @@ import { useContext, useEffect, useState } from "preact/hooks";
|
|||
import { defer } from "../../../../lib/defer";
|
||||
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { dispatch } from "../../../../redux";
|
||||
|
||||
import {
|
||||
AppContext,
|
||||
ClientStatus,
|
||||
|
@ -33,7 +31,7 @@ const EmbedInviteBase = styled.div`
|
|||
align-items: center;
|
||||
padding: 0 12px;
|
||||
margin-top: 2px;
|
||||
${() =>
|
||||
${() =>
|
||||
isTouchscreenDevice &&
|
||||
css`
|
||||
flex-wrap: wrap;
|
||||
|
@ -44,19 +42,17 @@ const EmbedInviteBase = styled.div`
|
|||
> button {
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const EmbedInviteDetails = styled.div`
|
||||
flex-grow: 1;
|
||||
padding-left: 12px;
|
||||
${() =>
|
||||
${() =>
|
||||
isTouchscreenDevice &&
|
||||
css`
|
||||
width: calc(100% - 55px);
|
||||
`
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const EmbedInviteName = styled.div`
|
||||
|
@ -74,11 +70,10 @@ type Props = {
|
|||
code: string;
|
||||
};
|
||||
|
||||
export function EmbedInvite(props: Props) {
|
||||
export function EmbedInvite({ code }: Props) {
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
const status = useContext(StatusContext);
|
||||
const code = props.code;
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
const [joinError, setJoinError] = useState<string | undefined>(undefined);
|
||||
|
@ -124,7 +119,8 @@ export function EmbedInvite(props: Props) {
|
|||
<EmbedInviteDetails>
|
||||
<EmbedInviteName>{invite.server_name}</EmbedInviteName>
|
||||
<EmbedInviteMemberCount>
|
||||
{invite.member_count.toLocaleString()} {invite.member_count === 1 ? "member" : "members"}
|
||||
{invite.member_count.toLocaleString()}{" "}
|
||||
{invite.member_count === 1 ? "member" : "members"}
|
||||
</EmbedInviteMemberCount>
|
||||
</EmbedInviteDetails>
|
||||
{processing ? (
|
||||
|
@ -151,10 +147,9 @@ export function EmbedInvite(props: Props) {
|
|||
|
||||
defer(() => {
|
||||
if (server) {
|
||||
dispatch({
|
||||
type: "UNREADS_MARK_MULTIPLE_READ",
|
||||
channels: server.channel_ids,
|
||||
});
|
||||
client.unreads!.markMultipleRead(
|
||||
server.channel_ids,
|
||||
);
|
||||
|
||||
history.push(
|
||||
`/server/${server._id}/channel/${invite.channel_id}`,
|
||||
|
@ -172,7 +167,9 @@ export function EmbedInvite(props: Props) {
|
|||
setProcessing(false);
|
||||
}
|
||||
}}>
|
||||
{client.servers.get(invite.server_id) ? "Joined" : "Join"}
|
||||
{client.servers.get(invite.server_id)
|
||||
? "Joined"
|
||||
: "Join"}
|
||||
</Button>
|
||||
)}
|
||||
</EmbedInviteBase>
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { Route, Switch } from "react-router";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import { SIDEBAR_CHANNELS } from "../../mobx/stores/Layout";
|
||||
|
||||
import SidebarBase from "./SidebarBase";
|
||||
import HomeSidebar from "./left/HomeSidebar";
|
||||
import ServerListSidebar from "./left/ServerListSidebar";
|
||||
import ServerSidebar from "./left/ServerSidebar";
|
||||
import { useSelector } from "react-redux";
|
||||
import { State } from "../../redux";
|
||||
|
||||
export default function LeftSidebar() {
|
||||
const isOpen = useSelector((state: State) => state.sectionToggle['sidebar_channels'] ?? true)
|
||||
const layout = useApplicationState().layout;
|
||||
const isOpen = layout.getSectionState(SIDEBAR_CHANNELS, true);
|
||||
|
||||
return (
|
||||
<SidebarBase>
|
||||
|
|
|
@ -11,7 +11,6 @@ import { internalEmit } from "../../../lib/eventEmitter";
|
|||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
import { reaction } from "mobx";
|
||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||
|
||||
import { useLayoutEffect } from "preact/hooks";
|
||||
|
||||
import { dispatch } from "../../../redux";
|
||||
import { Unreads } from "../../../redux/reducers/unreads";
|
||||
|
||||
type UnreadProps = {
|
||||
channel: Channel;
|
||||
unreads: Unreads;
|
||||
};
|
||||
|
||||
export function useUnreads({ channel, unreads }: UnreadProps) {
|
||||
// const firstLoad = useRef(true);
|
||||
useLayoutEffect(() => {
|
||||
function checkUnread(target: Channel) {
|
||||
if (!target) return;
|
||||
if (target._id !== channel._id) return;
|
||||
if (
|
||||
target.channel_type === "SavedMessages" ||
|
||||
target.channel_type === "VoiceChannel"
|
||||
)
|
||||
return;
|
||||
|
||||
const unread = unreads[channel._id]?.last_id;
|
||||
if (target.last_message_id) {
|
||||
if (
|
||||
!unread ||
|
||||
(unread && target.last_message_id.localeCompare(unread) > 0)
|
||||
) {
|
||||
dispatch({
|
||||
type: "UNREADS_MARK_READ",
|
||||
channel: channel._id,
|
||||
message: target.last_message_id,
|
||||
});
|
||||
|
||||
channel.ack(target.last_message_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkUnread(channel);
|
||||
return reaction(
|
||||
() => channel.last_message,
|
||||
() => checkUnread(channel),
|
||||
);
|
||||
}, [channel, unreads]);
|
||||
}
|
||||
|
||||
export function mapChannelWithUnread(channel: Channel, unreads: Unreads) {
|
||||
const last_message_id = channel.last_message_id;
|
||||
|
||||
let unread: "mention" | "unread" | undefined;
|
||||
let alertCount: undefined | number;
|
||||
if (last_message_id && unreads) {
|
||||
const u = unreads[channel._id];
|
||||
if (u) {
|
||||
if (u.mentions && u.mentions.length > 0) {
|
||||
alertCount = u.mentions.length;
|
||||
unread = "mention";
|
||||
} else if (
|
||||
u.last_id &&
|
||||
(last_message_id as string).localeCompare(u.last_id) > 0
|
||||
) {
|
||||
unread = "unread";
|
||||
}
|
||||
} else {
|
||||
unread = "unread";
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
channel,
|
||||
timestamp: last_message_id ?? channel._id,
|
||||
unread,
|
||||
alertCount,
|
||||
};
|
||||
}
|
|
@ -167,7 +167,7 @@ export default function Modal(props: Props) {
|
|||
isModalClosing = animateClose;
|
||||
const onClose = useCallback(() => {
|
||||
setAnimateClose(true);
|
||||
setTimeout(() => props.onClose?.(), 2e2);
|
||||
setTimeout(() => props.onClose!(), 2e2);
|
||||
}, [setAnimateClose, props]);
|
||||
|
||||
useEffect(() => internalSubscribe("Modal", "close", onClose), [onClose]);
|
||||
|
|
|
@ -5,7 +5,6 @@ import { createGlobalStyle } from "styled-components";
|
|||
import { useEffect } from "preact/hooks";
|
||||
|
||||
import { useApplicationState } from "../mobx/State";
|
||||
import { getState } from "../redux";
|
||||
|
||||
export type Variables =
|
||||
| "accent"
|
||||
|
@ -280,28 +279,6 @@ export const PRESETS: Record<string, Theme> = {
|
|||
},
|
||||
};
|
||||
|
||||
// todo: store used themes locally
|
||||
export function getBaseTheme(name: string): Theme {
|
||||
if (name in PRESETS) {
|
||||
return PRESETS[name];
|
||||
}
|
||||
|
||||
// TODO: properly initialize `themes` in state instead of letting it be undefined
|
||||
const themes = getState().themes ?? {};
|
||||
|
||||
if (name in themes) {
|
||||
const { theme } = themes[name];
|
||||
|
||||
return {
|
||||
...PRESETS[theme.light ? "light" : "dark"],
|
||||
...theme,
|
||||
};
|
||||
}
|
||||
|
||||
// how did we get here
|
||||
return PRESETS["dark"];
|
||||
}
|
||||
|
||||
const keys = Object.keys(PRESETS.dark);
|
||||
const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
|
||||
:root {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
|
||||
import State from "../redux/State";
|
||||
|
||||
import { Children } from "../types/Preact";
|
||||
import Locale from "./Locale";
|
||||
import Theme from "./Theme";
|
||||
|
@ -15,14 +13,12 @@ import Client from "./revoltjs/RevoltClient";
|
|||
export default function Context({ children }: { children: Children }) {
|
||||
return (
|
||||
<Router basename={import.meta.env.BASE_URL}>
|
||||
<State>
|
||||
<Locale>
|
||||
<Intermediate>
|
||||
<Client>{children}</Client>
|
||||
</Intermediate>
|
||||
</Locale>
|
||||
<Theme />
|
||||
</State>
|
||||
<Locale>
|
||||
<Intermediate>
|
||||
<Client>{children}</Client>
|
||||
</Intermediate>
|
||||
</Locale>
|
||||
<Theme />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Text } from "preact-i18n";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { dispatch } from "../../../redux";
|
||||
|
||||
import Modal from "../../../components/ui/Modal";
|
||||
|
||||
|
|
|
@ -6,31 +6,28 @@ import { Message } from "revolt.js/dist/maps/Messages";
|
|||
import { useContext, useEffect } from "preact/hooks";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import { connectState } from "../../redux/connector";
|
||||
import { QueuedMessage } from "../../redux/reducers/queue";
|
||||
|
||||
import { setGlobalEmojiPack } from "../../components/common/Emoji";
|
||||
|
||||
import { AppContext } from "./RevoltClient";
|
||||
|
||||
type Props = {
|
||||
messages: QueuedMessage[];
|
||||
};
|
||||
|
||||
function StateMonitor(props: Props) {
|
||||
export default function StateMonitor() {
|
||||
const client = useContext(AppContext);
|
||||
const state = useApplicationState();
|
||||
|
||||
useEffect(() => {
|
||||
function add(msg: Message) {
|
||||
if (!msg.nonce) return;
|
||||
if (!props.messages.find((x) => x.id === msg.nonce)) return;
|
||||
if (
|
||||
!state.queue.get(msg.channel_id).find((x) => x.id === msg.nonce)
|
||||
)
|
||||
return;
|
||||
state.queue.remove(msg.nonce);
|
||||
}
|
||||
|
||||
client.addListener("message", add);
|
||||
return () => client.removeListener("message", add);
|
||||
}, [client, props.messages]);
|
||||
}, [client]);
|
||||
|
||||
// Set global emoji pack.
|
||||
useEffect(() => {
|
||||
|
@ -40,9 +37,3 @@ function StateMonitor(props: Props) {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default connectState(StateMonitor, (state) => {
|
||||
return {
|
||||
messages: [...state.queue],
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,25 +1,6 @@
|
|||
/**
|
||||
* This file monitors changes to settings and syncs them to the server.
|
||||
*/
|
||||
import isEqual from "lodash.isequal";
|
||||
import { UserSettings } from "revolt-api/types/Sync";
|
||||
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
||||
|
||||
import { useCallback, useContext, useEffect, useMemo } from "preact/hooks";
|
||||
|
||||
import { dispatch } from "../../redux";
|
||||
import { connectState } from "../../redux/connector";
|
||||
import { Notifications } from "../../redux/reducers/notifications";
|
||||
import { Settings } from "../../redux/reducers/settings";
|
||||
import {
|
||||
DEFAULT_ENABLED_SYNC,
|
||||
SyncData,
|
||||
SyncKeys,
|
||||
SyncOptions,
|
||||
} from "../../redux/reducers/sync";
|
||||
|
||||
import { Language } from "../Locale";
|
||||
import { AppContext, ClientStatus, StatusContext } from "./RevoltClient";
|
||||
|
||||
/*type Props = {
|
||||
settings: Settings;
|
||||
|
|
|
@ -5,7 +5,6 @@ import { ClientboundNotification } from "revolt.js/dist/websocket/notifications"
|
|||
import { StateUpdater } from "preact/hooks";
|
||||
|
||||
import Auth from "../../mobx/stores/Auth";
|
||||
import { dispatch } from "../../redux";
|
||||
|
||||
import { ClientStatus } from "./RevoltClient";
|
||||
|
||||
|
@ -46,29 +45,6 @@ export function registerEvents(
|
|||
attemptReconnect();
|
||||
},
|
||||
|
||||
packet: (packet: ClientboundNotification) => {
|
||||
switch (packet.type) {
|
||||
case "ChannelAck": {
|
||||
dispatch({
|
||||
type: "UNREADS_MARK_READ",
|
||||
channel: packet.id,
|
||||
message: packet.message_id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
message: (message: Message) => {
|
||||
if (message.mention_ids?.includes(client.user!._id)) {
|
||||
dispatch({
|
||||
type: "UNREADS_MENTION",
|
||||
channel: message.channel_id,
|
||||
message: message._id,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ready: () => setStatus(ClientStatus.ONLINE),
|
||||
|
||||
logout: () => {
|
||||
|
|
|
@ -33,14 +33,8 @@ import { Text } from "preact-i18n";
|
|||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { useApplicationState } from "../mobx/State";
|
||||
import { dispatch } from "../redux";
|
||||
import { connectState } from "../redux/connector";
|
||||
import {
|
||||
getNotificationState,
|
||||
Notifications,
|
||||
NotificationState,
|
||||
} from "../redux/reducers/notifications";
|
||||
import { QueuedMessage } from "../redux/reducers/queue";
|
||||
import { QueuedMessage } from "../mobx/stores/MessageQueue";
|
||||
import { NotificationState } from "../mobx/stores/NotificationOptions";
|
||||
|
||||
import { Screen, useIntermediate } from "../context/intermediate/Intermediate";
|
||||
import {
|
||||
|
@ -174,21 +168,19 @@ export default function ContextMenus() {
|
|||
)
|
||||
return;
|
||||
|
||||
dispatch({
|
||||
type: "UNREADS_MARK_READ",
|
||||
channel: data.channel._id,
|
||||
message: data.channel.last_message_id!,
|
||||
});
|
||||
|
||||
data.channel.ack(undefined, true);
|
||||
client.unreads!.markRead(
|
||||
data.channel._id,
|
||||
data.channel.last_message_id!,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "mark_server_as_read":
|
||||
{
|
||||
dispatch({
|
||||
type: "UNREADS_MARK_MULTIPLE_READ",
|
||||
channels: data.server.channel_ids,
|
||||
});
|
||||
client.unreads!.markMultipleRead(
|
||||
data.server.channel_ids,
|
||||
);
|
||||
|
||||
data.server.ack();
|
||||
}
|
||||
|
@ -439,16 +431,6 @@ export default function ContextMenus() {
|
|||
case "open_server_settings":
|
||||
history.push(`/server/${data.id}/settings`);
|
||||
break;
|
||||
|
||||
case "set_notification_state": {
|
||||
const { key, state } = data;
|
||||
if (state) {
|
||||
dispatch({ type: "NOTIFICATIONS_SET", key, state });
|
||||
} else {
|
||||
dispatch({ type: "NOTIFICATIONS_REMOVE", key });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
})().catch((err) => {
|
||||
openScreen({ id: "error", error: takeError(err) });
|
||||
|
|
|
@ -14,6 +14,7 @@ import MessageQueue from "./stores/MessageQueue";
|
|||
import NotificationOptions from "./stores/NotificationOptions";
|
||||
import ServerConfig from "./stores/ServerConfig";
|
||||
import Settings from "./stores/Settings";
|
||||
import Sync from "./stores/Sync";
|
||||
|
||||
/**
|
||||
* Handles global application state.
|
||||
|
@ -28,6 +29,7 @@ export default class State {
|
|||
notifications: NotificationOptions;
|
||||
queue: MessageQueue;
|
||||
settings: Settings;
|
||||
sync: Sync;
|
||||
|
||||
private persistent: [string, Persistent<unknown>][] = [];
|
||||
|
||||
|
@ -44,6 +46,7 @@ export default class State {
|
|||
this.notifications = new NotificationOptions();
|
||||
this.queue = new MessageQueue();
|
||||
this.settings = new Settings();
|
||||
this.sync = new Sync();
|
||||
|
||||
makeAutoObservable(this);
|
||||
this.registerListeners = this.registerListeners.bind(this);
|
||||
|
@ -116,14 +119,25 @@ export default class State {
|
|||
}
|
||||
}
|
||||
|
||||
const StateContext = createContext<State>(null!);
|
||||
|
||||
export const StateContextProvider = StateContext.Provider;
|
||||
var state: State;
|
||||
|
||||
/**
|
||||
* Get the application state
|
||||
* @returns Application state
|
||||
*/
|
||||
export function useApplicationState() {
|
||||
return useContext(StateContext);
|
||||
if (!state) state = new State();
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Redux hydration:
|
||||
* localForage.getItem("state").then((s) => {
|
||||
if (s !== null) {
|
||||
dispatch({ type: "__INIT", state: s as State });
|
||||
}
|
||||
|
||||
state.hydrate().then(() => setLoaded(true));
|
||||
});
|
||||
*/
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
split settings per account(?)
|
||||
multiple accounts need to be supported
|
||||
|
||||
redux -> mobx migration (wipe existing redux data post-migration)
|
||||
|
||||
> look into talking with other tabs to detect multiple instances
|
||||
> (also use this to tell the user to close all tabs before updating)
|
||||
|
||||
write new settings data structures for server-side
|
||||
---- (deprecate existing API and replace with new endpoints?)
|
||||
alternatively: keep using current system and eventually migrate
|
||||
or: handle both incoming types of data and keep newer version (v1_prefix)
|
||||
need to document these data structures
|
||||
|
||||
provide state globally? perform all authentication from inside mobx
|
||||
mobx parent holds client information and prepares us for first render
|
||||
|
||||
reasoning for global:
|
||||
|
||||
- we can't and won't have more than one of the application running in a single tab
|
||||
- interactions become simpler
|
||||
- all accounts will be managed from one place anyways
|
||||
|
||||
things such as unreads can pass through this data store providing a host of
|
||||
information, such as whether there are any alerts on channels, etc
|
|
@ -1,4 +1,4 @@
|
|||
import { AuthState } from "../../redux/reducers/auth";
|
||||
import { Session } from "revolt-api/types/Auth";
|
||||
|
||||
import { Language } from "../../context/Locale";
|
||||
import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme";
|
||||
|
@ -7,6 +7,7 @@ import { Data as DataAuth } from "../stores/Auth";
|
|||
import { Data as DataLocaleOptions } from "../stores/LocaleOptions";
|
||||
import { Data as DataNotificationOptions } from "../stores/NotificationOptions";
|
||||
import { ISettings } from "../stores/Settings";
|
||||
import { Data as DataSync } from "../stores/Sync";
|
||||
|
||||
export type LegacyTheme = Overrides & {
|
||||
light?: boolean;
|
||||
|
@ -39,7 +40,29 @@ export interface LegacySyncData {
|
|||
notifications?: LegacyNotifications;
|
||||
}
|
||||
|
||||
function legacyMigrateAuth(auth: AuthState): DataAuth {
|
||||
export type LegacySyncKeys =
|
||||
| "theme"
|
||||
| "appearance"
|
||||
| "locale"
|
||||
| "notifications";
|
||||
|
||||
export interface LegacySyncOptions {
|
||||
disabled?: LegacySyncKeys[];
|
||||
revision?: {
|
||||
[key: string]: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LegacyAuthState {
|
||||
accounts: {
|
||||
[key: string]: {
|
||||
session: Session;
|
||||
};
|
||||
};
|
||||
active?: string;
|
||||
}
|
||||
|
||||
function legacyMigrateAuth(auth: LegacyAuthState): DataAuth {
|
||||
return {
|
||||
current: auth.active,
|
||||
sessions: auth.accounts,
|
||||
|
@ -82,3 +105,12 @@ function legacyMigrateNotification(
|
|||
channel,
|
||||
};
|
||||
}
|
||||
|
||||
function legacyMigrateSync(sync: LegacySyncOptions): DataSync {
|
||||
return {
|
||||
disabled: sync.disabled ?? [],
|
||||
revision: {
|
||||
...sync.revision,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,6 +12,11 @@ export interface Data {
|
|||
openSections?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export const SIDEBAR_MEMBERS = "sidebar_members";
|
||||
export const SIDEBAR_CHANNELS = "sidebar_channels";
|
||||
export const SECTION_MENTION = "mention";
|
||||
export const SECTION_NSFW = "nsfw";
|
||||
|
||||
/**
|
||||
* Keeps track of the last open channels, tabs, etc.
|
||||
* Handles providing good UX experience on navigating
|
||||
|
@ -165,4 +170,13 @@ export default class Layout implements Store, Persistent<Data> {
|
|||
this.openSections.set(id, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle state of a section.
|
||||
* @param id Section ID
|
||||
* @param def Default state value
|
||||
*/
|
||||
@action toggleSectionState(id: string, def?: boolean) {
|
||||
this.setSectionState(id, !this.getSectionState(id, def));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,10 +78,14 @@ export default class Settings implements Store, Persistent<ISettings> {
|
|||
/**
|
||||
* Get a settings key.
|
||||
* @param key Colon-divided key
|
||||
* @param defaultValue Default value if not present
|
||||
* @returns Value at key
|
||||
*/
|
||||
@computed get<T extends keyof ISettings>(key: T) {
|
||||
return this.data.get(key) as ISettings[T] | undefined;
|
||||
@computed get<T extends keyof ISettings>(
|
||||
key: T,
|
||||
defaultValue?: ISettings[T],
|
||||
) {
|
||||
return (this.data.get(key) as ISettings[T] | undefined) ?? defaultValue;
|
||||
}
|
||||
|
||||
@action remove<T extends keyof ISettings>(key: T) {
|
||||
|
|
|
@ -23,6 +23,9 @@ export const SYNC_KEYS: SyncKeys[] = [
|
|||
|
||||
export interface Data {
|
||||
disabled: SyncKeys[];
|
||||
revision: {
|
||||
[key: string]: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,12 +33,14 @@ export interface Data {
|
|||
*/
|
||||
export default class Sync implements Store, Persistent<Data> {
|
||||
private disabled: ObservableSet<SyncKeys>;
|
||||
private revision: ObservableMap<SyncKeys, number>;
|
||||
|
||||
/**
|
||||
* Construct new Sync store.
|
||||
*/
|
||||
constructor() {
|
||||
this.disabled = new ObservableSet();
|
||||
this.revision = new ObservableMap();
|
||||
makeAutoObservable(this);
|
||||
this.isEnabled = this.isEnabled.bind(this);
|
||||
}
|
||||
|
@ -47,6 +52,7 @@ export default class Sync implements Store, Persistent<Data> {
|
|||
toJSON() {
|
||||
return {
|
||||
enabled: [...this.disabled],
|
||||
revision: mapToRecord(this.revision),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -58,6 +64,22 @@ export default class Sync implements Store, Persistent<Data> {
|
|||
}
|
||||
}
|
||||
|
||||
@action enable(key: SyncKeys) {
|
||||
this.disabled.delete(key);
|
||||
}
|
||||
|
||||
@action disable(key: SyncKeys) {
|
||||
this.disabled.add(key);
|
||||
}
|
||||
|
||||
@action toggle(key: SyncKeys) {
|
||||
if (this.isEnabled(key)) {
|
||||
this.disable(key);
|
||||
} else {
|
||||
this.enable(key);
|
||||
}
|
||||
}
|
||||
|
||||
@computed isEnabled(key: SyncKeys) {
|
||||
return !this.disabled.has(key);
|
||||
}
|
||||
|
|
|
@ -7,11 +7,12 @@ import { Channel as ChannelI } from "revolt.js/dist/maps/Channels";
|
|||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { useEffect } from "preact/hooks";
|
||||
|
||||
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
||||
|
||||
import { dispatch, getState } from "../../redux";
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import { SIDEBAR_MEMBERS } from "../../mobx/stores/Layout";
|
||||
|
||||
import { useClient } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
|
@ -83,15 +84,8 @@ export function Channel({ id }: { id: string }) {
|
|||
return <TextChannel channel={channel} />;
|
||||
}
|
||||
|
||||
const MEMBERS_SIDEBAR_KEY = "sidebar_members";
|
||||
const CHANNELS_SIDEBAR_KEY = "sidebar_channels";
|
||||
const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
|
||||
const [showMembers, setMembers] = useState(
|
||||
getState().sectionToggle[MEMBERS_SIDEBAR_KEY] ?? true,
|
||||
);
|
||||
const [showChannels, setChannels] = useState(
|
||||
getState().sectionToggle[CHANNELS_SIDEBAR_KEY] ?? true,
|
||||
);
|
||||
const layout = useApplicationState().layout;
|
||||
|
||||
// Mark channel as read.
|
||||
useEffect(() => {
|
||||
|
@ -121,45 +115,7 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
|
|||
channel.nsfw
|
||||
)
|
||||
}>
|
||||
<ChannelHeader
|
||||
channel={channel}
|
||||
toggleSidebar={() => {
|
||||
setMembers(!showMembers);
|
||||
|
||||
if (showMembers) {
|
||||
dispatch({
|
||||
type: "SECTION_TOGGLE_SET",
|
||||
id: MEMBERS_SIDEBAR_KEY,
|
||||
state: false,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: "SECTION_TOGGLE_UNSET",
|
||||
id: MEMBERS_SIDEBAR_KEY,
|
||||
});
|
||||
}
|
||||
}}
|
||||
toggleChannelSidebar={() => {
|
||||
if (isTouchscreenDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
setChannels(!showChannels);
|
||||
|
||||
if (showChannels) {
|
||||
dispatch({
|
||||
type: "SECTION_TOGGLE_SET",
|
||||
id: CHANNELS_SIDEBAR_KEY,
|
||||
state: false,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: "SECTION_TOGGLE_UNSET",
|
||||
id: CHANNELS_SIDEBAR_KEY,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChannelHeader channel={channel} />
|
||||
<ChannelMain>
|
||||
<ChannelContent>
|
||||
<VoiceHeader id={channel._id} />
|
||||
|
@ -168,7 +124,10 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
|
|||
<JumpToBottom channel={channel} />
|
||||
<MessageBox channel={channel} />
|
||||
</ChannelContent>
|
||||
{!isTouchscreenDevice && showMembers && <RightSidebar />}
|
||||
{!isTouchscreenDevice &&
|
||||
layout.getSectionState(SIDEBAR_MEMBERS, true) && (
|
||||
<RightSidebar />
|
||||
)}
|
||||
</ChannelMain>
|
||||
</AgeGate>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { At, Hash, Menu } from "@styled-icons/boxicons-regular";
|
||||
import { At, Hash } from "@styled-icons/boxicons-regular";
|
||||
import { Notepad, Group } from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||
|
@ -7,6 +7,9 @@ import styled, { css } from "styled-components";
|
|||
|
||||
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import { SIDEBAR_MEMBERS } from "../../mobx/stores/Layout";
|
||||
|
||||
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
||||
import { getChannelName } from "../../context/revoltjs/util";
|
||||
|
||||
|
@ -69,14 +72,16 @@ const IconConainer = styled.div`
|
|||
cursor: pointer;
|
||||
color: var(--secondary-foreground);
|
||||
|
||||
${!isTouchscreenDevice && css`
|
||||
${!isTouchscreenDevice &&
|
||||
css`
|
||||
&:hover {
|
||||
color: var(--foreground);
|
||||
}
|
||||
`}
|
||||
`
|
||||
`;
|
||||
|
||||
export default observer(({ channel, toggleSidebar, toggleChannelSidebar }: ChannelHeaderProps) => {
|
||||
export default observer(({ channel }: ChannelHeaderProps) => {
|
||||
const layout = useApplicationState().layout;
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
const name = getChannelName(channel);
|
||||
|
@ -100,7 +105,12 @@ export default observer(({ channel, toggleSidebar, toggleChannelSidebar }: Chann
|
|||
return (
|
||||
<Header placement="primary">
|
||||
<HamburgerAction />
|
||||
<IconConainer onClick={toggleChannelSidebar}>{icon}</IconConainer>
|
||||
<IconConainer
|
||||
onClick={() =>
|
||||
layout.toggleSectionState(SIDEBAR_MEMBERS, true)
|
||||
}>
|
||||
{icon}
|
||||
</IconConainer>
|
||||
<Info>
|
||||
<span className="name">{name}</span>
|
||||
{isTouchscreenDevice &&
|
||||
|
@ -143,7 +153,7 @@ export default observer(({ channel, toggleSidebar, toggleChannelSidebar }: Chann
|
|||
</>
|
||||
)}
|
||||
</Info>
|
||||
<HeaderActions channel={channel} toggleSidebar={toggleSidebar} />
|
||||
<HeaderActions channel={channel} />
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -14,6 +14,9 @@ import { internalEmit } from "../../../lib/eventEmitter";
|
|||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { SIDEBAR_MEMBERS } from "../../../mobx/stores/Layout";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
import UpdateIndicator from "../../../components/common/UpdateIndicator";
|
||||
|
@ -21,10 +24,8 @@ import IconButton from "../../../components/ui/IconButton";
|
|||
|
||||
import { ChannelHeaderProps } from "../ChannelHeader";
|
||||
|
||||
export default function HeaderActions({
|
||||
channel,
|
||||
toggleSidebar,
|
||||
}: ChannelHeaderProps) {
|
||||
export default function HeaderActions({ channel }: ChannelHeaderProps) {
|
||||
const layout = useApplicationState().layout;
|
||||
const { openScreen } = useIntermediate();
|
||||
const history = useHistory();
|
||||
|
||||
|
@ -40,7 +41,7 @@ export default function HeaderActions({
|
|||
if (isTouchscreenDevice) {
|
||||
openRightSidebar();
|
||||
} else {
|
||||
toggleSidebar?.();
|
||||
layout.toggleSectionState(SIDEBAR_MEMBERS, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,15 +10,12 @@ import styled from "styled-components";
|
|||
import { decodeTime } from "ulid";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { memo } from "preact/compat";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
|
||||
import { ChannelRenderer } from "../../../lib/renderer/Singleton";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { QueuedMessage } from "../../../redux/reducers/queue";
|
||||
|
||||
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
|
|
|
@ -4,11 +4,12 @@ import styled, { css } from "styled-components";
|
|||
|
||||
import styles from "./Home.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useState } from "preact/hooks";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
||||
|
||||
import { dispatch, getState } from "../../redux";
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import { SIDEBAR_CHANNELS } from "../../mobx/stores/Layout";
|
||||
|
||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
|
@ -18,8 +19,6 @@ import Tooltip from "../../components/common/Tooltip";
|
|||
import Header from "../../components/ui/Header";
|
||||
import CategoryButton from "../../components/ui/fluent/CategoryButton";
|
||||
|
||||
const CHANNELS_SIDEBAR_KEY = "sidebar_channels";
|
||||
|
||||
const IconConainer = styled.div`
|
||||
cursor: pointer;
|
||||
color: var(--secondary-foreground);
|
||||
|
@ -34,29 +33,14 @@ const IconConainer = styled.div`
|
|||
|
||||
export default function Home() {
|
||||
const client = useContext(AppContext);
|
||||
const [showChannels, setChannels] = useState(
|
||||
getState().sectionToggle[CHANNELS_SIDEBAR_KEY] ?? true,
|
||||
);
|
||||
const layout = useApplicationState().layout;
|
||||
|
||||
const toggleChannelSidebar = () => {
|
||||
if (isTouchscreenDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
setChannels(!showChannels);
|
||||
|
||||
if (showChannels) {
|
||||
dispatch({
|
||||
type: "SECTION_TOGGLE_SET",
|
||||
id: CHANNELS_SIDEBAR_KEY,
|
||||
state: false,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: "SECTION_TOGGLE_UNSET",
|
||||
id: CHANNELS_SIDEBAR_KEY,
|
||||
});
|
||||
}
|
||||
layout.toggleSectionState(SIDEBAR_CHANNELS, true);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -10,8 +10,6 @@ import { useContext, useEffect, useState } from "preact/hooks";
|
|||
import { defer } from "../../lib/defer";
|
||||
import { TextReact } from "../../lib/i18n";
|
||||
|
||||
import { dispatch } from "../../redux";
|
||||
|
||||
import RequiresOnline from "../../context/revoltjs/RequiresOnline";
|
||||
import {
|
||||
AppContext,
|
||||
|
@ -168,11 +166,9 @@ export default function Invite() {
|
|||
|
||||
defer(() => {
|
||||
if (server) {
|
||||
dispatch({
|
||||
type: "UNREADS_MARK_MULTIPLE_READ",
|
||||
channels:
|
||||
server.channel_ids,
|
||||
});
|
||||
client.unreads!.markMultipleRead(
|
||||
server.channel_ids,
|
||||
);
|
||||
|
||||
history.push(
|
||||
`/server/${server._id}/channel/${invite.channel_id}`,
|
||||
|
|
|
@ -6,8 +6,6 @@ import { TextReact } from "../../../lib/i18n";
|
|||
import { stopPropagation } from "../../../lib/stopPropagation";
|
||||
import { voiceState } from "../../../lib/vortex/VoiceState";
|
||||
|
||||
import { connectState } from "../../../redux/connector";
|
||||
|
||||
import Button from "../../../components/ui/Button";
|
||||
import ComboBox from "../../../components/ui/ComboBox";
|
||||
import Overline from "../../../components/ui/Overline";
|
||||
|
@ -17,7 +15,7 @@ const constraints = { audio: true };
|
|||
|
||||
// TODO: do not rewrite this code until voice is rewritten!
|
||||
|
||||
export function Component() {
|
||||
export function Audio() {
|
||||
const [mediaStream, setMediaStream] = useState<MediaStream | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
@ -163,7 +161,3 @@ function changeAudioDevice(deviceId: string, deviceType: string) {
|
|||
window.localStorage.setItem("audioOutputDevice", deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
export const Audio = connectState(Component, () => {
|
||||
return;
|
||||
});
|
||||
|
|
|
@ -4,12 +4,7 @@ import styles from "./Panes.module.scss";
|
|||
import { Text } from "preact-i18n";
|
||||
import { useMemo } from "preact/hooks";
|
||||
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import LocaleOptions from "../../../mobx/stores/LocaleOptions";
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
|
||||
import {
|
||||
Language,
|
||||
|
|
|
@ -5,23 +5,16 @@ import { useContext, useEffect, useState } from "preact/hooks";
|
|||
import { urlBase64ToUint8Array } from "../../../lib/conversion";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { NotificationOptions } from "../../../redux/reducers/settings";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
|
||||
interface Props {
|
||||
options?: NotificationOptions;
|
||||
}
|
||||
|
||||
export function Component({ options }: Props) {
|
||||
export function Notifications() {
|
||||
const client = useContext(AppContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const sounds = useApplicationState().settings.sounds;
|
||||
const settings = useApplicationState().settings;
|
||||
const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
|
||||
undefined,
|
||||
);
|
||||
|
@ -43,7 +36,7 @@ export function Component({ options }: Props) {
|
|||
</h3>
|
||||
<Checkbox
|
||||
disabled={!("Notification" in window)}
|
||||
checked={options?.desktopEnabled ?? false}
|
||||
checked={settings.get("notifications:desktop", false)!}
|
||||
description={
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_desktop" />
|
||||
}
|
||||
|
@ -51,6 +44,7 @@ export function Component({ options }: Props) {
|
|||
if (desktopEnabled) {
|
||||
const permission =
|
||||
await Notification.requestPermission();
|
||||
|
||||
if (permission !== "granted") {
|
||||
return openScreen({
|
||||
id: "error",
|
||||
|
@ -59,10 +53,7 @@ export function Component({ options }: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: { desktopEnabled },
|
||||
});
|
||||
settings.set("notifications:desktop", desktopEnabled);
|
||||
}}>
|
||||
<Text id="app.settings.pages.notifications.enable_desktop" />
|
||||
</Checkbox>
|
||||
|
@ -115,20 +106,16 @@ export function Component({ options }: Props) {
|
|||
<h3>
|
||||
<Text id="app.settings.pages.notifications.sounds" />
|
||||
</h3>
|
||||
{sounds.getState().map(({ id, enabled }) => (
|
||||
{settings.sounds.getState().map(({ id, enabled }) => (
|
||||
<Checkbox
|
||||
key={id}
|
||||
checked={enabled}
|
||||
onChange={(enabled) => sounds.setEnabled(id, enabled)}>
|
||||
onChange={(enabled) =>
|
||||
settings.sounds.setEnabled(id, enabled)
|
||||
}>
|
||||
<Text id={`app.settings.pages.notifications.sound.${id}`} />
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Notifications = connectState(Component, (state) => {
|
||||
return {
|
||||
options: state.settings.notification,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import styles from "./Panes.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { SyncKeys, SyncOptions } from "../../../redux/reducers/sync";
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { SyncKeys } from "../../../mobx/stores/Sync";
|
||||
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
|
||||
interface Props {
|
||||
options?: SyncOptions;
|
||||
}
|
||||
export const Sync = observer(() => {
|
||||
const sync = useApplicationState().sync;
|
||||
|
||||
export function Component(props: Props) {
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
|
@ -27,31 +26,16 @@ export function Component(props: Props) {
|
|||
).map(([key, title]) => (
|
||||
<Checkbox
|
||||
key={key}
|
||||
checked={
|
||||
(props.options?.disabled ?? []).indexOf(key) === -1
|
||||
}
|
||||
checked={sync.isEnabled(key)}
|
||||
description={
|
||||
<Text
|
||||
id={`app.settings.pages.sync.descriptions.${key}`}
|
||||
/>
|
||||
}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: enabled
|
||||
? "SYNC_ENABLE_KEY"
|
||||
: "SYNC_DISABLE_KEY",
|
||||
key,
|
||||
})
|
||||
}>
|
||||
onChange={() => sync.toggle(key)}>
|
||||
<Text id={`app.settings.pages.${title}`} />
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Sync = connectState(Component, (state) => {
|
||||
return {
|
||||
options: state.sync,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -3,9 +3,8 @@ import styled from "styled-components";
|
|||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { dispatch } from "../../../redux";
|
||||
|
||||
import { Theme, generateVariables, ThemeOptions } from "../../../context/Theme";
|
||||
import { Theme, generateVariables } from "../../../context/Theme";
|
||||
|
||||
import Tip from "../../../components/ui/Tip";
|
||||
import previewPath from "../assets/preview.svg";
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import localForage from "localforage";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import MobXState, { StateContextProvider } from "../mobx/State";
|
||||
|
||||
import { dispatch, State, store } from ".";
|
||||
import { Children } from "../types/Preact";
|
||||
|
||||
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 { current: state } = useRef(new MobXState());
|
||||
|
||||
// Globally expose the application state.
|
||||
useEffect(() => {
|
||||
(window as unknown as Record<string, unknown>).state = state;
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
localForage.getItem("state").then((s) => {
|
||||
if (s !== null) {
|
||||
dispatch({ type: "__INIT", state: s as State });
|
||||
}
|
||||
|
||||
state.hydrate().then(() => setLoaded(true));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!loaded) return null;
|
||||
|
||||
useEffect(state.registerListeners);
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<StateContextProvider value={state}>
|
||||
{props.children}
|
||||
</StateContextProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { connect, ConnectedComponent } from "react-redux";
|
||||
|
||||
import { h } from "preact";
|
||||
import { memo } from "preact/compat";
|
||||
|
||||
import { State } from ".";
|
||||
|
||||
export function connectState<T>(
|
||||
component: (props: any) => h.JSX.Element | null,
|
||||
mapKeys: (state: State, props: T) => any,
|
||||
memoize?: boolean,
|
||||
): ConnectedComponent<(props: any) => h.JSX.Element | null, T> {
|
||||
const c = connect(mapKeys)(component);
|
||||
return memoize ? memo(c) : c;
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
import localForage from "localforage";
|
||||
import { createStore } from "redux";
|
||||
import { RevoltConfiguration } from "revolt-api/types/Core";
|
||||
|
||||
import { Language } from "../context/Locale";
|
||||
|
||||
import rootReducer, { Action } from "./reducers";
|
||||
import { AuthState } from "./reducers/auth";
|
||||
import { Drafts } from "./reducers/drafts";
|
||||
import { ExperimentOptions } from "./reducers/experiments";
|
||||
import { LastOpened } from "./reducers/last_opened";
|
||||
import { Notifications } from "./reducers/notifications";
|
||||
import { QueuedMessage } from "./reducers/queue";
|
||||
import { SectionToggle } from "./reducers/section_toggle";
|
||||
import { Settings } from "./reducers/settings";
|
||||
import { SyncOptions } from "./reducers/sync";
|
||||
import { Themes } from "./reducers/themes";
|
||||
import { TrustedLinks } from "./reducers/trusted_links";
|
||||
import { Unreads } from "./reducers/unreads";
|
||||
|
||||
export type State = {
|
||||
config: RevoltConfiguration;
|
||||
locale: Language;
|
||||
auth: AuthState;
|
||||
settings: Settings;
|
||||
unreads: Unreads;
|
||||
queue: QueuedMessage[];
|
||||
drafts: Drafts;
|
||||
sync: SyncOptions;
|
||||
experiments: ExperimentOptions;
|
||||
lastOpened: LastOpened;
|
||||
notifications: Notifications;
|
||||
sectionToggle: SectionToggle;
|
||||
trustedLinks: TrustedLinks;
|
||||
themes: Themes;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const store = createStore((state: any, action: any) => {
|
||||
if (import.meta.env.DEV) {
|
||||
console.debug("State Update:", action);
|
||||
}
|
||||
|
||||
if (action.type === "__INIT") {
|
||||
return action.state;
|
||||
}
|
||||
|
||||
return rootReducer(state, action);
|
||||
});
|
||||
|
||||
// Save state using localForage.
|
||||
store.subscribe(() => {
|
||||
const {
|
||||
config,
|
||||
locale,
|
||||
auth,
|
||||
settings,
|
||||
unreads,
|
||||
queue,
|
||||
drafts,
|
||||
sync,
|
||||
experiments,
|
||||
lastOpened,
|
||||
notifications,
|
||||
sectionToggle,
|
||||
trustedLinks,
|
||||
themes,
|
||||
} = store.getState() as State;
|
||||
|
||||
localForage.setItem("state", {
|
||||
config,
|
||||
locale,
|
||||
auth,
|
||||
settings,
|
||||
unreads,
|
||||
queue,
|
||||
drafts,
|
||||
sync,
|
||||
experiments,
|
||||
lastOpened,
|
||||
notifications,
|
||||
sectionToggle,
|
||||
trustedLinks,
|
||||
themes,
|
||||
});
|
||||
});
|
||||
|
||||
export function dispatch(action: Action) {
|
||||
store.dispatch(action);
|
||||
}
|
||||
|
||||
export function getState(): State {
|
||||
return store.getState();
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
import { Session } from "revolt-api/types/Auth";
|
||||
|
||||
export interface AuthState {
|
||||
accounts: {
|
||||
[key: string]: {
|
||||
session: Session;
|
||||
};
|
||||
};
|
||||
active?: string;
|
||||
}
|
||||
|
||||
export type AuthAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "LOGIN";
|
||||
session: Session;
|
||||
}
|
||||
| {
|
||||
type: "LOGOUT";
|
||||
user_id?: string;
|
||||
};
|
||||
|
||||
export function auth(
|
||||
state = { accounts: {} } as AuthState,
|
||||
action: AuthAction,
|
||||
): AuthState {
|
||||
switch (action.type) {
|
||||
case "LOGIN":
|
||||
return {
|
||||
accounts: {
|
||||
...state.accounts,
|
||||
[action.session.user_id]: {
|
||||
session: action.session,
|
||||
},
|
||||
},
|
||||
active: action.session.user_id,
|
||||
};
|
||||
case "LOGOUT": {
|
||||
const accounts = Object.assign({}, state.accounts);
|
||||
action.user_id && delete accounts[action.user_id];
|
||||
|
||||
return {
|
||||
accounts,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
export type Drafts = { [key: string]: string };
|
||||
|
||||
export type DraftAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "SET_DRAFT";
|
||||
channel: string;
|
||||
content: string;
|
||||
}
|
||||
| {
|
||||
type: "CLEAR_DRAFT";
|
||||
channel: string;
|
||||
}
|
||||
| {
|
||||
type: "RESET";
|
||||
};
|
||||
|
||||
export function drafts(state: Drafts = {}, action: DraftAction): Drafts {
|
||||
switch (action.type) {
|
||||
case "SET_DRAFT":
|
||||
return {
|
||||
...state,
|
||||
[action.channel]: action.content,
|
||||
};
|
||||
case "CLEAR_DRAFT": {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [action.channel]: _, ...newState } = state;
|
||||
return newState;
|
||||
}
|
||||
case "RESET":
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
import { getState } from "..";
|
||||
|
||||
export type Experiments = "search" | "theme_shop";
|
||||
|
||||
export const AVAILABLE_EXPERIMENTS: Experiments[] = ["theme_shop"];
|
||||
|
||||
export const EXPERIMENTS: {
|
||||
[key in Experiments]: { title: string; description: string };
|
||||
} = {
|
||||
search: {
|
||||
title: "Search",
|
||||
description: "Allows you to search for messages in channels.",
|
||||
},
|
||||
theme_shop: {
|
||||
title: "Theme Shop",
|
||||
description: "Allows you to access and set user submitted themes.",
|
||||
},
|
||||
};
|
||||
|
||||
export interface ExperimentOptions {
|
||||
enabled?: Experiments[];
|
||||
}
|
||||
|
||||
export type ExperimentsAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "EXPERIMENTS_ENABLE";
|
||||
key: Experiments;
|
||||
}
|
||||
| {
|
||||
type: "EXPERIMENTS_DISABLE";
|
||||
key: Experiments;
|
||||
};
|
||||
|
||||
export function experiments(
|
||||
state = {} as ExperimentOptions,
|
||||
action: ExperimentsAction,
|
||||
): ExperimentOptions {
|
||||
switch (action.type) {
|
||||
case "EXPERIMENTS_ENABLE":
|
||||
return {
|
||||
...state,
|
||||
enabled: [
|
||||
...(state.enabled ?? [])
|
||||
.filter((x) => AVAILABLE_EXPERIMENTS.includes(x))
|
||||
.filter((v) => v !== action.key),
|
||||
action.key,
|
||||
],
|
||||
};
|
||||
case "EXPERIMENTS_DISABLE":
|
||||
return {
|
||||
...state,
|
||||
enabled: state.enabled
|
||||
?.filter((v) => v !== action.key)
|
||||
.filter((x) => AVAILABLE_EXPERIMENTS.includes(x)),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function isExperimentEnabled(
|
||||
name: Experiments,
|
||||
experiments: ExperimentOptions = getState().experiments,
|
||||
) {
|
||||
return experiments.enabled?.includes(name) ?? false;
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
import { combineReducers } from "redux";
|
||||
|
||||
import { State } from "..";
|
||||
import { auth, AuthAction } from "./auth";
|
||||
import { drafts, DraftAction } from "./drafts";
|
||||
import { experiments, ExperimentsAction } from "./experiments";
|
||||
import { lastOpened, LastOpenedAction } from "./last_opened";
|
||||
import { locale, LocaleAction } from "./locale";
|
||||
import { notifications, NotificationsAction } from "./notifications";
|
||||
import { queue, QueueAction } from "./queue";
|
||||
import { sectionToggle, SectionToggleAction } from "./section_toggle";
|
||||
import { config, ConfigAction } from "./server_config";
|
||||
import { settings, SettingsAction } from "./settings";
|
||||
import { sync, SyncAction } from "./sync";
|
||||
import { themes, ThemesAction } from "./themes";
|
||||
import { trustedLinks, TrustedLinksAction } from "./trusted_links";
|
||||
import { unreads, UnreadsAction } from "./unreads";
|
||||
|
||||
export default combineReducers({
|
||||
config,
|
||||
locale,
|
||||
auth,
|
||||
settings,
|
||||
unreads,
|
||||
queue,
|
||||
drafts,
|
||||
sync,
|
||||
experiments,
|
||||
lastOpened,
|
||||
notifications,
|
||||
sectionToggle,
|
||||
trustedLinks,
|
||||
themes,
|
||||
});
|
||||
|
||||
export type Action =
|
||||
| ConfigAction
|
||||
| LocaleAction
|
||||
| AuthAction
|
||||
| SettingsAction
|
||||
| UnreadsAction
|
||||
| QueueAction
|
||||
| DraftAction
|
||||
| SyncAction
|
||||
| ExperimentsAction
|
||||
| LastOpenedAction
|
||||
| NotificationsAction
|
||||
| SectionToggleAction
|
||||
| TrustedLinksAction
|
||||
| ThemesAction
|
||||
| { type: "__INIT"; state: State };
|
|
@ -1,32 +0,0 @@
|
|||
export interface LastOpened {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export type LastOpenedAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "LAST_OPENED_SET";
|
||||
parent: string;
|
||||
child: string;
|
||||
}
|
||||
| {
|
||||
type: "RESET";
|
||||
};
|
||||
|
||||
export function lastOpened(
|
||||
state = {} as LastOpened,
|
||||
action: LastOpenedAction,
|
||||
): LastOpened {
|
||||
switch (action.type) {
|
||||
case "LAST_OPENED_SET": {
|
||||
return {
|
||||
...state,
|
||||
[action.parent]: action.child,
|
||||
};
|
||||
}
|
||||
case "RESET":
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import { Language, Languages } from "../../context/Locale";
|
||||
|
||||
import type { SyncUpdateAction } from "./sync";
|
||||
|
||||
export type LocaleAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "SET_LOCALE";
|
||||
locale: Language;
|
||||
}
|
||||
| SyncUpdateAction;
|
||||
|
||||
export function findLanguage(lang?: string): Language {
|
||||
if (!lang) {
|
||||
if (typeof navigator === "undefined") {
|
||||
lang = Language.ENGLISH;
|
||||
} else {
|
||||
lang = navigator.language;
|
||||
}
|
||||
}
|
||||
|
||||
const code = lang.replace("-", "_");
|
||||
const short = code.split("_")[0];
|
||||
|
||||
const values = [];
|
||||
for (const key in Language) {
|
||||
const value = Language[key as keyof typeof Language];
|
||||
|
||||
// Skip alternative/joke languages
|
||||
if (Languages[value].cat === "alt") continue;
|
||||
|
||||
values.push(value);
|
||||
if (value.startsWith(code)) {
|
||||
return value as Language;
|
||||
}
|
||||
}
|
||||
|
||||
for (const value of values.reverse()) {
|
||||
if (value.startsWith(short)) {
|
||||
return value as Language;
|
||||
}
|
||||
}
|
||||
|
||||
return Language.ENGLISH;
|
||||
}
|
||||
|
||||
export function locale(state = findLanguage(), action: LocaleAction): Language {
|
||||
switch (action.type) {
|
||||
case "SET_LOCALE":
|
||||
return action.locale;
|
||||
case "SYNC_UPDATE":
|
||||
return (action.update.locale?.[1] ?? state) as Language;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||
import { Message } from "revolt.js/dist/maps/Messages";
|
||||
|
||||
import type { SyncUpdateAction } from "./sync";
|
||||
|
||||
export type NotificationState = "all" | "mention" | "none" | "muted";
|
||||
|
||||
export type Notifications = {
|
||||
[key: string]: NotificationState;
|
||||
};
|
||||
|
||||
export const DEFAULT_STATES: {
|
||||
[key in Channel["channel_type"]]: NotificationState;
|
||||
} = {
|
||||
SavedMessages: "all",
|
||||
DirectMessage: "all",
|
||||
Group: "all",
|
||||
TextChannel: "mention",
|
||||
VoiceChannel: "mention",
|
||||
};
|
||||
|
||||
export function getNotificationState(
|
||||
notifications: Notifications,
|
||||
channel: Channel,
|
||||
) {
|
||||
return notifications[channel._id] ?? DEFAULT_STATES[channel.channel_type];
|
||||
}
|
||||
|
||||
export function shouldNotify(
|
||||
state: NotificationState,
|
||||
message: Message,
|
||||
user_id: string,
|
||||
) {
|
||||
switch (state) {
|
||||
case "muted":
|
||||
case "none":
|
||||
return false;
|
||||
case "mention": {
|
||||
if (!message.mention_ids?.includes(user_id)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type NotificationsAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "NOTIFICATIONS_SET";
|
||||
key: string;
|
||||
state: NotificationState;
|
||||
}
|
||||
| {
|
||||
type: "NOTIFICATIONS_REMOVE";
|
||||
key: string;
|
||||
}
|
||||
| SyncUpdateAction
|
||||
| {
|
||||
type: "RESET";
|
||||
};
|
||||
|
||||
export function notifications(
|
||||
state = {} as Notifications,
|
||||
action: NotificationsAction,
|
||||
): Notifications {
|
||||
switch (action.type) {
|
||||
case "NOTIFICATIONS_SET":
|
||||
return {
|
||||
...state,
|
||||
[action.key]: action.state,
|
||||
};
|
||||
case "NOTIFICATIONS_REMOVE": {
|
||||
const { [action.key]: _, ...newState } = state;
|
||||
return newState;
|
||||
}
|
||||
case "SYNC_UPDATE":
|
||||
return action.update.notifications?.[1] ?? state;
|
||||
case "RESET":
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
export enum QueueStatus {
|
||||
SENDING = "sending",
|
||||
ERRORED = "errored",
|
||||
}
|
||||
|
||||
export interface Reply {
|
||||
id: string;
|
||||
mention: boolean;
|
||||
}
|
||||
|
||||
export type QueuedMessageData = {
|
||||
_id: string;
|
||||
author: string;
|
||||
channel: string;
|
||||
|
||||
content: string;
|
||||
replies: Reply[];
|
||||
};
|
||||
|
||||
export interface QueuedMessage {
|
||||
id: string;
|
||||
channel: string;
|
||||
data: QueuedMessageData;
|
||||
status: QueueStatus;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type QueueAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "QUEUE_ADD";
|
||||
nonce: string;
|
||||
channel: string;
|
||||
message: QueuedMessageData;
|
||||
}
|
||||
| {
|
||||
type: "QUEUE_FAIL";
|
||||
nonce: string;
|
||||
error: string;
|
||||
}
|
||||
| {
|
||||
type: "QUEUE_START";
|
||||
nonce: string;
|
||||
}
|
||||
| {
|
||||
type: "QUEUE_REMOVE";
|
||||
nonce: string;
|
||||
}
|
||||
| {
|
||||
type: "QUEUE_DROP_ALL";
|
||||
}
|
||||
| {
|
||||
type: "QUEUE_FAIL_ALL";
|
||||
}
|
||||
| {
|
||||
type: "RESET";
|
||||
};
|
||||
|
||||
export function queue(
|
||||
state: QueuedMessage[] = [],
|
||||
action: QueueAction,
|
||||
): QueuedMessage[] {
|
||||
switch (action.type) {
|
||||
case "QUEUE_ADD": {
|
||||
return [
|
||||
...state.filter((x) => x.id !== action.nonce),
|
||||
{
|
||||
id: action.nonce,
|
||||
data: action.message,
|
||||
channel: action.channel,
|
||||
status: QueueStatus.SENDING,
|
||||
},
|
||||
];
|
||||
}
|
||||
case "QUEUE_FAIL": {
|
||||
const entry = state.find(
|
||||
(x) => x.id === action.nonce,
|
||||
) as QueuedMessage;
|
||||
return [
|
||||
...state.filter((x) => x.id !== action.nonce),
|
||||
{
|
||||
...entry,
|
||||
status: QueueStatus.ERRORED,
|
||||
error: action.error,
|
||||
},
|
||||
];
|
||||
}
|
||||
case "QUEUE_START": {
|
||||
const entry = state.find(
|
||||
(x) => x.id === action.nonce,
|
||||
) as QueuedMessage;
|
||||
return [
|
||||
...state.filter((x) => x.id !== action.nonce),
|
||||
{
|
||||
...entry,
|
||||
status: QueueStatus.SENDING,
|
||||
},
|
||||
];
|
||||
}
|
||||
case "QUEUE_REMOVE":
|
||||
return state.filter((x) => x.id !== action.nonce);
|
||||
case "QUEUE_FAIL_ALL":
|
||||
return state.map((x) => {
|
||||
return {
|
||||
...x,
|
||||
status: QueueStatus.ERRORED,
|
||||
};
|
||||
});
|
||||
case "QUEUE_DROP_ALL":
|
||||
case "RESET":
|
||||
return [];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
export interface SectionToggle {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
|
||||
export type SectionToggleAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "SECTION_TOGGLE_SET";
|
||||
id: string;
|
||||
state: boolean;
|
||||
}
|
||||
| {
|
||||
type: "SECTION_TOGGLE_UNSET";
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: "RESET";
|
||||
};
|
||||
|
||||
export function sectionToggle(
|
||||
state = {} as SectionToggle,
|
||||
action: SectionToggleAction,
|
||||
): SectionToggle {
|
||||
switch (action.type) {
|
||||
case "SECTION_TOGGLE_SET": {
|
||||
return {
|
||||
...state,
|
||||
[action.id]: action.state,
|
||||
};
|
||||
}
|
||||
case "SECTION_TOGGLE_UNSET": {
|
||||
const { [action.id]: _, ...newState } = state;
|
||||
return newState;
|
||||
}
|
||||
case "RESET":
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import type { RevoltConfiguration } from "revolt-api/types/Core";
|
||||
|
||||
export type ConfigAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "SET_CONFIG";
|
||||
config: RevoltConfiguration;
|
||||
};
|
||||
|
||||
export function config(
|
||||
state = {} as RevoltConfiguration,
|
||||
action: ConfigAction,
|
||||
): RevoltConfiguration {
|
||||
switch (action.type) {
|
||||
case "SET_CONFIG":
|
||||
return action.config;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
import type { Theme, ThemeOptions } from "../../context/Theme";
|
||||
|
||||
import { setGlobalEmojiPack } from "../../components/common/Emoji";
|
||||
|
||||
import type { SyncUpdateAction } from "./sync";
|
||||
|
||||
type Sounds = "message" | "outbound" | "call_join" | "call_leave";
|
||||
|
||||
export type SoundOptions = {
|
||||
[key in Sounds]?: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_SOUNDS: SoundOptions = {
|
||||
message: true,
|
||||
outbound: false,
|
||||
call_join: true,
|
||||
call_leave: true,
|
||||
};
|
||||
|
||||
export interface NotificationOptions {
|
||||
desktopEnabled?: boolean;
|
||||
sounds?: SoundOptions;
|
||||
}
|
||||
|
||||
export type EmojiPacks = "mutant" | "twemoji" | "noto" | "openmoji";
|
||||
export interface AppearanceOptions {
|
||||
emojiPack?: EmojiPacks;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
theme?: ThemeOptions;
|
||||
appearance?: AppearanceOptions;
|
||||
notification?: NotificationOptions;
|
||||
}
|
||||
|
||||
export type SettingsAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "SETTINGS_SET_THEME";
|
||||
theme: ThemeOptions;
|
||||
}
|
||||
| {
|
||||
type: "SETTINGS_SET_THEME_OVERRIDE";
|
||||
custom?: Partial<Theme>;
|
||||
}
|
||||
| {
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS";
|
||||
options: NotificationOptions;
|
||||
}
|
||||
| {
|
||||
type: "SETTINGS_SET_APPEARANCE";
|
||||
options: Partial<AppearanceOptions>;
|
||||
}
|
||||
| SyncUpdateAction
|
||||
| {
|
||||
type: "RESET";
|
||||
};
|
||||
|
||||
export function settings(
|
||||
state = {} as Settings,
|
||||
action: SettingsAction,
|
||||
): Settings {
|
||||
// setGlobalEmojiPack(state.appearance?.emojiPack ?? "mutant");
|
||||
|
||||
switch (action.type) {
|
||||
case "SETTINGS_SET_THEME":
|
||||
return {
|
||||
...state,
|
||||
theme: {
|
||||
...state.theme,
|
||||
...action.theme,
|
||||
},
|
||||
};
|
||||
case "SETTINGS_SET_THEME_OVERRIDE":
|
||||
return {
|
||||
...state,
|
||||
theme: {
|
||||
...state.theme,
|
||||
custom: {
|
||||
...state.theme?.custom,
|
||||
...action.custom,
|
||||
},
|
||||
},
|
||||
};
|
||||
case "SETTINGS_SET_NOTIFICATION_OPTIONS":
|
||||
return {
|
||||
...state,
|
||||
notification: {
|
||||
...state.notification,
|
||||
...action.options,
|
||||
},
|
||||
};
|
||||
case "SETTINGS_SET_APPEARANCE":
|
||||
return {
|
||||
...state,
|
||||
appearance: {
|
||||
...state.appearance,
|
||||
...action.options,
|
||||
},
|
||||
};
|
||||
case "SYNC_UPDATE":
|
||||
return {
|
||||
...state,
|
||||
appearance: action.update.appearance?.[1] ?? state.appearance,
|
||||
theme: action.update.theme?.[1] ?? state.theme,
|
||||
};
|
||||
case "RESET":
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
import type { Language } from "../../context/Locale";
|
||||
import type { ThemeOptions } from "../../context/Theme";
|
||||
|
||||
import type { Notifications } from "./notifications";
|
||||
import type { AppearanceOptions } from "./settings";
|
||||
|
||||
export type SyncKeys = "theme" | "appearance" | "locale" | "notifications";
|
||||
|
||||
export interface SyncData {
|
||||
locale?: Language;
|
||||
theme?: ThemeOptions;
|
||||
appearance?: AppearanceOptions;
|
||||
notifications?: Notifications;
|
||||
}
|
||||
|
||||
export const DEFAULT_ENABLED_SYNC: SyncKeys[] = [
|
||||
"theme",
|
||||
"appearance",
|
||||
"locale",
|
||||
"notifications",
|
||||
];
|
||||
export interface SyncOptions {
|
||||
disabled?: SyncKeys[];
|
||||
revision?: {
|
||||
[key: string]: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type SyncUpdateAction = {
|
||||
type: "SYNC_UPDATE";
|
||||
update: { [key in SyncKeys]?: [number, SyncData[key]] };
|
||||
};
|
||||
|
||||
export type SyncAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "SYNC_ENABLE_KEY";
|
||||
key: SyncKeys;
|
||||
}
|
||||
| {
|
||||
type: "SYNC_DISABLE_KEY";
|
||||
key: SyncKeys;
|
||||
}
|
||||
| {
|
||||
type: "SYNC_SET_REVISION";
|
||||
key: SyncKeys;
|
||||
timestamp: number;
|
||||
}
|
||||
| SyncUpdateAction;
|
||||
|
||||
export function sync(
|
||||
state = {} as SyncOptions,
|
||||
action: SyncAction,
|
||||
): SyncOptions {
|
||||
switch (action.type) {
|
||||
case "SYNC_DISABLE_KEY":
|
||||
return {
|
||||
...state,
|
||||
disabled: [
|
||||
...(state.disabled ?? []).filter((v) => v !== action.key),
|
||||
action.key,
|
||||
],
|
||||
};
|
||||
case "SYNC_ENABLE_KEY":
|
||||
return {
|
||||
...state,
|
||||
disabled: state.disabled?.filter((v) => v !== action.key),
|
||||
};
|
||||
case "SYNC_SET_REVISION":
|
||||
return {
|
||||
...state,
|
||||
revision: {
|
||||
...state.revision,
|
||||
[action.key]: action.timestamp,
|
||||
},
|
||||
};
|
||||
case "SYNC_UPDATE": {
|
||||
const revision = { ...state.revision };
|
||||
for (const key of Object.keys(action.update)) {
|
||||
const value = action.update[key as SyncKeys];
|
||||
if (value) {
|
||||
revision[key] = value[0];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
revision,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import { Theme } from "../../context/Theme";
|
||||
|
||||
import { ThemeMetadata } from "../../pages/settings/panes/ThemeShop";
|
||||
|
||||
export interface StoredTheme {
|
||||
slug: string;
|
||||
meta: ThemeMetadata;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
export type Themes = Record<string, StoredTheme>;
|
||||
|
||||
export type ThemesAction =
|
||||
| { type: undefined }
|
||||
| { type: "THEMES_SET_THEME"; theme: StoredTheme }
|
||||
| { type: "THEMES_REMOVE_THEME"; slug: string }
|
||||
| { type: "RESET" };
|
||||
|
||||
export function themes(state: Themes = {}, action: ThemesAction) {
|
||||
switch (action.type) {
|
||||
case "THEMES_SET_THEME":
|
||||
return {
|
||||
...state,
|
||||
[action.theme.slug]: action.theme,
|
||||
};
|
||||
case "THEMES_REMOVE_THEME":
|
||||
return { ...state, [action.slug]: null };
|
||||
case "RESET":
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
export interface TrustedLinks {
|
||||
domains?: string[];
|
||||
}
|
||||
|
||||
export type TrustedLinksAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "TRUSTED_LINKS_ADD_DOMAIN";
|
||||
domain: string;
|
||||
}
|
||||
| {
|
||||
type: "TRUSTED_LINKS_REMOVE_DOMAIN";
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export function trustedLinks(
|
||||
state = {} as TrustedLinks,
|
||||
action: TrustedLinksAction,
|
||||
): TrustedLinks {
|
||||
switch (action.type) {
|
||||
case "TRUSTED_LINKS_ADD_DOMAIN":
|
||||
return {
|
||||
...state,
|
||||
domains: [
|
||||
...(state.domains ?? []).filter((v) => v !== action.domain),
|
||||
action.domain,
|
||||
],
|
||||
};
|
||||
case "TRUSTED_LINKS_REMOVE_DOMAIN":
|
||||
return {
|
||||
...state,
|
||||
domains: state.domains?.filter((v) => v !== action.domain),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
import type { ChannelUnread } from "revolt-api/types/Sync";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export interface Unreads {
|
||||
[key: string]: Partial<Omit<ChannelUnread, "_id">>;
|
||||
}
|
||||
|
||||
export type UnreadsAction =
|
||||
| { type: undefined }
|
||||
| {
|
||||
type: "UNREADS_MARK_READ";
|
||||
channel: string;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
type: "UNREADS_MARK_MULTIPLE_READ";
|
||||
channels: string[];
|
||||
}
|
||||
| {
|
||||
type: "UNREADS_SET";
|
||||
unreads: ChannelUnread[];
|
||||
}
|
||||
| {
|
||||
type: "UNREADS_MENTION";
|
||||
channel: string;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
type: "RESET";
|
||||
};
|
||||
|
||||
export function unreads(state = {} as Unreads, action: UnreadsAction): Unreads {
|
||||
switch (action.type) {
|
||||
case "UNREADS_MARK_READ":
|
||||
return {
|
||||
...state,
|
||||
[action.channel]: {
|
||||
last_id: action.message,
|
||||
},
|
||||
};
|
||||
case "UNREADS_MARK_MULTIPLE_READ": {
|
||||
const newState = { ...state };
|
||||
const last_id = ulid();
|
||||
for (const channel of action.channels) {
|
||||
newState[channel] = {
|
||||
last_id,
|
||||
};
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
case "UNREADS_SET": {
|
||||
const obj: Unreads = {};
|
||||
for (const entry of action.unreads) {
|
||||
const { _id, ...v } = entry;
|
||||
obj[_id.channel] = v;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
case "UNREADS_MENTION": {
|
||||
const obj = state[action.channel];
|
||||
|
||||
return {
|
||||
...state,
|
||||
[action.channel]: {
|
||||
...obj,
|
||||
mentions: [...(obj?.mentions ?? []), action.message],
|
||||
},
|
||||
};
|
||||
}
|
||||
case "RESET":
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
38
yarn.lock
38
yarn.lock
|
@ -2463,10 +2463,10 @@ events@^3.3.0:
|
|||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
|
||||
|
||||
exponential-backoff@^3.1.0:
|
||||
"exponential-backoff@npm:@insertish/exponential-backoff":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.0.tgz#9409c7e579131f8bd4b32d7d8094a911040f2e68"
|
||||
integrity sha512-oBuz5SYz5zzyuHINoe9ooePwSu0xApKWgeNzok4hZ5YKXFh9zrQBEM15CXqoZkJJPuI2ArvqjPQd8UKJA753XA==
|
||||
resolved "https://registry.yarnpkg.com/@insertish/exponential-backoff/-/exponential-backoff-3.1.0.tgz#1d2e4c215fa8647779cfeab74ecb54a5c36835e6"
|
||||
integrity sha512-8Jab9OfjheI84T04QjUwXceSO1DMGy8goDqVdnuoffC2fg23zBnikLJkrRHiT/ao4c08v4R2mU7+/DXMWmROng==
|
||||
|
||||
fake-mediastreamtrack@^1.1.6:
|
||||
version "1.1.6"
|
||||
|
@ -2912,10 +2912,10 @@ isexe@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
|
||||
|
||||
isomorphic-ws@^4.0.1:
|
||||
"isomorphic-ws@npm:@insertish/isomorphic-ws":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
|
||||
integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
|
||||
resolved "https://registry.yarnpkg.com/@insertish/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#5bcd6f73b93efa9ccdb6abf887ae808d40827169"
|
||||
integrity sha512-kFD/p8T4Hkqr992QrdkbW/cQ/W/q2d9MPCobwzBv2PwTKLkCD9RaYDy6m17qRnSLQQ5PU0kHCG8kaOwAqzj1vQ==
|
||||
|
||||
javascript-natural-sort@0.7.1:
|
||||
version "0.7.1"
|
||||
|
@ -3576,18 +3576,6 @@ react-redux@^7.2.0:
|
|||
prop-types "^15.7.2"
|
||||
react-is "^16.13.1"
|
||||
|
||||
react-redux@^7.2.4:
|
||||
version "7.2.4"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
|
||||
integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.1"
|
||||
"@types/react-redux" "^7.1.16"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.13.1"
|
||||
|
||||
react-router-dom@^5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.1.tgz#34af8b551a4ce17487d3f80e651b91651978dff6"
|
||||
|
@ -3651,7 +3639,7 @@ readdirp@~3.6.0:
|
|||
dependencies:
|
||||
picomatch "^2.2.1"
|
||||
|
||||
redux@^4.0.0, redux@^4.0.4, redux@^4.1.0:
|
||||
redux@^4.0.0, redux@^4.0.4:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.1.tgz#76f1c439bb42043f985fbd9bf21990e60bd67f47"
|
||||
integrity sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==
|
||||
|
@ -3770,15 +3758,15 @@ 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.2.0-patch.0:
|
||||
version "5.2.0-patch.0"
|
||||
resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.2.0-patch.0.tgz#af6afc402399e5394b50b2e7d1573ff490fd3906"
|
||||
integrity sha512-PnHKqRpEvrBFm1xtLA/lGG5FIsp5kW4eB8sYiejjQCA1DWi7Xg6MNvyOjjha6jKftPXF8roivfZWEnM7sY1bnA==
|
||||
revolt.js@5.2.1-patch.1:
|
||||
version "5.2.1-patch.1"
|
||||
resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.2.1-patch.1.tgz#4b392d4dae12ea28f559ef89790368f53788c81d"
|
||||
integrity sha512-u2vvbCWXKx+vZKqlt5izowf9XnMbWdh3GaPMzipek6l6mBYSCIlr796HoiiIO3c2T3AWqh3zav97rm8z3jOIXg==
|
||||
dependencies:
|
||||
axios "^0.21.4"
|
||||
eventemitter3 "^4.0.7"
|
||||
exponential-backoff "^3.1.0"
|
||||
isomorphic-ws "^4.0.1"
|
||||
exponential-backoff "npm:@insertish/exponential-backoff"
|
||||
isomorphic-ws "npm:@insertish/isomorphic-ws"
|
||||
lodash.defaultsdeep "^4.6.1"
|
||||
lodash.flatten "^4.4.0"
|
||||
lodash.isequal "^4.5.0"
|
||||
|
|
Loading…
Reference in a new issue