Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 1054 additions and 510 deletions
import dayjs from "dayjs";
import { decodeTime } from "ulid";
import { Text } from "preact-i18n";
import styles from "./Panes.module.scss";
import Tip from "../../../components/ui/Tip";
import { useHistory } from "react-router-dom";
import Button from "../../../components/ui/Button";
import Preloader from "../../../components/ui/Preloader";
import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { HelpCircle } from "@styled-icons/boxicons-regular";
import { Chrome, Android, Apple, Windows } from "@styled-icons/boxicons-logos";
import { HelpCircle, Desktop } from "@styled-icons/boxicons-regular";
import {
Android,
Safari,
Firefoxbrowser,
Googlechrome,
Ios,
Microsoftedge,
Linux,
Macos,
Microsoftedge,
Safari,
Windows
Opera,
} from "@styled-icons/simple-icons";
import relativeTime from "dayjs/plugin/relativeTime";
import { useHistory } from "react-router-dom";
import { decodeTime } from "ulid";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { dayjs } from "../../../context/Locale";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Button from "../../../components/ui/Button";
import Preloader from "../../../components/ui/Preloader";
import Tip from "../../../components/ui/Tip";
dayjs.extend(relativeTime);
interface Session {
......@@ -43,14 +43,14 @@ export function Sessions() {
}
useEffect(() => {
client.req("GET", "/auth/sessions").then(data => {
client.req("GET", "/auth/sessions").then((data) => {
data.sort(
(a, b) =>
(b.id === deviceId ? 1 : 0) - (a.id === deviceId ? 1 : 0)
(b.id === deviceId ? 1 : 0) - (a.id === deviceId ? 1 : 0),
);
setSessions(data);
});
}, []);
}, [client, setSessions, deviceId]);
if (typeof sessions === "undefined") {
return (
......@@ -64,15 +64,19 @@ export function Sessions() {
const name = session.friendly_name;
switch (true) {
case /firefox/i.test(name):
return <Firefoxbrowser />;
return <Firefoxbrowser size={32} />;
case /chrome/i.test(name):
return <Googlechrome />;
return <Chrome size={32} />;
case /safari/i.test(name):
return <Safari />;
return <Safari size={32} />;
case /edge/i.test(name):
return <Microsoftedge />;
return <Microsoftedge size={32} />;
case /opera/i.test(name):
return <Opera size={32} />;
case /desktop/i.test(name):
return <Desktop size={32} />;
default:
return <HelpCircle />;
return <HelpCircle size={32} />;
}
}
......@@ -80,34 +84,34 @@ export function Sessions() {
const name = session.friendly_name;
switch (true) {
case /linux/i.test(name):
return <Linux />;
return <Linux size={14} />;
case /android/i.test(name):
return <Android />;
return <Android size={14} />;
case /mac.*os/i.test(name):
return <Macos />;
return <Macos size={14} />;
case /ios/i.test(name):
return <Ios />;
return <Apple size={14} />;
case /windows/i.test(name):
return <Windows />;
return <Windows size={14} />;
default:
return null;
}
}
const mapped = sessions.map(session => {
const mapped = sessions.map((session) => {
return {
...session,
timestamp: decodeTime(session.id)
timestamp: decodeTime(session.id),
};
});
mapped.sort((a, b) => b.timestamp - a.timestamp);
let id = mapped.findIndex(x => x.id === deviceId);
const id = mapped.findIndex((x) => x.id === deviceId);
const render = [
mapped[id],
...mapped.slice(0, id),
...mapped.slice(id + 1, mapped.length)
...mapped.slice(id + 1, mapped.length),
];
return (
......@@ -115,64 +119,116 @@ export function Sessions() {
<h3>
<Text id="app.settings.pages.sessions.active_sessions" />
</h3>
{render.map(session => (
<div
className={styles.entry}
data-active={session.id === deviceId}
data-deleting={attemptingDelete.indexOf(session.id) > -1}
>
{deviceId === session.id && (
<span className={styles.label}>
<Text id="app.settings.pages.sessions.this_device" />{" "}
</span>
)}
<div className={styles.session}>
<div className={styles.icon}>
{getIcon(session)}
<div>{getSystemIcon(session)}</div>
</div>
<div className={styles.info}>
<span className={styles.name}>
{session.friendly_name}
{render.map((session) => {
const systemIcon = getSystemIcon(session);
return (
<div
key={session.id}
className={styles.entry}
data-active={session.id === deviceId}
data-deleting={
attemptingDelete.indexOf(session.id) > -1
}>
{deviceId === session.id && (
<span className={styles.label}>
<Text id="app.settings.pages.sessions.this_device" />{" "}
</span>
<span className={styles.time}>
<Text
id="app.settings.pages.sessions.created"
fields={{
time_ago: dayjs(
session.timestamp
).fromNow()
)}
<div className={styles.session}>
<div className={styles.detail}>
<svg width={42} height={42} viewBox="0 0 32 32">
<foreignObject
x="0"
y="0"
width="32"
height="32"
mask={
systemIcon
? "url(#session)"
: undefined
}>
{getIcon(session)}
</foreignObject>
<foreignObject
x="18"
y="18"
width="14"
height="14">
{systemIcon}
</foreignObject>
</svg>
<div className={styles.info}>
<input
type="text"
className={styles.name}
value={session.friendly_name}
autocomplete="off"
style={{ pointerEvents: "none" }}
/>
<span className={styles.time}>
<Text
id="app.settings.pages.sessions.created"
fields={{
time_ago: dayjs(
session.timestamp,
).fromNow(),
}}
/>
</span>
</div>
</div>
{deviceId !== session.id && (
<Button
onClick={async () => {
setDelete([
...attemptingDelete,
session.id,
]);
await client.req(
"DELETE",
`/auth/sessions/${session.id}` as "/auth/sessions",
);
setSessions(
sessions?.filter(
(x) => x.id !== session.id,
),
);
}}
/>
</span>
disabled={
attemptingDelete.indexOf(session.id) >
-1
}>
<Text id="app.settings.pages.logOut" />
</Button>
)}
</div>
{deviceId !== session.id && (
<Button
onClick={async () => {
setDelete([
...attemptingDelete,
session.id
]);
await client.req(
"DELETE",
`/auth/sessions/${session.id}` as any
);
setSessions(
sessions?.filter(
x => x.id !== session.id
)
);
}}
disabled={
attemptingDelete.indexOf(session.id) > -1
}
>
<Text id="app.settings.pages.logOut" />
</Button>
)}
</div>
</div>
))}
);
})}
<Button
error
onClick={async () => {
// ! FIXME: add to rAuth
const del: string[] = [];
render.forEach((session) => {
if (deviceId !== session.id) {
del.push(session.id);
}
});
setDelete(del);
for (const id of del) {
await client.req(
"DELETE",
`/auth/sessions/${id}` as "/auth/sessions",
);
}
setSessions(sessions.filter((x) => x.id === deviceId));
}}>
<Text id="app.settings.pages.sessions.logout" />
</Button>
<Tip>
<span>
<Text id="app.settings.tips.sessions.a" />
......
import { Text } from "preact-i18n";
import styles from "./Panes.module.scss";
import Checkbox from "../../../components/ui/Checkbox";
import { Text } from "preact-i18n";
import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector";
import { WithDispatcher } from "../../../redux/reducers";
import { SyncKeys, SyncOptions } from "../../../redux/reducers/sync";
import Checkbox from "../../../components/ui/Checkbox";
interface Props {
options?: SyncOptions;
}
export function Component(props: Props & WithDispatcher) {
export function Component(props: Props) {
return (
<div className={styles.notifications}>
<h3>
<Text id="app.settings.pages.sync.categories" />
</h3>
{
([
['appearance', 'appearance.title'],
['theme', 'appearance.theme'],
['locale', 'language.title']
{(
[
["appearance", "appearance.title"],
["theme", "appearance.theme"],
["locale", "language.title"],
// notifications sync is always-on
] as [ SyncKeys, string ][]).map(
([ key, title ]) =>
<Checkbox
checked={(props.options?.disabled ?? []).indexOf(key) === -1}
description={<Text id={`app.settings.pages.sync.descriptions.${key}`} />}
onChange={enabled => {
props.dispatcher({
type: enabled ? 'SYNC_ENABLE_KEY' : 'SYNC_DISABLE_KEY',
key
});
}}
>
<Text id={`app.settings.pages.${title}`} />
</Checkbox>
)
}
] as [SyncKeys, string][]
).map(([key, title]) => (
<Checkbox
key={key}
checked={
(props.options?.disabled ?? []).indexOf(key) === -1
}
description={
<Text
id={`app.settings.pages.sync.descriptions.${key}`}
/>
}
onChange={(enabled) =>
dispatch({
type: enabled
? "SYNC_ENABLE_KEY"
: "SYNC_DISABLE_KEY",
key,
})
}>
<Text id={`app.settings.pages.${title}`} />
</Checkbox>
))}
</div>
);
}
export const Sync = connectState(
Component,
state => {
return {
options: state.sync
};
},
true
);
export const Sync = connectState(Component, (state) => {
return {
options: state.sync,
};
});
import Tip from "../../../components/ui/Tip";
import { Servers } from "revolt.js/dist/api/objects";
import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { XCircle } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { Route } from "revolt.js/dist/api/routes";
import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import UserIcon from "../../../components/common/user/UserIcon";
import IconButton from "../../../components/ui/IconButton";
import Preloader from "../../../components/ui/Preloader";
interface Props {
server: Servers.Server;
server: Server;
}
export function Bans({ server }: Props) {
const client = useContext(AppContext);
const [bans, setBans] = useState<Servers.Ban[] | undefined>(undefined);
export const Bans = observer(({ server }: Props) => {
const [deleting, setDelete] = useState<string[]>([]);
const [data, setData] = useState<
Route<"GET", "/servers/id/bans">["response"] | undefined
>(undefined);
useEffect(() => {
client.servers.fetchBans(server._id)
.then(bans => setBans(bans))
}, [ ]);
server.fetchBans().then(setData);
}, [server, setData]);
return (
<div>
<Tip warning>This section is under construction.</Tip>
{ bans?.map(x => <div>{x._id.user}: {x.reason ?? 'no reason'} <button onClick={() => client.servers.unbanUser(server._id, x._id.user)}>unban</button></div>) }
<div className={styles.userList}>
<div className={styles.subtitle}>
<span>
<Text id="app.settings.server_pages.bans.user" />
</span>
<span class={styles.reason}>
<Text id="app.settings.server_pages.bans.reason" />
</span>
<span>
<Text id="app.settings.server_pages.bans.revoke" />
</span>
</div>
{typeof data === "undefined" && <Preloader type="ring" />}
{data?.bans.map((x) => {
const user = data.users.find((y) => y._id === x._id.user);
return (
<div
key={x._id.user}
className={styles.ban}
data-deleting={deleting.indexOf(x._id.user) > -1}>
<span>
<UserIcon attachment={user?.avatar} size={24} />
{user?.username}
</span>
<div className={styles.reason}>
{x.reason ?? (
<Text id="app.settings.server_pages.bans.no_reason" />
)}
</div>
<IconButton
onClick={async () => {
setDelete([...deleting, x._id.user]);
await server.unbanUser(x._id.user);
setData({
...data,
bans: data.bans.filter(
(y) => y._id.user !== x._id.user,
),
});
}}
disabled={deleting.indexOf(x._id.user) > -1}>
<XCircle size={24} />
</IconButton>
</div>
);
})}
</div>
);
}
});
import isEqual from "lodash.isequal";
import { observer } from "mobx-react-lite";
import { Category } from "revolt-api/types/Servers";
import { Server } from "revolt.js/dist/maps/Servers";
import { ulid } from "ulid";
import { useState } from "preact/hooks";
import ChannelIcon from "../../../components/common/ChannelIcon";
import Button from "../../../components/ui/Button";
import ComboBox from "../../../components/ui/ComboBox";
import InputBox from "../../../components/ui/InputBox";
import Tip from "../../../components/ui/Tip";
interface Props {
server: Server;
}
// ! FIXME: really bad code
export const Categories = observer(({ server }: Props) => {
const channels = server.channels.filter((x) => typeof x !== "undefined");
const [cats, setCats] = useState<Category[]>(server.categories ?? []);
const [name, setName] = useState("");
return (
<div>
<Tip warning>This section is under construction.</Tip>
<p>
<Button
contrast
disabled={isEqual(server.categories ?? [], cats)}
onClick={() => server.edit({ categories: cats })}>
save categories
</Button>
</p>
<h2>categories</h2>
{cats.map((category) => (
<div style={{ background: "var(--hover)" }} key={category.id}>
<InputBox
value={category.title}
onChange={(e) =>
setCats(
cats.map((y) =>
y.id === category.id
? {
...y,
title: e.currentTarget.value,
}
: y,
),
)
}
contrast
/>
<Button
contrast
onClick={() =>
setCats(cats.filter((x) => x.id !== category.id))
}>
delete {category.title}
</Button>
</div>
))}
<h2>create new</h2>
<p>
<InputBox
value={name}
onChange={(e) => setName(e.currentTarget.value)}
contrast
/>
<Button
contrast
onClick={() => {
setName("");
setCats([
...cats,
{
id: ulid(),
title: name,
channels: [],
},
]);
}}>
create
</Button>
</p>
<h2>channels</h2>
{channels.map((channel) => {
return (
<div
key={channel!._id}
style={{
display: "flex",
gap: "12px",
alignItems: "center",
}}>
<div style={{ flexShrink: 0 }}>
<ChannelIcon target={channel} size={24} />{" "}
<span>{channel!.name}</span>
</div>
<ComboBox
style={{ flexGrow: 1 }}
value={
cats.find((x) =>
x.channels.includes(channel!._id),
)?.id ?? "none"
}
onChange={(e) =>
setCats(
cats.map((x) => {
return {
...x,
channels: [
...x.channels.filter(
(y) => y !== channel!._id,
),
...(e.currentTarget.value ===
x.id
? [channel!._id]
: []),
],
};
}),
)
}>
<option value="none">Uncategorised</option>
{cats.map((x) => (
<option key={x.id} value={x.id}>
{x.title}
</option>
))}
</ComboBox>
</div>
);
})}
</div>
);
});
import { Text } from "preact-i18n";
import styles from './Panes.module.scss';
import { XCircle } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { ServerInvite } from "revolt-api/types/Invites";
import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import Preloader from "../../../components/ui/Preloader";
import IconButton from "../../../components/ui/IconButton";
import UserIcon from "../../../components/common/user/UserIcon";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../../context/revoltjs/util";
import { Invites as InvitesNS, Servers } from "revolt.js/dist/api/objects";
import { useChannels, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
import UserIcon from "../../../components/common/user/UserIcon";
import IconButton from "../../../components/ui/IconButton";
import Preloader from "../../../components/ui/Preloader";
interface Props {
server: Servers.Server;
server: Server;
}
export function Invites({ server }: Props) {
const [invites, setInvites] = useState<InvitesNS.ServerInvite[] | undefined>(undefined);
const ctx = useForceUpdate();
export const Invites = observer(({ server }: Props) => {
const [deleting, setDelete] = useState<string[]>([]);
const users = useUsers(invites?.map(x => x.creator) ?? [], ctx);
const channels = useChannels(invites?.map(x => x.channel) ?? [], ctx);
const [invites, setInvites] = useState<ServerInvite[] | undefined>(
undefined,
);
const client = useClient();
const users = invites?.map((invite) => client.users.get(invite.creator));
const channels = invites?.map((invite) =>
client.channels.get(invite.channel),
);
useEffect(() => {
ctx.client.servers.fetchInvites(server._id)
.then(invites => setInvites(invites))
}, [ ]);
server.fetchInvites().then(setInvites);
}, [server, setInvites]);
return (
<div className={styles.invites}>
<div className={styles.userList}>
<div className={styles.subtitle}>
<span>Invite Code</span>
<span>Invitor</span>
<span>Channel</span>
<span>Revoke</span>
<span>
<Text id="app.settings.server_pages.invites.code" />
</span>
<span>
<Text id="app.settings.server_pages.invites.invitor" />
</span>
<span>
<Text id="app.settings.server_pages.invites.channel" />
</span>
<span>
<Text id="app.settings.server_pages.invites.revoke" />
</span>
</div>
{ typeof invites === 'undefined' && <Preloader type="ring" /> }
{
invites?.map(
invite => {
let creator = users.find(x => x?._id === invite.creator);
let channel = channels.find(x => x?._id === invite.channel);
{typeof invites === "undefined" && <Preloader type="ring" />}
{invites?.map((invite, index) => {
const creator = users![index];
const channel = channels![index];
return (
<div className={styles.invite}
data-deleting={deleting.indexOf(invite._id) > -1}>
<code>{ invite._id }</code>
<span>
<UserIcon target={creator} size={24} /> {creator?.username ?? 'unknown'}
</span>
<span>{ (channel && creator) ? getChannelName(ctx.client, channel, true) : '#unknown' }</span>
<IconButton
onClick={async () => {
setDelete([
...deleting,
invite._id
]);
return (
<div
key={invite._id}
className={styles.invite}
data-deleting={deleting.indexOf(invite._id) > -1}>
<code>{invite._id}</code>
<span>
<UserIcon target={creator} size={24} />{" "}
{creator?.username ?? (
<Text id="app.main.channel.unknown_user" />
)}
</span>
<span>
{channel && creator
? getChannelName(channel, true)
: "#??"}
</span>
<IconButton
onClick={async () => {
setDelete([...deleting, invite._id]);
await ctx.client.deleteInvite(invite._id);
setInvites(
invites?.filter(
x => x._id !== invite._id
)
);
}}
disabled={deleting.indexOf(invite._id) > -1}>
<XCircle size={24} />
</IconButton>
</div>
)
}
)
}
await client.deleteInvite(invite._id);
setInvites(
invites?.filter(
(x) => x._id !== invite._id,
),
);
}}
disabled={deleting.indexOf(invite._id) > -1}>
<XCircle size={24} />
</IconButton>
</div>
);
})}
</div>
);
}
});
import styles from './Panes.module.scss';
import { ChevronDown } from "@styled-icons/boxicons-regular";
import { isEqual } from "lodash";
import { observer } from "mobx-react-lite";
import { Member } from "revolt.js/dist/maps/Members";
import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import { Servers } from "revolt.js/dist/api/objects";
import { useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
import UserIcon from "../../../components/common/user/UserIcon";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
import IconButton from "../../../components/ui/IconButton";
import Overline from "../../../components/ui/Overline";
interface Props {
server: Servers.Server;
server: Server;
}
// ! FIXME: bad code :)
export function Members({ server }: Props) {
const [members, setMembers] = useState<Servers.Member[] | undefined>(undefined);
export const Members = observer(({ server }: Props) => {
const [selected, setSelected] = useState<undefined | string>();
const [data, setData] = useState<
{ members: Member[]; users: User[] } | undefined
>(undefined);
const ctx = useForceUpdate();
const users = useUsers(members?.map(x => x._id.user) ?? [], ctx);
useEffect(() => {
server.fetchMembers().then(setData);
}, [server, setData]);
const [roles, setRoles] = useState<string[]>([]);
useEffect(() => {
ctx.client.servers.members.fetchMembers(server._id)
.then(members => setMembers(members))
}, [ ]);
if (selected) {
setRoles(
data!.members.find((x) => x._id.user === selected)?.roles ?? [],
);
}
}, [setRoles, selected, data]);
return (
<div className={styles.members}>
<div className={styles.userList}>
<div className={styles.subtitle}>
{ members?.length ?? 0 } Members
{data?.members.length ?? 0} Members
</div>
{ members && members.length > 0 && users?.map(x => x &&
<div className={styles.member}>
<div>@{x.username}</div>
</div>)
}
{data &&
data.members.length > 0 &&
data.members
.map((member) => {
return {
member,
user: data.users.find(
(x) => x._id === member._id.user,
),
};
})
.map(({ member, user }) => (
// @ts-expect-error brokey
// eslint-disable-next-line react/jsx-no-undef
<Fragment key={member._id.user}>
<div
className={styles.member}
data-open={selected === member._id.user}
onClick={() =>
setSelected(
selected === member._id.user
? undefined
: member._id.user,
)
}>
<span>
<UserIcon target={user} size={24} />{" "}
{user?.username ?? (
<Text id="app.main.channel.unknown_user" />
)}
</span>
<IconButton className={styles.chevron}>
<ChevronDown size={24} />
</IconButton>
</div>
{selected === member._id.user && (
<div
key={`drop_${member._id.user}`}
className={styles.memberView}>
<Overline type="subtle">Roles</Overline>
{Object.keys(server.roles ?? {}).map(
(key) => {
const role = server.roles![key];
return (
<Checkbox
key={key}
checked={
roles.includes(key) ??
false
}
onChange={(v) => {
if (v) {
setRoles([
...roles,
key,
]);
} else {
setRoles(
roles.filter(
(x) =>
x !==
key,
),
);
}
}}>
<span
style={{
color: role.colour,
}}>
{role.name}
</span>
</Checkbox>
);
},
)}
<Button
compact
disabled={isEqual(
member.roles ?? [],
roles,
)}
onClick={() =>
member.edit({
roles,
})
}>
<Text id="app.special.modals.actions.save" />
</Button>
</div>
)}
</Fragment>
))}
</div>
);
}
});
import { Text } from "preact-i18n";
import isEqual from "lodash.isequal";
import styles from './Panes.module.scss';
import Button from "../../../components/ui/Button";
import InputBox from "../../../components/ui/InputBox";
import ComboBox from "../../../components/ui/ComboBox";
import { Servers, Server } from "revolt.js/dist/api/objects";
import { observer } from "mobx-react-lite";
import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { useContext, useEffect, useState } from "preact/hooks";
import { getChannelName } from "../../../context/revoltjs/util";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { FileUploader } from "../../../context/revoltjs/FileUploads";
import { getChannelName } from "../../../context/revoltjs/util";
import Button from "../../../components/ui/Button";
import ComboBox from "../../../components/ui/ComboBox";
import InputBox from "../../../components/ui/InputBox";
interface Props {
server: Servers.Server;
server: Server;
}
export function Overview({ server }: Props) {
const client = useContext(AppContext);
export const Overview = observer(({ server }: Props) => {
const [name, setName] = useState(server.name);
const [description, setDescription] = useState(server.description ?? '');
const [systemMessages, setSystemMessages] = useState(server.system_messages);
const [description, setDescription] = useState(server.description ?? "");
const [systemMessages, setSystemMessages] = useState(
server.system_messages,
);
useEffect(() => setName(server.name), [ server.name ]);
useEffect(() => setDescription(server.description ?? ''), [ server.description ]);
useEffect(() => setSystemMessages(server.system_messages), [ server.system_messages ]);
useEffect(() => setName(server.name), [server.name]);
useEffect(
() => setDescription(server.description ?? ""),
[server.description],
);
useEffect(
() => setSystemMessages(server.system_messages),
[server.system_messages],
);
const [ changed, setChanged ] = useState(false);
const [changed, setChanged] = useState(false);
function save() {
let changes: Partial<Pick<Servers.Server, 'name' | 'description' | 'system_messages'>> = {};
const changes: Record<string, unknown> = {};
if (name !== server.name) changes.name = name;
if (description !== server.description) changes.description = description;
if (!isEqual(systemMessages, server.system_messages)) changes.system_messages = systemMessages;
client.servers.edit(server._id, changes);
if (description !== server.description)
changes.description = description;
if (!isEqual(systemMessages, server.system_messages))
changes.system_messages = systemMessages ?? undefined;
server.edit(changes);
setChanged(false);
}
......@@ -47,9 +59,9 @@ export function Overview({ server }: Props) {
fileType="icons"
behaviour="upload"
maxFileSize={2_500_000}
onUpload={icon => client.servers.edit(server._id, { icon })}
previewURL={client.servers.getIconURL(server._id, { max_side: 256 }, true)}
remove={() => client.servers.edit(server._id, { remove: 'Icon' })}
onUpload={(icon) => server.edit({ icon })}
previewURL={server.generateIconURL({ max_side: 256 }, true)}
remove={() => server.edit({ remove: "Icon" })}
/>
<div className={styles.name}>
<h3>
......@@ -59,9 +71,9 @@ export function Overview({ server }: Props) {
contrast
value={name}
maxLength={32}
onChange={e => {
setName(e.currentTarget.value)
if (!changed) setChanged(true)
onChange={(e) => {
setName(e.currentTarget.value);
if (!changed) setChanged(true);
}}
/>
</div>
......@@ -76,67 +88,85 @@ export function Overview({ server }: Props) {
maxLength={1024}
value={description}
placeholder={"Add a topic..."}
onChange={ev => {
onChange={(ev) => {
setDescription(ev.currentTarget.value);
if (!changed) setChanged(true)
if (!changed) setChanged(true);
}}
/>
<h3>
<Text id="app.main.servers.custom_banner" />
</h3>
<FileUploader
height={160}
style="banner"
fileType="banners"
behaviour="upload"
maxFileSize={6_000_000}
onUpload={(banner) => server.edit({ banner })}
previewURL={server.generateBannerURL({ width: 1000 }, true)}
remove={() => server.edit({ remove: "Banner" })}
/>
<h3>
<Text id="app.settings.server_pages.overview.system_messages" />
</h3>
{[
[ 'User Joined', 'user_joined' ],
[ 'User Left', 'user_left' ],
[ 'User Kicked', 'user_kicked' ],
[ 'User Banned', 'user_banned' ]
].map(([ i18n, key ]) =>
["User Joined", "user_joined"],
["User Left", "user_left"],
["User Kicked", "user_kicked"],
["User Banned", "user_banned"],
].map(([i18n, key]) => (
// ! FIXME: temporary code just so we can expose the options
<p style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<span style={{ flexShrink: '0' }}>{i18n}</span>
<ComboBox value={systemMessages?.[key as keyof typeof systemMessages] ?? 'disabled'}
onChange={e => {
if (!changed) setChanged(true)
<p
key={key}
style={{
display: "flex",
gap: "8px",
alignItems: "center",
}}>
<span style={{ flexShrink: "0", flex: `25%` }}>{i18n}</span>
<ComboBox
value={
systemMessages?.[
key as keyof typeof systemMessages
] ?? "disabled"
}
onChange={(e) => {
if (!changed) setChanged(true);
const v = e.currentTarget.value;
if (v === 'disabled') {
const { [key as keyof typeof systemMessages]: _, ...other } = systemMessages;
if (v === "disabled") {
const {
[key as keyof typeof systemMessages]: _,
...other
} = systemMessages;
setSystemMessages(other);
} else {
setSystemMessages({
...systemMessages,
[key]: v
[key]: v,
});
}
}}>
<option value='disabled'><Text id="general.disabled" /></option>
{ server.channels.map(id => {
const channel = client.channels.get(id);
if (!channel) return null;
return <option value={id}>{ getChannelName(client, channel, true) }</option>;
}) }
<option value="disabled">
<Text id="general.disabled" />
</option>
{server.channels
.filter((x) => typeof x !== "undefined")
.map((channel) => (
<option key={channel!._id} value={channel!._id}>
{getChannelName(channel!, true)}
</option>
))}
</ComboBox>
</p>
)}
))}
<p>
<Button onClick={save} contrast disabled={!changed}>
<Text id="app.special.modals.actions.save" />
</Button>
</p>
<h3>
<Text id="app.main.servers.custom_banner" />
</h3>
<FileUploader
height={160}
style="banner"
fileType="banners"
behaviour="upload"
maxFileSize={6_000_000}
onUpload={banner => client.servers.edit(server._id, { banner })}
previewURL={client.servers.getBannerURL(server._id, { width: 1000 }, true)}
remove={() => client.servers.edit(server._id, { remove: 'Banner' })}
/>
</div>
);
}
});
......@@ -6,6 +6,10 @@
.name {
flex-grow: 1;
h3 {
margin-top: 0;
}
input {
width: 100%;
}
......@@ -13,21 +17,32 @@
}
}
.invites {
.userList {
gap: 8px;
display: flex;
flex-direction: column;
.subtitle {
gap: 8px;
display: flex;
justify-content: space-between;
font-size: 13px;
text-transform: uppercase;
color: var(--secondary-foreground);
font-weight: 700;
.reason {
text-align: center;
}
}
.reason {
flex: 2;
}
.invite {
.invite,
.ban,
.member {
gap: 8px;
padding: 10px;
display: flex;
......@@ -35,7 +50,8 @@
flex-direction: row;
background: var(--secondary-background);
code, span {
span,
code {
flex: 1;
}
......@@ -54,25 +70,23 @@
opacity: 0.5;
}
}
}
.members {
.subtitle {
display: flex;
justify-content: space-between;
font-size: 13px;
text-transform: uppercase;
color: var(--secondary-foreground);
font-weight: 700;
}
.member {
gap: 8px;
cursor: pointer;
.chevron {
transition: 0.2s ease all;
}
&:not([data-open="true"]) .chevron {
transform: rotateZ(90deg);
}
}
.memberView {
padding: 10px;
display: flex;
align-items: center;
flex-direction: row;
background: var(--secondary-background);
margin: 0 10px;
background: var(--background);
}
}
......@@ -91,10 +105,6 @@
flex-grow: 1;
padding: 0 8px;
overflow-y: scroll;
section {
margin-bottom: 1em;
}
}
.title {
......@@ -103,7 +113,8 @@
margin-bottom: 1em;
align-items: center;
h1, h2 {
h1,
h2 {
margin: 0;
min-width: 0;
flex-grow: 1;
......@@ -122,4 +133,4 @@
display: flex;
padding: 8px 0;
}
}
\ No newline at end of file
}
import { Plus } from "@styled-icons/boxicons-regular";
import isEqual from "lodash.isequal";
import { observer } from "mobx-react-lite";
import { ChannelPermission, ServerPermission } from "revolt.js";
import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import styles from './Panes.module.scss';
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import Button from "../../../components/ui/Button";
import Overline from "../../../components/ui/Overline";
import { Servers } from "revolt.js/dist/api/objects";
import Checkbox from "../../../components/ui/Checkbox";
import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { ChannelPermission, ServerPermission } from "revolt.js/dist/api/permissions";
import Tip from "../../../components/ui/Tip";
import IconButton from "../../../components/ui/IconButton";
import ButtonItem from "../../../components/navigation/items/ButtonItem";
import isEqual from 'lodash.isequal';
import ColourSwatches from "../../../components/ui/ColourSwatches";
import InputBox from "../../../components/ui/InputBox";
import { Plus } from "@styled-icons/boxicons-regular";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import Overline from "../../../components/ui/Overline";
import ButtonItem from "../../../components/navigation/items/ButtonItem";
interface Props {
server: Servers.Server;
server: Server;
}
const I32ToU32 = (arr: number[]) => arr.map(x => x >>> 0);
const I32ToU32 = (arr: number[]) => arr.map((x) => x >>> 0);
// ! FIXME: bad code :)
export function Roles({ server }: Props) {
const [ role, setRole ] = useState('default');
export const Roles = observer(({ server }: Props) => {
const [role, setRole] = useState("default");
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const roles = server.roles ?? {};
const roles = useMemo(() => server.roles ?? {}, [server]);
if (role !== 'default' && typeof roles[role] === 'undefined') {
useEffect(() => setRole('default'));
return;
if (role !== "default" && typeof roles[role] === "undefined") {
useEffect(() => setRole("default"), [role]);
return null;
}
const v = (id: string) => I32ToU32(id === 'default' ? server.default_permissions : roles[id].permissions)
const [ perm, setPerm ] = useState(v(role));
useEffect(() => setPerm(v(role)), [ role, roles[role]?.permissions ]);
const {
name: roleName,
colour: roleColour,
permissions,
} = roles[role] ?? {};
const getPermissions = useCallback(
(id: string) => {
return I32ToU32(
id === "default"
? server.default_permissions
: roles[id].permissions,
);
},
[roles, server],
);
const [perm, setPerm] = useState(getPermissions(role));
const [name, setName] = useState(roleName);
const [colour, setColour] = useState(roleColour);
useEffect(
() => setPerm(getPermissions(role)),
[getPermissions, role, permissions],
);
useEffect(() => setName(roleName), [role, roleName]);
useEffect(() => setColour(roleColour), [role, roleColour]);
const modified =
!isEqual(perm, getPermissions(role)) ||
!isEqual(name, roleName) ||
!isEqual(colour, roleColour);
const save = () => {
if (!isEqual(perm, getPermissions(role))) {
server.setPermissions(role, {
server: perm[0],
channel: perm[1],
});
}
if (!isEqual(name, roleName) || !isEqual(colour, roleColour)) {
server.editRole(role, { name, colour });
}
};
const modified = !isEqual(perm, v(role));
const save = () => client.servers.setPermissions(server._id, role, { server: perm[0], channel: perm[1] });
const deleteRole = () => {
setRole('default');
client.servers.deleteRole(server._id, role);
setRole("default");
server.deleteRole(role);
};
return (
<div className={styles.roles}>
<div className={styles.list}>
<div className={styles.title}>
<h1><Text id="app.settings.server_pages.roles.title" /></h1>
<Plus size={22} onClick={() =>
openScreen({ id: 'special_input', type: 'create_role', server: server._id, callback: id => setRole(id) })} />
</div>
{ [ 'default', ...Object.keys(roles) ]
.map(id => {
if (id === 'default') {
return (
<ButtonItem active={role === 'default'} onClick={() => setRole('default')}>
<Text id="app.settings.permissions.default_role" />
</ButtonItem>
)
} else {
return (
<ButtonItem active={role === id} onClick={() => setRole(id)}>
{ roles[id].name }
</ButtonItem>
)
<h1>
<Text id="app.settings.server_pages.roles.title" />
</h1>
<Plus
size={22}
onClick={() =>
openScreen({
id: "special_input",
type: "create_role",
server,
callback: (id) => setRole(id),
})
}
})
}
/>
</div>
{["default", ...Object.keys(roles)].map((id) => {
if (id === "default") {
return (
<ButtonItem
active={role === "default"}
onClick={() => setRole("default")}>
<Text id="app.settings.permissions.default_role" />
</ButtonItem>
);
}
return (
<ButtonItem
key={id}
active={role === id}
onClick={() => setRole(id)}
style={{ color: roles[id].colour }}>
{roles[id].name}
</ButtonItem>
);
})}
</div>
<div className={styles.permissions}>
<div className={styles.title}>
<h2>{ role === 'default' ? <Text id="app.settings.permissions.default_role" /> : roles[role].name }</h2>
<Button contrast disabled={!modified} onClick={save}>Save</Button>
<h2>
{role === "default" ? (
<Text id="app.settings.permissions.default_role" />
) : (
roles[role].name
)}
</h2>
<Button contrast disabled={!modified} onClick={save}>
Save
</Button>
</div>
{role !== "default" && (
<>
<section>
<Overline type="subtle">Role Name</Overline>
<p>
<InputBox
value={name}
onChange={(e) =>
setName(e.currentTarget.value)
}
contrast
/>
</p>
</section>
<section>
<Overline type="subtle">Role Colour</Overline>
<p>
<ColourSwatches
value={colour ?? "gray"}
onChange={(value) => setColour(value)}
/>
</p>
</section>
</>
)}
<section>
<Overline type="subtle"><Text id="app.settings.permissions.server" /></Overline>
{ Object.keys(ServerPermission)
.map(key => {
if (key === 'View') return;
let value = ServerPermission[key as keyof typeof ServerPermission];
return (
<Checkbox checked={(perm[0] & value) > 0}
onChange={() => setPerm([ perm[0] ^ value, perm[1] ])}
description={<Text id={`permissions.server.${key}.d`} />}>
<Text id={`permissions.server.${key}.t`} />
</Checkbox>
)
})
}
<Overline type="subtle">
<Text id="app.settings.permissions.server" />
</Overline>
{Object.keys(ServerPermission).map((key) => {
if (key === "View") return;
const value =
ServerPermission[
key as keyof typeof ServerPermission
];
return (
<Checkbox
key={key}
checked={(perm[0] & value) > 0}
onChange={() =>
setPerm([perm[0] ^ value, perm[1]])
}
description={
<Text id={`permissions.server.${key}.d`} />
}>
<Text id={`permissions.server.${key}.t`} />
</Checkbox>
);
})}
</section>
<section>
<Overline type="subtle"><Text id="app.settings.permissions.channel" /></Overline>
{ Object.keys(ChannelPermission)
.map(key => {
if (key === 'ManageChannel') return;
let value = ChannelPermission[key as keyof typeof ChannelPermission];
return (
<Checkbox checked={((perm[1] >>> 0) & value) > 0}
onChange={() => setPerm([ perm[0], perm[1] ^ value ])}
disabled={key === 'View'}
description={<Text id={`permissions.channel.${key}.d`} />}>
<Text id={`permissions.channel.${key}.t`} />
</Checkbox>
)
})
}
<Overline type="subtle">
<Text id="app.settings.permissions.channel" />
</Overline>
{Object.keys(ChannelPermission).map((key) => {
if (key === "ManageChannel") return;
const value =
ChannelPermission[
key as keyof typeof ChannelPermission
];
return (
<Checkbox
key={key}
checked={((perm[1] >>> 0) & value) > 0}
onChange={() =>
setPerm([perm[0], perm[1] ^ value])
}
disabled={key === "View"}
description={
<Text id={`permissions.channel.${key}.d`} />
}>
<Text id={`permissions.channel.${key}.t`} />
</Checkbox>
);
})}
</section>
<div className={styles.actions}>
<Button contrast disabled={!modified} onClick={save}>Save</Button>
{ role !== 'default' && <Button contrast error onClick={deleteRole}>Delete</Button> }
<Button contrast disabled={!modified} onClick={save}>
Save
</Button>
{role !== "default" && (
<Button contrast error onClick={deleteRole}>
Delete
</Button>
)}
</div>
</div>
</div>
);
}
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
/* eslint-disable */
import JSX = preact.JSX;
import { store } from ".";
import localForage from "localforage";
import { Provider } from "react-redux";
import { Children } from "../types/Preact";
import { useEffect, useState } from "preact/hooks";
import { dispatch, State, store } from ".";
import { Children } from "../types/Preact";
interface Props {
children: Children;
}
export default function State(props: Props) {
export default function StateLoader(props: Props) {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
localForage.getItem("state")
.then(state => {
if (state !== null) {
store.dispatch({ type: "__INIT", state });
}
setLoaded(true);
});
localForage.getItem("state").then((state) => {
if (state !== null) {
dispatch({ type: "__INIT", state: state as State });
}
setLoaded(true);
});
}, []);
if (!loaded) return null;
......
/* eslint-disable @typescript-eslint/no-explicit-any */
import { connect, ConnectedComponent } from "react-redux";
import { State } from ".";
import { h } from "preact";
import { memo } from "preact/compat";
import { connect, ConnectedComponent } from "react-redux";
import { State } from ".";
export function connectState<T>(
component: (props: any) => h.JSX.Element | null,
mapKeys: (state: State, props: T) => any,
useDispatcher?: boolean,
memoize?: boolean
memoize?: boolean,
): ConnectedComponent<(props: any) => h.JSX.Element | null, T> {
let c = (
useDispatcher
? connect(mapKeys, (dispatcher) => {
return { dispatcher };
})
: connect(mapKeys)
)(component);
const c = connect(mapKeys)(component);
return memoize ? memo(c) : c;
}
import { createStore } from "redux";
import rootReducer from "./reducers";
import localForage from "localforage";
import { createStore } from "redux";
import { RevoltConfiguration } from "revolt-api/types/Core";
import { Core } from "revolt.js/dist/api/objects";
import { Typing } from "./reducers/typing";
import { Drafts } from "./reducers/drafts";
import { AuthState } from "./reducers/auth";
import { Language } from "../context/Locale";
import { Unreads } from "./reducers/unreads";
import { SyncOptions } from "./reducers/sync";
import { Settings } from "./reducers/settings";
import { QueuedMessage } from "./reducers/queue";
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 { Unreads } from "./reducers/unreads";
export type State = {
config: Core.RevoltNodeConfiguration,
config: RevoltConfiguration;
locale: Language;
auth: AuthState;
settings: Settings;
unreads: Unreads;
queue: QueuedMessage[];
typing: Typing;
drafts: Drafts;
sync: SyncOptions;
experiments: ExperimentOptions;
......@@ -59,7 +58,7 @@ store.subscribe(() => {
experiments,
lastOpened,
notifications,
sectionToggle
sectionToggle,
} = store.getState() as State;
localForage.setItem("state", {
......@@ -74,6 +73,14 @@ store.subscribe(() => {
experiments,
lastOpened,
notifications,
sectionToggle
sectionToggle,
});
});
export function dispatch(action: Action) {
store.dispatch(action);
}
export function getState(): State {
return store.getState();
}
import type { Auth } from "revolt.js/dist/api/objects";
import { Session } from "revolt-api/types/Auth";
export interface AuthState {
accounts: {
[key: string]: {
session: Auth.Session;
session: Session;
};
};
active?: string;
......@@ -13,7 +13,7 @@ export type AuthAction =
| { type: undefined }
| {
type: "LOGIN";
session: Auth.Session;
session: Session;
}
| {
type: "LOGOUT";
......@@ -22,7 +22,7 @@ export type AuthAction =
export function auth(
state = { accounts: {} } as AuthState,
action: AuthAction
action: AuthAction,
): AuthState {
switch (action.type) {
case "LOGIN":
......
export type Experiments = never;
export const AVAILABLE_EXPERIMENTS: Experiments[] = [];
export type Experiments = "search";
export const AVAILABLE_EXPERIMENTS: Experiments[] = ["search"];
export const EXPERIMENTS: {
[key in Experiments]: { title: string; description: string };
} = {
search: {
title: "Search",
description: "Allows you to search for messages in channels.",
},
};
export interface ExperimentOptions {
enabled?: Experiments[];
......@@ -18,7 +26,7 @@ export type ExperimentsAction =
export function experiments(
state = {} as ExperimentOptions,
action: ExperimentsAction
action: ExperimentsAction,
): ExperimentOptions {
switch (action.type) {
case "EXPERIMENTS_ENABLE":
......
import { State } from "..";
import { combineReducers } from "redux";
import { config, ConfigAction } from "./server_config";
import { settings, SettingsAction } from "./settings";
import { locale, LocaleAction } from "./locale";
import { State } from "..";
import { auth, AuthAction } from "./auth";
import { unreads, UnreadsAction } from "./unreads";
import { queue, QueueAction } from "./queue";
import { typing, TypingAction } from "./typing";
import { drafts, DraftAction } from "./drafts";
import { sync, SyncAction } from "./sync";
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 { unreads, UnreadsAction } from "./unreads";
export default combineReducers({
config,
......@@ -22,13 +21,12 @@ export default combineReducers({
settings,
unreads,
queue,
typing,
drafts,
sync,
experiments,
lastOpened,
notifications,
sectionToggle
sectionToggle,
});
export type Action =
......@@ -38,7 +36,6 @@ export type Action =
| SettingsAction
| UnreadsAction
| QueueAction
| TypingAction
| DraftAction
| SyncAction
| ExperimentsAction
......@@ -46,17 +43,3 @@ export type Action =
| NotificationsAction
| SectionToggleAction
| { type: "__INIT"; state: State };
export type WithDispatcher = { dispatcher: (action: Action) => void };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function filter(obj: any, keys: string[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newObj: any = {};
for (const key of keys) {
const v = obj[key];
if (v) newObj[key] = v;
}
return newObj;
}
export interface LastOpened {
[key: string]: string
[key: string]: string;
}
export type LastOpenedAction =
| { type: undefined }
| {
type: "LAST_OPENED_SET";
parent: string;
child: string;
type: "LAST_OPENED_SET";
parent: string;
child: string;
}
| {
type: "RESET";
};
export function lastOpened(state = {} as LastOpened, action: LastOpenedAction): LastOpened {
export function lastOpened(
state = {} as LastOpened,
action: LastOpenedAction,
): LastOpened {
switch (action.type) {
case "LAST_OPENED_SET": {
return {
...state,
[action.parent]: action.child
}
[action.parent]: action.child,
};
}
case "RESET":
return {};
......
import { Language } from "../../context/Locale";
import type { SyncUpdateAction } from "./sync";
export type LocaleAction =
......
import type { Channel, Message } from "revolt.js";
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 NotificationState = "all" | "mention" | "none" | "muted";
export type Notifications = {
[key: string]: NotificationState
}
[key: string]: NotificationState;
};
export const DEFAULT_STATES: { [key in Channel['channel_type']]: NotificationState } = {
'SavedMessages': 'all',
'DirectMessage': 'all',
'Group': 'all',
'TextChannel': 'mention',
'VoiceChannel': 'mention'
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) {
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) {
export function shouldNotify(
state: NotificationState,
message: Message,
user_id: string,
) {
switch (state) {
case 'muted':
case 'none': return false;
case 'mention': {
if (!message.mentions?.includes(user_id)) return false;
case "muted":
case "none":
return false;
case "mention": {
if (!message.mention_ids?.includes(user_id)) return false;
}
}
......@@ -34,34 +46,33 @@ export function shouldNotify(state: NotificationState, message: Message, user_id
export type NotificationsAction =
| { type: undefined }
| {
type: "NOTIFICATIONS_SET";
key: string;
state: NotificationState;
type: "NOTIFICATIONS_SET";
key: string;
state: NotificationState;
}
| {
type: "NOTIFICATIONS_REMOVE";
key: string;
type: "NOTIFICATIONS_REMOVE";
key: string;
}
| SyncUpdateAction
| {
type: "RESET";
type: "RESET";
};
export function notifications(
state = {} as Notifications,
action: NotificationsAction
action: NotificationsAction,
): Notifications {
switch (action.type) {
case "NOTIFICATIONS_SET":
return {
...state,
[action.key]: action.state
[action.key]: action.state,
};
case "NOTIFICATIONS_REMOVE":
{
const { [action.key]: _, ...newState } = state;
return newState;
}
case "NOTIFICATIONS_REMOVE": {
const { [action.key]: _, ...newState } = state;
return newState;
}
case "SYNC_UPDATE":
return action.update.notifications?.[1] ?? state;
case "RESET":
......
import type { MessageObject } from "../../context/revoltjs/util";
export enum QueueStatus {
SENDING = "sending",
ERRORED = "errored",
}
export interface Reply {
id: string,
mention: boolean
id: string;
mention: boolean;
}
export type QueuedMessageData = Omit<MessageObject, 'content' | 'replies'> & {
export type QueuedMessageData = {
_id: string;
author: string;
channel: string;
content: string;
replies: Reply[];
}
};
export interface QueuedMessage {
id: string;
......@@ -56,7 +58,7 @@ export type QueueAction =
export function queue(
state: QueuedMessage[] = [],
action: QueueAction
action: QueueAction,
): QueuedMessage[] {
switch (action.type) {
case "QUEUE_ADD": {
......@@ -72,7 +74,7 @@ export function queue(
}
case "QUEUE_FAIL": {
const entry = state.find(
(x) => x.id === action.nonce
(x) => x.id === action.nonce,
) as QueuedMessage;
return [
...state.filter((x) => x.id !== action.nonce),
......@@ -85,7 +87,7 @@ export function queue(
}
case "QUEUE_START": {
const entry = state.find(
(x) => x.id === action.nonce
(x) => x.id === action.nonce,
) as QueuedMessage;
return [
...state.filter((x) => x.id !== action.nonce),
......