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 686 additions and 351 deletions
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 { import {
Android, Safari,
Firefoxbrowser, Firefoxbrowser,
Googlechrome, Microsoftedge,
Ios,
Linux, Linux,
Macos, Macos,
Microsoftedge, Opera,
Safari,
Windows,
} from "@styled-icons/simple-icons"; } from "@styled-icons/simple-icons";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";
...@@ -19,6 +16,7 @@ import styles from "./Panes.module.scss"; ...@@ -19,6 +16,7 @@ import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import { dayjs } from "../../../context/Locale";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
...@@ -52,7 +50,7 @@ export function Sessions() { ...@@ -52,7 +50,7 @@ export function Sessions() {
); );
setSessions(data); setSessions(data);
}); });
}, []); }, [client, setSessions, deviceId]);
if (typeof sessions === "undefined") { if (typeof sessions === "undefined") {
return ( return (
...@@ -66,15 +64,19 @@ export function Sessions() { ...@@ -66,15 +64,19 @@ export function Sessions() {
const name = session.friendly_name; const name = session.friendly_name;
switch (true) { switch (true) {
case /firefox/i.test(name): case /firefox/i.test(name):
return <Firefoxbrowser />; return <Firefoxbrowser size={32} />;
case /chrome/i.test(name): case /chrome/i.test(name):
return <Googlechrome />; return <Chrome size={32} />;
case /safari/i.test(name): case /safari/i.test(name):
return <Safari />; return <Safari size={32} />;
case /edge/i.test(name): 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: default:
return <HelpCircle />; return <HelpCircle size={32} />;
} }
} }
...@@ -82,15 +84,15 @@ export function Sessions() { ...@@ -82,15 +84,15 @@ export function Sessions() {
const name = session.friendly_name; const name = session.friendly_name;
switch (true) { switch (true) {
case /linux/i.test(name): case /linux/i.test(name):
return <Linux />; return <Linux size={14} />;
case /android/i.test(name): case /android/i.test(name):
return <Android />; return <Android size={14} />;
case /mac.*os/i.test(name): case /mac.*os/i.test(name):
return <Macos />; return <Macos size={14} />;
case /ios/i.test(name): case /ios/i.test(name):
return <Ios />; return <Apple size={14} />;
case /windows/i.test(name): case /windows/i.test(name):
return <Windows />; return <Windows size={14} />;
default: default:
return null; return null;
} }
...@@ -104,7 +106,7 @@ export function Sessions() { ...@@ -104,7 +106,7 @@ export function Sessions() {
}); });
mapped.sort((a, b) => b.timestamp - a.timestamp); 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 = [ const render = [
mapped[id], mapped[id],
...@@ -117,62 +119,116 @@ export function Sessions() { ...@@ -117,62 +119,116 @@ export function Sessions() {
<h3> <h3>
<Text id="app.settings.pages.sessions.active_sessions" /> <Text id="app.settings.pages.sessions.active_sessions" />
</h3> </h3>
{render.map((session) => ( {render.map((session) => {
<div const systemIcon = getSystemIcon(session);
className={styles.entry} return (
data-active={session.id === deviceId} <div
data-deleting={attemptingDelete.indexOf(session.id) > -1}> key={session.id}
{deviceId === session.id && ( className={styles.entry}
<span className={styles.label}> data-active={session.id === deviceId}
<Text id="app.settings.pages.sessions.this_device" />{" "} data-deleting={
</span> attemptingDelete.indexOf(session.id) > -1
)} }>
<div className={styles.session}> {deviceId === session.id && (
<div className={styles.icon}> <span className={styles.label}>
{getIcon(session)} <Text id="app.settings.pages.sessions.this_device" />{" "}
<div>{getSystemIcon(session)}</div>
</div>
<div className={styles.info}>
<span className={styles.name}>
{session.friendly_name}
</span> </span>
<span className={styles.time}> )}
<Text <div className={styles.session}>
id="app.settings.pages.sessions.created" <div className={styles.detail}>
fields={{ <svg width={42} height={42} viewBox="0 0 32 32">
time_ago: dayjs( <foreignObject
session.timestamp, x="0"
).fromNow(), 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,
),
);
}} }}
/> disabled={
</span> attemptingDelete.indexOf(session.id) >
-1
}>
<Text id="app.settings.pages.logOut" />
</Button>
)}
</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,
),
);
}}
disabled={
attemptingDelete.indexOf(session.id) > -1
}>
<Text id="app.settings.pages.logOut" />
</Button>
)}
</div> </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> <Tip>
<span> <span>
<Text id="app.settings.tips.sessions.a" /> <Text id="app.settings.tips.sessions.a" />
......
...@@ -26,6 +26,7 @@ export function Component(props: Props) { ...@@ -26,6 +26,7 @@ export function Component(props: Props) {
] as [SyncKeys, string][] ] as [SyncKeys, string][]
).map(([key, title]) => ( ).map(([key, title]) => (
<Checkbox <Checkbox
key={key}
checked={ checked={
(props.options?.disabled ?? []).indexOf(key) === -1 (props.options?.disabled ?? []).indexOf(key) === -1
} }
......
import { Servers } from "revolt.js/dist/api/objects"; 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 { useContext, useEffect, useState } from "preact/hooks"; import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import UserIcon from "../../../components/common/user/UserIcon";
import IconButton from "../../../components/ui/IconButton";
import Tip from "../../../components/ui/Tip"; import Preloader from "../../../components/ui/Preloader";
interface Props { interface Props {
server: Servers.Server; server: Server;
} }
export function Bans({ server }: Props) { export const Bans = observer(({ server }: Props) => {
const client = useContext(AppContext); const [deleting, setDelete] = useState<string[]>([]);
const [bans, setBans] = useState<Servers.Ban[] | undefined>(undefined); const [data, setData] = useState<
Route<"GET", "/servers/id/bans">["response"] | undefined
>(undefined);
useEffect(() => { useEffect(() => {
client.servers.fetchBans(server._id).then((bans) => setBans(bans)); server.fetchBans().then(setData);
}, []); }, [server, setData]);
return ( return (
<div> <div className={styles.userList}>
<Tip warning>This section is under construction.</Tip> <div className={styles.subtitle}>
{bans?.map((x) => ( <span>
<div> <Text id="app.settings.server_pages.bans.user" />
{x._id.user}: {x.reason ?? "no reason"}{" "} </span>
<button <span class={styles.reason}>
onClick={() => <Text id="app.settings.server_pages.bans.reason" />
client.servers.unbanUser(server._id, x._id.user) </span>
}> <span>
unban <Text id="app.settings.server_pages.bans.revoke" />
</button> </span>
</div> </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> </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 { XCircle } from "@styled-icons/boxicons-regular"; import { XCircle } from "@styled-icons/boxicons-regular";
import { Invites as InvitesNS, Servers } from "revolt.js/dist/api/objects"; 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 styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { import { useClient } from "../../../context/revoltjs/RevoltClient";
useChannels,
useForceUpdate,
useUsers,
} from "../../../context/revoltjs/hooks";
import { getChannelName } from "../../../context/revoltjs/util"; import { getChannelName } from "../../../context/revoltjs/util";
import UserIcon from "../../../components/common/user/UserIcon"; import UserIcon from "../../../components/common/user/UserIcon";
...@@ -17,57 +15,68 @@ import IconButton from "../../../components/ui/IconButton"; ...@@ -17,57 +15,68 @@ import IconButton from "../../../components/ui/IconButton";
import Preloader from "../../../components/ui/Preloader"; import Preloader from "../../../components/ui/Preloader";
interface Props { interface Props {
server: Servers.Server; server: Server;
} }
export function Invites({ server }: Props) { export const Invites = observer(({ server }: Props) => {
const [invites, setInvites] = useState<
InvitesNS.ServerInvite[] | undefined
>(undefined);
const ctx = useForceUpdate();
const [deleting, setDelete] = useState<string[]>([]); const [deleting, setDelete] = useState<string[]>([]);
const users = useUsers(invites?.map((x) => x.creator) ?? [], ctx); const [invites, setInvites] = useState<ServerInvite[] | undefined>(
const channels = useChannels(invites?.map((x) => x.channel) ?? [], ctx); 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(() => { useEffect(() => {
ctx.client.servers server.fetchInvites().then(setInvites);
.fetchInvites(server._id) }, [server, setInvites]);
.then((invites) => setInvites(invites));
}, []);
return ( return (
<div className={styles.invites}> <div className={styles.userList}>
<div className={styles.subtitle}> <div className={styles.subtitle}>
<span>Invite Code</span> <span>
<span>Invitor</span> <Text id="app.settings.server_pages.invites.code" />
<span>Channel</span> </span>
<span>Revoke</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> </div>
{typeof invites === "undefined" && <Preloader type="ring" />} {typeof invites === "undefined" && <Preloader type="ring" />}
{invites?.map((invite) => { {invites?.map((invite, index) => {
let creator = users.find((x) => x?._id === invite.creator); const creator = users![index];
let channel = channels.find((x) => x?._id === invite.channel); const channel = channels![index];
return ( return (
<div <div
key={invite._id}
className={styles.invite} className={styles.invite}
data-deleting={deleting.indexOf(invite._id) > -1}> data-deleting={deleting.indexOf(invite._id) > -1}>
<code>{invite._id}</code> <code>{invite._id}</code>
<span> <span>
<UserIcon target={creator} size={24} />{" "} <UserIcon target={creator} size={24} />{" "}
{creator?.username ?? "unknown"} {creator?.username ?? (
<Text id="app.main.channel.unknown_user" />
)}
</span> </span>
<span> <span>
{channel && creator {channel && creator
? getChannelName(ctx.client, channel, true) ? getChannelName(channel, true)
: "#unknown"} : "#??"}
</span> </span>
<IconButton <IconButton
onClick={async () => { onClick={async () => {
setDelete([...deleting, invite._id]); setDelete([...deleting, invite._id]);
await ctx.client.deleteInvite(invite._id); await client.deleteInvite(invite._id);
setInvites( setInvites(
invites?.filter( invites?.filter(
...@@ -83,4 +92,4 @@ export function Invites({ server }: Props) { ...@@ -83,4 +92,4 @@ export function Invites({ server }: Props) {
})} })}
</div> </div>
); );
} });
import { Servers } from "revolt.js/dist/api/objects"; 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 styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
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 { interface Props {
server: Servers.Server; server: Server;
} }
// ! FIXME: bad code :) export const Members = observer(({ server }: Props) => {
export function Members({ server }: Props) { const [selected, setSelected] = useState<undefined | string>();
const [members, setMembers] = useState<Servers.Member[] | undefined>( const [data, setData] = useState<
undefined, { members: Member[]; users: User[] } | undefined
); >(undefined);
const ctx = useForceUpdate(); useEffect(() => {
const users = useUsers(members?.map((x) => x._id.user) ?? [], ctx); server.fetchMembers().then(setData);
}, [server, setData]);
const [roles, setRoles] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
ctx.client.servers.members if (selected) {
.fetchMembers(server._id) setRoles(
.then((members) => setMembers(members)); data!.members.find((x) => x._id.user === selected)?.roles ?? [],
}, []); );
}
}, [setRoles, selected, data]);
return ( return (
<div className={styles.members}> <div className={styles.userList}>
<div className={styles.subtitle}> <div className={styles.subtitle}>
{members?.length ?? 0} Members {data?.members.length ?? 0} Members
</div> </div>
{members && {data &&
members.length > 0 && data.members.length > 0 &&
users?.map( data.members
(x) => .map((member) => {
x && ( return {
<div className={styles.member}> member,
<div>@{x.username}</div> 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> </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> </div>
); );
} });
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
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 styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { FileUploader } from "../../../context/revoltjs/FileUploads"; import { FileUploader } from "../../../context/revoltjs/FileUploads";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../../context/revoltjs/util"; import { getChannelName } from "../../../context/revoltjs/util";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
...@@ -16,12 +16,10 @@ import ComboBox from "../../../components/ui/ComboBox"; ...@@ -16,12 +16,10 @@ import ComboBox from "../../../components/ui/ComboBox";
import InputBox from "../../../components/ui/InputBox"; import InputBox from "../../../components/ui/InputBox";
interface Props { interface Props {
server: Servers.Server; server: Server;
} }
export function Overview({ server }: Props) { export const Overview = observer(({ server }: Props) => {
const client = useContext(AppContext);
const [name, setName] = useState(server.name); const [name, setName] = useState(server.name);
const [description, setDescription] = useState(server.description ?? ""); const [description, setDescription] = useState(server.description ?? "");
const [systemMessages, setSystemMessages] = useState( const [systemMessages, setSystemMessages] = useState(
...@@ -40,16 +38,14 @@ export function Overview({ server }: Props) { ...@@ -40,16 +38,14 @@ export function Overview({ server }: Props) {
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
function save() { function save() {
let changes: Partial< const changes: Record<string, unknown> = {};
Pick<Servers.Server, "name" | "description" | "system_messages">
> = {};
if (name !== server.name) changes.name = name; if (name !== server.name) changes.name = name;
if (description !== server.description) if (description !== server.description)
changes.description = description; changes.description = description;
if (!isEqual(systemMessages, server.system_messages)) if (!isEqual(systemMessages, server.system_messages))
changes.system_messages = systemMessages; changes.system_messages = systemMessages ?? undefined;
client.servers.edit(server._id, changes); server.edit(changes);
setChanged(false); setChanged(false);
} }
...@@ -63,17 +59,9 @@ export function Overview({ server }: Props) { ...@@ -63,17 +59,9 @@ export function Overview({ server }: Props) {
fileType="icons" fileType="icons"
behaviour="upload" behaviour="upload"
maxFileSize={2_500_000} maxFileSize={2_500_000}
onUpload={(icon) => onUpload={(icon) => server.edit({ icon })}
client.servers.edit(server._id, { icon }) previewURL={server.generateIconURL({ max_side: 256 }, true)}
} remove={() => server.edit({ remove: "Icon" })}
previewURL={client.servers.getIconURL(
server._id,
{ max_side: 256 },
true,
)}
remove={() =>
client.servers.edit(server._id, { remove: "Icon" })
}
/> />
<div className={styles.name}> <div className={styles.name}>
<h3> <h3>
...@@ -115,17 +103,9 @@ export function Overview({ server }: Props) { ...@@ -115,17 +103,9 @@ export function Overview({ server }: Props) {
fileType="banners" fileType="banners"
behaviour="upload" behaviour="upload"
maxFileSize={6_000_000} maxFileSize={6_000_000}
onUpload={(banner) => onUpload={(banner) => server.edit({ banner })}
client.servers.edit(server._id, { banner }) previewURL={server.generateBannerURL({ width: 1000 }, true)}
} remove={() => server.edit({ remove: "Banner" })}
previewURL={client.servers.getBannerURL(
server._id,
{ width: 1000 },
true,
)}
remove={() =>
client.servers.edit(server._id, { remove: "Banner" })
}
/> />
<h3> <h3>
...@@ -139,6 +119,7 @@ export function Overview({ server }: Props) { ...@@ -139,6 +119,7 @@ export function Overview({ server }: Props) {
].map(([i18n, key]) => ( ].map(([i18n, key]) => (
// ! FIXME: temporary code just so we can expose the options // ! FIXME: temporary code just so we can expose the options
<p <p
key={key}
style={{ style={{
display: "flex", display: "flex",
gap: "8px", gap: "8px",
...@@ -170,15 +151,13 @@ export function Overview({ server }: Props) { ...@@ -170,15 +151,13 @@ export function Overview({ server }: Props) {
<option value="disabled"> <option value="disabled">
<Text id="general.disabled" /> <Text id="general.disabled" />
</option> </option>
{server.channels.map((id) => { {server.channels
const channel = client.channels.get(id); .filter((x) => typeof x !== "undefined")
if (!channel) return null; .map((channel) => (
return ( <option key={channel!._id} value={channel!._id}>
<option value={id}> {getChannelName(channel!, true)}
{getChannelName(client, channel, true)}
</option> </option>
); ))}
})}
</ComboBox> </ComboBox>
</p> </p>
))} ))}
...@@ -190,4 +169,4 @@ export function Overview({ server }: Props) { ...@@ -190,4 +169,4 @@ export function Overview({ server }: Props) {
</p> </p>
</div> </div>
); );
} });
...@@ -17,21 +17,32 @@ ...@@ -17,21 +17,32 @@
} }
} }
.invites { .userList {
gap: 8px; gap: 8px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.subtitle { .subtitle {
gap: 8px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
font-size: 13px; font-size: 13px;
text-transform: uppercase; text-transform: uppercase;
color: var(--secondary-foreground); color: var(--secondary-foreground);
font-weight: 700; font-weight: 700;
.reason {
text-align: center;
}
}
.reason {
flex: 2;
} }
.invite { .invite,
.ban,
.member {
gap: 8px; gap: 8px;
padding: 10px; padding: 10px;
display: flex; display: flex;
...@@ -39,7 +50,8 @@ ...@@ -39,7 +50,8 @@
flex-direction: row; flex-direction: row;
background: var(--secondary-background); background: var(--secondary-background);
code, span { span,
code {
flex: 1; flex: 1;
} }
...@@ -58,25 +70,23 @@ ...@@ -58,25 +70,23 @@
opacity: 0.5; 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 { .member {
gap: 8px; cursor: pointer;
.chevron {
transition: 0.2s ease all;
}
&:not([data-open="true"]) .chevron {
transform: rotateZ(90deg);
}
}
.memberView {
padding: 10px; padding: 10px;
display: flex; margin: 0 10px;
align-items: center; background: var(--background);
flex-direction: row;
background: var(--secondary-background);
} }
} }
...@@ -95,10 +105,6 @@ ...@@ -95,10 +105,6 @@
flex-grow: 1; flex-grow: 1;
padding: 0 8px; padding: 0 8px;
overflow-y: scroll; overflow-y: scroll;
section {
margin-bottom: 1em;
}
} }
.title { .title {
...@@ -107,7 +113,8 @@ ...@@ -107,7 +113,8 @@
margin-bottom: 1em; margin-bottom: 1em;
align-items: center; align-items: center;
h1, h2 { h1,
h2 {
margin: 0; margin: 0;
min-width: 0; min-width: 0;
flex-grow: 1; flex-grow: 1;
...@@ -126,4 +133,4 @@ ...@@ -126,4 +133,4 @@
display: flex; display: flex;
padding: 8px 0; padding: 8px 0;
} }
} }
\ No newline at end of file
import { Plus } from "@styled-icons/boxicons-regular"; import { Plus } from "@styled-icons/boxicons-regular";
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import { Servers } from "revolt.js/dist/api/objects"; import { observer } from "mobx-react-lite";
import { import { ChannelPermission, ServerPermission } from "revolt.js";
ChannelPermission, import { Server } from "revolt.js/dist/maps/Servers";
ServerPermission,
} from "revolt.js/dist/api/permissions";
import styles from "./Panes.module.scss"; import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox"; import Checkbox from "../../../components/ui/Checkbox";
import IconButton from "../../../components/ui/IconButton"; import ColourSwatches from "../../../components/ui/ColourSwatches";
import InputBox from "../../../components/ui/InputBox"; import InputBox from "../../../components/ui/InputBox";
import Overline from "../../../components/ui/Overline"; import Overline from "../../../components/ui/Overline";
import Tip from "../../../components/ui/Tip";
import ButtonItem from "../../../components/navigation/items/ButtonItem"; import ButtonItem from "../../../components/navigation/items/ButtonItem";
interface Props { 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 :) // ! FIXME: bad code :)
export function Roles({ server }: Props) { export const Roles = observer(({ server }: Props) => {
const [role, setRole] = useState("default"); const [role, setRole] = useState("default");
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const client = useContext(AppContext); const roles = useMemo(() => server.roles ?? {}, [server]);
const roles = server.roles ?? {};
if (role !== "default" && typeof roles[role] === "undefined") { if (role !== "default" && typeof roles[role] === "undefined") {
useEffect(() => setRole("default")); useEffect(() => setRole("default"), [role]);
return null; return null;
} }
const v = (id: string) => const {
I32ToU32( name: roleName,
id === "default" colour: roleColour,
? server.default_permissions permissions,
: roles[id].permissions, } = roles[role] ?? {};
);
const [perm, setPerm] = useState(v(role)); const getPermissions = useCallback(
useEffect(() => setPerm(v(role)), [role, roles[role]?.permissions]); (id: string) => {
return I32ToU32(
const modified = !isEqual(perm, v(role)); id === "default"
const save = () => ? server.default_permissions
client.servers.setPermissions(server._id, role, { : roles[id].permissions,
server: perm[0], );
channel: perm[1], },
}); [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 deleteRole = () => { const deleteRole = () => {
setRole("default"); setRole("default");
client.servers.deleteRole(server._id, role); server.deleteRole(role);
}; };
return ( return (
...@@ -73,7 +100,7 @@ export function Roles({ server }: Props) { ...@@ -73,7 +100,7 @@ export function Roles({ server }: Props) {
openScreen({ openScreen({
id: "special_input", id: "special_input",
type: "create_role", type: "create_role",
server: server._id, server,
callback: (id) => setRole(id), callback: (id) => setRole(id),
}) })
} }
...@@ -88,15 +115,16 @@ export function Roles({ server }: Props) { ...@@ -88,15 +115,16 @@ export function Roles({ server }: Props) {
<Text id="app.settings.permissions.default_role" /> <Text id="app.settings.permissions.default_role" />
</ButtonItem> </ButtonItem>
); );
} else {
return (
<ButtonItem
active={role === id}
onClick={() => setRole(id)}>
{roles[id].name}
</ButtonItem>
);
} }
return (
<ButtonItem
key={id}
active={role === id}
onClick={() => setRole(id)}
style={{ color: roles[id].colour }}>
{roles[id].name}
</ButtonItem>
);
})} })}
</div> </div>
<div className={styles.permissions}> <div className={styles.permissions}>
...@@ -112,19 +140,45 @@ export function Roles({ server }: Props) { ...@@ -112,19 +140,45 @@ export function Roles({ server }: Props) {
Save Save
</Button> </Button>
</div> </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> <section>
<Overline type="subtle"> <Overline type="subtle">
<Text id="app.settings.permissions.server" /> <Text id="app.settings.permissions.server" />
</Overline> </Overline>
{Object.keys(ServerPermission).map((key) => { {Object.keys(ServerPermission).map((key) => {
if (key === "View") return; if (key === "View") return;
let value = const value =
ServerPermission[ ServerPermission[
key as keyof typeof ServerPermission key as keyof typeof ServerPermission
]; ];
return ( return (
<Checkbox <Checkbox
key={key}
checked={(perm[0] & value) > 0} checked={(perm[0] & value) > 0}
onChange={() => onChange={() =>
setPerm([perm[0] ^ value, perm[1]]) setPerm([perm[0] ^ value, perm[1]])
...@@ -143,13 +197,14 @@ export function Roles({ server }: Props) { ...@@ -143,13 +197,14 @@ export function Roles({ server }: Props) {
</Overline> </Overline>
{Object.keys(ChannelPermission).map((key) => { {Object.keys(ChannelPermission).map((key) => {
if (key === "ManageChannel") return; if (key === "ManageChannel") return;
let value = const value =
ChannelPermission[ ChannelPermission[
key as keyof typeof ChannelPermission key as keyof typeof ChannelPermission
]; ];
return ( return (
<Checkbox <Checkbox
key={key}
checked={((perm[1] >>> 0) & value) > 0} checked={((perm[1] >>> 0) & value) > 0}
onChange={() => onChange={() =>
setPerm([perm[0], perm[1] ^ value]) setPerm([perm[0], perm[1] ^ value])
...@@ -176,4 +231,4 @@ export function Roles({ server }: Props) { ...@@ -176,4 +231,4 @@ export function Roles({ server }: Props) {
</div> </div>
</div> </div>
); );
} });
// eslint-disable-next-line @typescript-eslint/no-unused-vars /* eslint-disable */
import JSX = preact.JSX; import JSX = preact.JSX;
...@@ -11,6 +11,6 @@ export function connectState<T>( ...@@ -11,6 +11,6 @@ export function connectState<T>(
mapKeys: (state: State, props: T) => any, mapKeys: (state: State, props: T) => any,
memoize?: boolean, memoize?: boolean,
): ConnectedComponent<(props: any) => h.JSX.Element | null, T> { ): ConnectedComponent<(props: any) => h.JSX.Element | null, T> {
let c = connect(mapKeys)(component); const c = connect(mapKeys)(component);
return memoize ? memo(c) : c; return memoize ? memo(c) : c;
} }
import localForage from "localforage"; import localForage from "localforage";
import { createStore } from "redux"; import { createStore } from "redux";
import { Core } from "revolt.js/dist/api/objects"; import { RevoltConfiguration } from "revolt-api/types/Core";
import { Language } from "../context/Locale"; import { Language } from "../context/Locale";
...@@ -14,17 +14,15 @@ import { QueuedMessage } from "./reducers/queue"; ...@@ -14,17 +14,15 @@ import { QueuedMessage } from "./reducers/queue";
import { SectionToggle } from "./reducers/section_toggle"; import { SectionToggle } from "./reducers/section_toggle";
import { Settings } from "./reducers/settings"; import { Settings } from "./reducers/settings";
import { SyncOptions } from "./reducers/sync"; import { SyncOptions } from "./reducers/sync";
import { Typing } from "./reducers/typing";
import { Unreads } from "./reducers/unreads"; import { Unreads } from "./reducers/unreads";
export type State = { export type State = {
config: Core.RevoltNodeConfiguration; config: RevoltConfiguration;
locale: Language; locale: Language;
auth: AuthState; auth: AuthState;
settings: Settings; settings: Settings;
unreads: Unreads; unreads: Unreads;
queue: QueuedMessage[]; queue: QueuedMessage[];
typing: Typing;
drafts: Drafts; drafts: Drafts;
sync: SyncOptions; sync: SyncOptions;
experiments: ExperimentOptions; experiments: ExperimentOptions;
...@@ -82,3 +80,7 @@ store.subscribe(() => { ...@@ -82,3 +80,7 @@ store.subscribe(() => {
export function dispatch(action: Action) { export function dispatch(action: Action) {
store.dispatch(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 { export interface AuthState {
accounts: { accounts: {
[key: string]: { [key: string]: {
session: Auth.Session; session: Session;
}; };
}; };
active?: string; active?: string;
...@@ -13,7 +13,7 @@ export type AuthAction = ...@@ -13,7 +13,7 @@ export type AuthAction =
| { type: undefined } | { type: undefined }
| { | {
type: "LOGIN"; type: "LOGIN";
session: Auth.Session; session: Session;
} }
| { | {
type: "LOGOUT"; type: "LOGOUT";
......
export type Experiments = never; export type Experiments = "search";
export const AVAILABLE_EXPERIMENTS: Experiments[] = []; 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 { export interface ExperimentOptions {
enabled?: Experiments[]; enabled?: Experiments[];
......
...@@ -12,7 +12,6 @@ import { sectionToggle, SectionToggleAction } from "./section_toggle"; ...@@ -12,7 +12,6 @@ import { sectionToggle, SectionToggleAction } from "./section_toggle";
import { config, ConfigAction } from "./server_config"; import { config, ConfigAction } from "./server_config";
import { settings, SettingsAction } from "./settings"; import { settings, SettingsAction } from "./settings";
import { sync, SyncAction } from "./sync"; import { sync, SyncAction } from "./sync";
import { typing, TypingAction } from "./typing";
import { unreads, UnreadsAction } from "./unreads"; import { unreads, UnreadsAction } from "./unreads";
export default combineReducers({ export default combineReducers({
...@@ -22,7 +21,6 @@ export default combineReducers({ ...@@ -22,7 +21,6 @@ export default combineReducers({
settings, settings,
unreads, unreads,
queue, queue,
typing,
drafts, drafts,
sync, sync,
experiments, experiments,
...@@ -38,7 +36,6 @@ export type Action = ...@@ -38,7 +36,6 @@ export type Action =
| SettingsAction | SettingsAction
| UnreadsAction | UnreadsAction
| QueueAction | QueueAction
| TypingAction
| DraftAction | DraftAction
| SyncAction | SyncAction
| ExperimentsAction | ExperimentsAction
...@@ -46,15 +43,3 @@ export type Action = ...@@ -46,15 +43,3 @@ export type Action =
| NotificationsAction | NotificationsAction
| SectionToggleAction | SectionToggleAction
| { type: "__INIT"; state: State }; | { type: "__INIT"; state: State };
// 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;
}
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"; import type { SyncUpdateAction } from "./sync";
...@@ -35,7 +36,7 @@ export function shouldNotify( ...@@ -35,7 +36,7 @@ export function shouldNotify(
case "none": case "none":
return false; return false;
case "mention": { case "mention": {
if (!message.mentions?.includes(user_id)) return false; if (!message.mention_ids?.includes(user_id)) return false;
} }
} }
......
import type { MessageObject } from "../../context/revoltjs/util";
export enum QueueStatus { export enum QueueStatus {
SENDING = "sending", SENDING = "sending",
ERRORED = "errored", ERRORED = "errored",
...@@ -10,7 +8,11 @@ export interface Reply { ...@@ -10,7 +8,11 @@ export interface Reply {
mention: boolean; mention: boolean;
} }
export type QueuedMessageData = Omit<MessageObject, "content" | "replies"> & { export type QueuedMessageData = {
_id: string;
author: string;
channel: string;
content: string; content: string;
replies: Reply[]; replies: Reply[];
}; };
......
import type { Core } from "revolt.js/dist/api/objects"; import type { RevoltConfiguration } from "revolt-api/types/Core";
export type ConfigAction = export type ConfigAction =
| { type: undefined } | { type: undefined }
| { | {
type: "SET_CONFIG"; type: "SET_CONFIG";
config: Core.RevoltNodeConfiguration; config: RevoltConfiguration;
}; };
export function config( export function config(
state = {} as Core.RevoltNodeConfiguration, state = {} as RevoltConfiguration,
action: ConfigAction, action: ConfigAction,
): Core.RevoltNodeConfiguration { ): RevoltConfiguration {
switch (action.type) { switch (action.type) {
case "SET_CONFIG": case "SET_CONFIG":
return action.config; return action.config;
......
...@@ -2,7 +2,6 @@ import type { Theme, ThemeOptions } from "../../context/Theme"; ...@@ -2,7 +2,6 @@ import type { Theme, ThemeOptions } from "../../context/Theme";
import { setEmojiPack } from "../../components/common/Emoji"; import { setEmojiPack } from "../../components/common/Emoji";
import { filter } from ".";
import type { Sounds } from "../../assets/sounds/Audio"; import type { Sounds } from "../../assets/sounds/Audio";
import type { SyncUpdateAction } from "./sync"; import type { SyncUpdateAction } from "./sync";
...@@ -67,7 +66,7 @@ export function settings( ...@@ -67,7 +66,7 @@ export function settings(
return { return {
...state, ...state,
theme: { theme: {
...filter(state.theme, ["custom", "preset", "ligatures"]), ...state.theme,
...action.theme, ...action.theme,
}, },
}; };
...@@ -94,7 +93,7 @@ export function settings( ...@@ -94,7 +93,7 @@ export function settings(
return { return {
...state, ...state,
appearance: { appearance: {
...filter(state.appearance, ["emojiPack"]), ...state.appearance,
...action.options, ...action.options,
}, },
}; };
......
export type TypingUser = { id: string; started: number };
export type Typing = { [key: string]: TypingUser[] };
export type TypingAction =
| { type: undefined }
| {
type: "TYPING_START";
channel: string;
user: string;
}
| {
type: "TYPING_STOP";
channel: string;
user: string;
}
| {
type: "RESET";
};
export function typing(state: Typing = {}, action: TypingAction): Typing {
switch (action.type) {
case "TYPING_START":
return {
...state,
[action.channel]: [
...(state[action.channel] ?? []).filter(
(x) => x.id !== action.user,
),
{
id: action.user,
started: +new Date(),
},
],
};
case "TYPING_STOP":
return {
...state,
[action.channel]:
state[action.channel]?.filter(
(x) => x.id !== action.user,
) ?? [],
};
case "RESET":
return {};
default:
return state;
}
}