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 641 additions and 462 deletions
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="468pt" height="617pt" viewBox="0 0 468 617"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.5, written by Peter Selinger 2001-2004
</metadata>
<g transform="translate(0,617) scale(0.709091,-0.709091)"
fill="#000099" stroke="none">
<path fill="#000099" stroke="none" d="M302 838 c-14 -14 -16 -126 -3 -147 5
-8 16 -11 25 -8 12 5 16 21 16 71 0 89 -10 112 -38 84z"/>
<path fill="#000099" stroke="none" d="M521 775 c-27 -57 -32 -108 -10 -113
18 -3 84 122 75 144 -11 30 -44 15 -65 -31z"/>
<path fill="#000099" stroke="none" d="M34 797 c-8 -22 59 -158 76 -154 38 7
-11 167 -51 167 -11 0 -22 -6 -25 -13z"/>
<path fill="#000099" stroke="none" d="M254 590 c-50 -7 -128 -52 -175 -100
-98 -100 -65 -346 57 -423 63 -40 107 -50 200 -44 125 7 212 62 275 172 53 92
32 220 -51 317 -62 71 -170 99 -306 78z"/>
<path fill="#ffff63" stroke="none" d="M443 539 c47 -13 112 -70 138 -120 24
-48 26 -147 3 -190 -22 -43 -82 -108 -117 -125 -137 -71 -277 -55 -351 41 -39
52 -51 92 -51 175 1 77 19 113 82 161 80 63 198 86 296 58z"/>
<path fill="#000099" stroke="none" d="M462 367 c-5 -7 -15 -28 -21 -48 -21
-67 -100 -120 -144 -98 -30 15 -65 56 -88 102 -21 40 -51 48 -57 14 -5 -26 53
-111 96 -141 89 -62 204 -7 252 119 15 40 -15 81 -38 52z"/>
</g>
</svg>
\ No newline at end of file
import { Channels } from "revolt.js/dist/api/objects";
import styled, { css } from "styled-components";
import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { FileUploader } from "../../../context/revoltjs/FileUploads";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Button from "../../../components/ui/Button";
import InputBox from "../../../components/ui/InputBox";
interface Props {
channel:
| Channels.GroupChannel
| Channels.TextChannel
| Channels.VoiceChannel;
channel: Channel;
}
const Row = styled.div`
......@@ -32,13 +29,11 @@ const Row = styled.div`
}
`;
export default function Overview({ channel }: Props) {
const client = useContext(AppContext);
const [name, setName] = useState(channel.name);
export default observer(({ channel }: Props) => {
const [name, setName] = useState(channel.name ?? undefined);
const [description, setDescription] = useState(channel.description ?? "");
useEffect(() => setName(channel.name), [channel.name]);
useEffect(() => setName(channel.name ?? undefined), [channel.name]);
useEffect(
() => setDescription(channel.description ?? ""),
[channel.description],
......@@ -46,12 +41,12 @@ export default function Overview({ channel }: Props) {
const [changed, setChanged] = useState(false);
function save() {
const changes: any = {};
const changes: Record<string, string | undefined> = {};
if (name !== channel.name) changes.name = name;
if (description !== channel.description)
changes.description = description;
client.channels.edit(channel._id, changes);
channel.edit(changes);
setChanged(false);
}
......@@ -65,17 +60,12 @@ export default function Overview({ channel }: Props) {
fileType="icons"
behaviour="upload"
maxFileSize={2_500_000}
onUpload={(icon) =>
client.channels.edit(channel._id, { icon })
}
previewURL={client.channels.getIconURL(
channel._id,
onUpload={(icon) => channel.edit({ icon })}
previewURL={channel.generateIconURL(
{ max_side: 256 },
true,
)}
remove={() =>
client.channels.edit(channel._id, { remove: "Icon" })
}
remove={() => channel.edit({ remove: "Icon" })}
defaultPreview={
channel.channel_type === "Group"
? "/assets/group.png"
......@@ -127,4 +117,4 @@ export default function Overview({ channel }: Props) {
</p>
</div>
);
}
});
import { Channels } from "revolt.js/dist/api/objects";
import { ChannelPermission } from "revolt.js/dist/api/permissions";
import { observer } from "mobx-react-lite";
import {
ChannelPermission,
DEFAULT_PERMISSION_DM,
} from "revolt.js/dist/api/permissions";
import { Channel } from "revolt.js/dist/maps/Channels";
import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { useServer } from "../../../context/revoltjs/hooks";
import { useEffect, useState } from "preact/hooks";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
import Tip from "../../../components/ui/Tip";
// ! FIXME: export from revolt.js
const DEFAULT_PERMISSION_DM =
ChannelPermission.View +
ChannelPermission.SendMessage +
ChannelPermission.ManageChannel +
ChannelPermission.VoiceCall +
ChannelPermission.InviteOthers +
ChannelPermission.EmbedLinks +
ChannelPermission.UploadFiles;
interface Props {
channel:
| Channels.GroupChannel
| Channels.TextChannel
| Channels.VoiceChannel;
channel: Channel;
}
// ! FIXME: bad code :)
export default function Permissions({ channel }: Props) {
export default observer(({ channel }: Props) => {
const [selected, setSelected] = useState("default");
const client = useContext(AppContext);
type R = { name: string; permissions: number };
const roles: { [key: string]: R } = {};
if (channel.channel_type !== "Group") {
const server = useServer(channel.server);
const server = channel.server;
const a = server?.roles ?? {};
for (const b of Object.keys(a)) {
roles[b] = {
......@@ -77,6 +64,7 @@ export default function Permissions({ channel }: Props) {
return (
<Checkbox
key={id}
checked={selected === id}
onChange={(selected) => selected && setSelected(id)}>
{role.name}
......@@ -104,10 +92,10 @@ export default function Permissions({ channel }: Props) {
<Button
contrast
onClick={() => {
client.channels.setPermissions(channel._id, selected, p);
channel.setPermissions(selected, p);
}}>
click here to save permissions for role
</Button>
</div>
);
}
});
import { At } from "@styled-icons/boxicons-regular";
import { Envelope, Key, HelpCircle } from "@styled-icons/boxicons-solid";
import { At, Key, Block } from "@styled-icons/boxicons-regular";
import {
Envelope,
HelpCircle,
Lock,
Trash,
} from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link, useHistory } from "react-router-dom";
import { Users } from "revolt.js/dist/api/objects";
import { useHistory } from "react-router-dom";
import { Profile } from "revolt-api/types/Users";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { useData } from "../../../mobx/State";
import { stopPropagation } from "../../../lib/stopPropagation";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import {
......@@ -16,26 +21,22 @@ import {
StatusContext,
useClient,
} from "../../../context/revoltjs/RevoltClient";
import { useForceUpdate } from "../../../context/revoltjs/hooks";
import Tooltip from "../../../components/common/Tooltip";
import UserIcon from "../../../components/common/user/UserIcon";
import Button from "../../../components/ui/Button";
import Tip from "../../../components/ui/Tip";
import CategoryButton from "../../../components/ui/fluent/CategoryButton";
export const Account = observer(() => {
const { openScreen, writeClipboard } = useIntermediate();
const status = useContext(StatusContext);
const client = useClient();
const store = useData();
const user = store.users.get(client.user!._id)!;
const [email, setEmail] = useState("...");
const [revealEmail, setRevealEmail] = useState(false);
const [profile, setProfile] = useState<undefined | Users.Profile>(
undefined,
);
const [profile, setProfile] = useState<undefined | Profile>(undefined);
const history = useHistory();
function switchPage(to: string) {
......@@ -50,99 +51,114 @@ export const Account = observer(() => {
}
if (profile === undefined && status === ClientStatus.ONLINE) {
client.users
.fetchProfile(user._id)
client
.user!.fetchProfile()
.then((profile) => setProfile(profile ?? {}));
}
}, [status]);
}, [client, email, profile, status]);
return (
<div className={styles.user}>
<div className={styles.banner}>
<UserIcon
className={styles.avatar}
target={user}
size={72}
onClick={() => switchPage("profile")}
/>
<div className={styles.userDetail}>
<div className={styles.username}>@{user.username}</div>
<div className={styles.userid}>
<Tooltip
content={
<Text id="app.settings.pages.account.unique_id" />
}>
<HelpCircle size={16} />
</Tooltip>
<Tooltip content={<Text id="app.special.copy" />}>
<a onClick={() => writeClipboard(user._id)}>
{user._id}
</a>
</Tooltip>
<div className={styles.container}>
<UserIcon
className={styles.avatar}
target={client.user!}
size={72}
onClick={() => switchPage("profile")}
/>
<div className={styles.userDetail}>
@{client.user!.username}
<div className={styles.userid}>
<Tooltip
content={
<Text id="app.settings.pages.account.unique_id" />
}>
<HelpCircle size={16} />
</Tooltip>
<Tooltip content={<Text id="app.special.copy" />}>
<a
onClick={() =>
writeClipboard(client.user!._id)
}>
{client.user!._id}
</a>
</Tooltip>
</div>
</div>
</div>
<Button onClick={() => switchPage("profile")} contrast>
<Text id="app.settings.pages.profile.edit_profile" />
</Button>
</div>
<div className={styles.details}>
<div>
{(
[
["username", user.username, <At size={24} />],
["email", email, <Envelope size={24} />],
["password", "***********", <Key size={24} />],
[
"username",
client.user!.username,
<At key="at" size={24} />,
],
["email", email, <Envelope key="envelope" size={24} />],
["password", "•••••••••", <Key key="key" size={24} />],
] as const
).map(([field, value, icon]) => (
<div>
{icon}
<div className={styles.detail}>
<div className={styles.subtext}>
<Text id={`login.${field}`} />
</div>
<p>
{field === "email" ? (
revealEmail ? (
value
) : (
<>
***********@{value.split("@").pop()}{" "}
<a
onClick={() =>
setRevealEmail(true)
}>
<Text id="app.special.modals.actions.reveal" />
</a>
</>
)
<CategoryButton
key={field}
icon={icon}
description={
field === "email" ? (
revealEmail ? (
<>
{value}{" "}
<a
onClick={(ev) =>
stopPropagation(
ev,
setRevealEmail(false),
)
}>
<Text id="app.special.modals.actions.hide" />
</a>
</>
) : (
value
)}
</p>
</div>
<div>
<Button
onClick={() =>
openScreen({
id: "modify_account",
field,
})
}
contrast>
<Text id="app.settings.pages.account.change_field" />
</Button>
</div>
</div>
<>
•••••••••••@{value.split("@").pop()}{" "}
<a
onClick={(ev) =>
stopPropagation(
ev,
setRevealEmail(true),
)
}>
<Text id="app.special.modals.actions.reveal" />
</a>
</>
)
) : (
value
)
}
account
action="chevron"
onClick={() =>
openScreen({
id: "modify_account",
field,
})
}>
<Text id={`login.${field}`} />
</CategoryButton>
))}
</div>
<h3>
<Text id="app.settings.pages.account.account_management.title" />
</h3>
<h5>
<Text id="app.settings.pages.account.account_management.description" />
</h5>
<h3>
<Text id="app.settings.pages.account.2fa.title" />
</h3>
<h5>
Currently work in progress, see{" "}
{/*<Text id="app.settings.pages.account.2fa.description" />*/}
Two-factor authentication is currently work-in-progress, see{" "}
{` `}
<a
href="https://gitlab.insrt.uk/insert/rauth/-/issues/2"
target="_blank"
......@@ -151,28 +167,39 @@ export const Account = observer(() => {
</a>
.
</h5>
{/*<h5><Text id="app.settings.pages.account.two_factor_auth.description" /></h5>
<Button accent compact>
<Text id="app.settings.pages.account.two_factor_auth.add_auth" />
</Button>*/}
<CategoryButton
icon={<Lock size={24} color="var(--error)" />}
description={"Set up 2FA Authentication on your account."}
disabled
action="chevron">
Set up Two-factor authentication
</CategoryButton>
<h3>
<Text id="app.settings.pages.account.manage.title" />
</h3>
<h5>
<Text id="app.settings.pages.account.manage.description" />
</h5>
<div className={styles.buttons}>
{/* <Button contrast>
<Text id="app.settings.pages.account.manage.disable" />
</Button> */}
<a href="mailto:contact@revolt.chat?subject=Delete%20my%20account">
<Button error compact>
<Text id="app.settings.pages.account.manage.delete" />
</Button>
</a>
</div>
<CategoryButton
icon={<Block size={24} color="var(--error)" />}
description={
"Disable your account. You won't be able to access it unless you log back in."
}
disabled
action={<Text id="general.unavailable" />}>
<Text id="app.settings.pages.account.manage.disable" />
</CategoryButton>
<a href="mailto:contact@revolt.chat?subject=Delete%20my%20account">
<CategoryButton
icon={<Trash size={24} color="var(--error)" />}
description={
"Delete your account, including all of your data."
}
hover
action="external">
<Text id="app.settings.pages.account.manage.delete" />
</CategoryButton>
</a>
<Tip>
<span>
<Text id="app.settings.tips.account.a" />
......
import { Reset, Import } from "@styled-icons/boxicons-regular";
import { Pencil } from "@styled-icons/boxicons-solid";
// @ts-ignore
// @ts-expect-error shade-blend-color does not have typings.
import pSBC from "shade-blend-color";
import styles from "./Panes.module.scss";
......@@ -17,8 +17,10 @@ import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
import {
DEFAULT_FONT,
DEFAULT_MONO_FONT,
Fonts,
FONTS,
FONT_KEYS,
MonospaceFonts,
MONOSPACE_FONTS,
MONOSPACE_FONT_KEYS,
Theme,
......@@ -57,12 +59,12 @@ export function Component(props: Props) {
});
}
function pushOverride(custom: Partial<Theme>) {
const pushOverride = useCallback((custom: Partial<Theme>) => {
dispatch({
type: "SETTINGS_SET_THEME_OVERRIDE",
custom,
});
}
}, []);
function setAccent(accent: string) {
setOverride({
......@@ -81,12 +83,14 @@ export function Component(props: Props) {
});
}
const setOverride = useCallback(debounce(pushOverride, 200), []) as (
custom: Partial<Theme>,
) => void;
// eslint-disable-next-line react-hooks/exhaustive-deps
const setOverride = useCallback(
debounce(pushOverride as (...args: unknown[]) => void, 200),
[pushOverride],
) as (custom: Partial<Theme>) => void;
const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? "");
useEffect(() => setOverride({ css }), [css]);
useEffect(() => setOverride({ css }), [setOverride, css]);
const selected = props.settings.theme?.preset ?? "dark";
return (
......@@ -170,15 +174,15 @@ export function Component(props: Props) {
<ComboBox
value={theme.font ?? DEFAULT_FONT}
onChange={(e) =>
pushOverride({ font: e.currentTarget.value as any })
pushOverride({ font: e.currentTarget.value as Fonts })
}>
{FONT_KEYS.map((key) => (
<option value={key}>
<option value={key} key={key}>
{FONTS[key as keyof typeof FONTS].name}
</option>
))}
</ComboBox>
{/* TOFIX: Only show when a font with ligature support is selected, i.e.: Inter.
{/* TOFIX: Only show when a font with ligature support is selected, i.e.: Inter.*/}
<p>
<Checkbox
checked={props.settings.theme?.ligatures === true}
......@@ -192,7 +196,7 @@ export function Component(props: Props) {
}>
<Text id="app.settings.pages.appearance.ligatures" />
</Checkbox>
</p>*/}
</p>
<h3>
<Text id="app.settings.pages.appearance.emoji_pack" />
......@@ -406,11 +410,12 @@ export function Component(props: Props) {
value={theme.monospaceFont ?? DEFAULT_MONO_FONT}
onChange={(e) =>
pushOverride({
monospaceFont: e.currentTarget.value as any,
monospaceFont: e.currentTarget
.value as MonospaceFonts,
})
}>
{MONOSPACE_FONT_KEYS.map((key) => (
<option value={key}>
<option value={key} key={key}>
{
MONOSPACE_FONTS[
key as keyof typeof MONOSPACE_FONTS
......
......@@ -23,6 +23,7 @@ export function Component(props: Props) {
</h3>
{AVAILABLE_EXPERIMENTS.map((key) => (
<Checkbox
key={key}
checked={(props.options?.enabled ?? []).indexOf(key) > -1}
onChange={(enabled) =>
dispatch({
......
......@@ -70,7 +70,7 @@ export function Feedback() {
placeholder={
(
<Text id="app.settings.pages.feedback.other" />
) as any
) as unknown as string
}
/>
</Localizer>
......
......@@ -13,6 +13,7 @@ import {
import Emoji from "../../../components/common/Emoji";
import Checkbox from "../../../components/ui/Checkbox";
import Tip from "../../../components/ui/Tip";
import tokiponaSVG from "../assets/toki_pona.svg";
type Props = {
locale: Language;
......@@ -35,7 +36,11 @@ function Entry({ entry: [x, lang], locale }: { entry: Key } & Props) {
}
}}>
<div className={styles.flag}>
<Emoji size={42} emoji={lang.emoji} />
{lang.emoji === "🙂" ? (
<img src={tokiponaSVG} width={42} />
) : (
<Emoji size={42} emoji={lang.emoji} />
)}
</div>
<span className={styles.description}>{lang.display}</span>
</Checkbox>
......@@ -55,7 +60,17 @@ export function Component(props: Props) {
</h3>
<div className={styles.list}>
{languages
.filter(([, lang]) => !lang.alt)
.filter(([, lang]) => !lang.cat)
.map(([x, lang]) => (
<Entry key={x} entry={[x, lang]} {...props} />
))}
</div>
<h3>
<Text id="app.settings.pages.language.const" />
</h3>
<div className={styles.list}>
{languages
.filter(([, lang]) => lang.cat === "const")
.map(([x, lang]) => (
<Entry key={x} entry={[x, lang]} {...props} />
))}
......@@ -65,7 +80,7 @@ export function Component(props: Props) {
</h3>
<div className={styles.list}>
{languages
.filter(([, lang]) => lang.alt)
.filter(([, lang]) => lang.cat === "alt")
.map(([x, lang]) => (
<Entry key={x} entry={[x, lang]} {...props} />
))}
......
import { useEffect, useState } from "preact/hooks";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
export function Native() {
const [config, setConfig] = useState(window.native.getConfig());
const [autoStart, setAutoStart] = useState<boolean | undefined>();
const fetchValue = () => window.native.getAutoStart().then(setAutoStart);
const [hintReload, setHintReload] = useState(false);
const [hintRelaunch, setHintRelaunch] = useState(false);
const [confirmDev, setConfirmDev] = useState(false);
useEffect(() => {
fetchValue();
}, []);
return (
<div>
<h3>App Behavior</h3>
<h5>Some options might require a restart.</h5>
<Checkbox
checked={autoStart ?? false}
disabled={typeof autoStart === "undefined"}
onChange={async (v) => {
if (v) {
await window.native.enableAutoStart();
} else {
await window.native.disableAutoStart();
}
setAutoStart(v);
}}
description="Launch Revolt when you log into your computer.">
Start with computer
</Checkbox>
<Checkbox
checked={config.discordRPC}
onChange={(discordRPC) => {
window.native.set("discordRPC", discordRPC);
setConfig({
...config,
discordRPC,
});
}}
description="Rep Revolt on your Discord status.">
Enable Discord status
</Checkbox>
<Checkbox
checked={config.build === "nightly"}
onChange={(nightly) => {
const build = nightly ? "nightly" : "stable";
window.native.set("build", build);
setHintReload(true);
setConfig({
...config,
build,
});
}}
description="Use the beta branch of Revolt.">
Revolt Nightly
</Checkbox>
<h3>Titlebar</h3>
<Checkbox
checked={!config.frame}
onChange={(frame) => {
window.native.set("frame", !frame);
setHintRelaunch(true);
setConfig({
...config,
frame: !frame,
});
}}
description={<>Let Revolt use its own window frame.</>}>
Custom window frame
</Checkbox>
<Checkbox //FIXME: In Titlebar.tsx, enable .quick css
disabled={true}
checked={!config.frame}
onChange={(frame) => {
window.native.set("frame", !frame);
setHintRelaunch(true);
setConfig({
...config,
frame: !frame,
});
}}
description="Show mute/deafen buttons on the titlebar.">
Enable quick action buttons
</Checkbox>
<h3>Advanced</h3>
<Checkbox
checked={config.hardwareAcceleration}
onChange={async (hardwareAcceleration) => {
window.native.set(
"hardwareAcceleration",
hardwareAcceleration,
);
setHintRelaunch(true);
setConfig({
...config,
hardwareAcceleration,
});
}}
description="Uses your GPU to render the app, disable if you run into visual issues.">
Hardware Acceleration
</Checkbox>
<p style={{ display: "flex", gap: "8px" }}>
<Button
contrast
compact
disabled={!hintReload}
onClick={window.native.reload}>
Reload Page
</Button>
<Button
contrast
compact
disabled={!hintRelaunch}
onClick={window.native.relaunch}>
Reload App
</Button>
</p>
<h3 style={{ marginTop: "4em" }}>Local Development Mode</h3>
{config.build === "dev" ? (
<>
<h5>Development mode is currently on.</h5>
<Button
contrast
compact
onClick={() => {
window.native.set("build", "stable");
window.native.reload();
}}>
Exit Development Mode
</Button>
</>
) : (
<>
<Checkbox
checked={confirmDev}
onChange={setConfirmDev}
description={
<>
This will change the app to the 'dev' branch,
instead loading the app from a local server on
your machine.
<br />
<b>
Without a server running,{" "}
<span style={{ color: "var(--error)" }}>
the app will not load!
</span>
</b>
</>
}>
I understand there's no going back.
</Checkbox>
<p>
<Button
error
compact
disabled={!confirmDev}
onClick={() => {
window.native.set("build", "dev");
window.native.reload();
}}>
Enter Development Mode
</Button>
</p>
</>
)}
</div>
);
}
......@@ -127,6 +127,7 @@ export function Component({ options }: Props) {
</h3>
{SOUNDS_ARRAY.map((key) => (
<Checkbox
key={key}
checked={!!enabledSounds[key]}
onChange={(enabled) =>
dispatch({
......
.user {
.banner {
gap: 24px;
position: relative;
margin-top: 8px;
margin-bottom: 15px;
gap: 16px;
width: 100%;
padding: 1em;
padding: 12px 10px;
display: flex;
overflow: hidden;
align-items: center;
background: var(--secondary-header);
border-radius: var(--border-radius);
.container {
display: flex;
gap: 24px;
align-items: center;
flex-direction: row;
width: 100%;
}
.userDetail {
display: flex;
flex-grow: 1;
gap: 2px;
flex-direction: column;
font-size: 1.5rem;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.avatar {
cursor: pointer;
transition: 0.2s ease filter;
......@@ -18,13 +40,8 @@
}
}
.username {
font-size: 1.5rem;
font-weight: 600;
}
.userid {
font-size: .875rem;
font-size: 12px;
font-weight: 600;
display: flex;
align-items: center;
......@@ -40,57 +57,28 @@
.details {
display: flex;
margin-top: 1em;
padding: 1em 0;
gap: 10px;
flex-direction: column;
/*border-top: 1px solid var(--secondary-header);
border-width: 100%;*/
> div {
gap: 12px;
padding: 4px;
/*padding: 4px;*/
padding: 8px 12px;
display: flex;
align-items: center;
flex-direction: row;
margin-bottom: 5px;
background: var(--secondary-header);
border-radius: 6px;
> svg {
flex-shrink: 0;
}
}
.detail {
flex-grow: 1;
min-width: 0;
display: flex;
flex-direction: column;
.subtext {
display: inline;
font-size: .875rem;
font-weight: 600;
color: var(--foreground);
text-transform: uppercase;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
a {
font-size: .875rem;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
p {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
p {
margin: 0;
font-size: 1rem;
......@@ -103,7 +91,7 @@
display: grid;
place-items: center;
grid-template-columns: minmax(auto, 100%);
> div {
width: 100%;
max-width: 560px;
......@@ -131,6 +119,20 @@
}
}
@media only screen and (max-width: 800px) {
.user {
.banner {
gap: 18px;
padding: 0;
flex-direction: column;
> button {
width: 100%;
}
}
}
}
.appearance {
.theme {
min-width: 0;
......@@ -141,12 +143,14 @@
.themes {
gap: 8px;
display: flex;
width: 100%;
img {
cursor: pointer;
border-radius: var(--border-radius);
transition: border 0.3s;
border: 3px solid transparent;
width: 100%;
&[data-active="true"] {
cursor: default;
......@@ -163,14 +167,13 @@
}
details {
summary {
font-size: .8125rem;
font-size: 0.8125rem;
font-weight: 700;
text-transform: uppercase;
color: var(--secondary-foreground);
cursor: pointer;
}
}
}
.emojiPack {
......@@ -224,15 +227,12 @@
text-transform: unset;
a {
opacity: 0.7;
color: var(--accent);
font-weight: 600;
&:hover {
text-decoration: underline;
}
}
@media only screen and (max-width: 800px) {
......@@ -258,7 +258,7 @@
cursor: pointer;
display: flex;
align-items: center;
font-size: .875rem;
font-size: 0.875rem;
min-width: 0;
flex-grow: 1;
padding: 8px;
......@@ -279,7 +279,7 @@
row-gap: 8px;
display: grid;
column-gap: 16px;
grid-template-columns: repeat(auto-fill,minmax(200px,1fr));
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
margin-bottom: 20px;
.entry {
......@@ -292,7 +292,7 @@
flex: 1;
display: block;
font-weight: 600;
font-size: .875rem;
font-size: 0.875rem;
margin-bottom: 8px;
text-transform: capitalize;
......@@ -354,9 +354,9 @@
display: flex;
gap: 12px;
flex-grow: 1;
svg {
vertical-align: auto;
margin-top: 1px;
}
}
}
......@@ -379,7 +379,6 @@
border-bottom: 2px solid var(--primary-background);
}
}
}
&[data-deleting="true"] {
......@@ -414,7 +413,7 @@
.label {
margin-bottom: 8px;
color: var(--primary-text);
font-size: .75rem;
font-size: 0.75rem;
font-weight: 600;
}
......@@ -434,7 +433,7 @@
}
.time {
font-size: .75rem;
font-size: 0.75rem;
color: var(--teriary-text);
text-overflow: ellipsis;
overflow: hidden;
......@@ -451,7 +450,7 @@
align-items: unset;
flex-direction: column;
gap: 20px;
> button {
width: 100%;
}
......@@ -465,28 +464,42 @@
.languages {
.list {
display: flex;
flex-direction: column;
margin-bottom: 1em;
gap: 8px;
.entry {
height: 50px;
display: flex;
height: 45px;
padding: 0 8px;
background: var(--secondary-header);
border-radius: var(--border-radius);
margin-top: 0;
&:hover {
background: var(--secondary-background);
}
}
.entry > span > span {
gap: 20px;
gap: 12px;
display: flex;
align-items: center;
flex-direction: row;
.flag {
display: flex;
font-size: 2.625rem;
line-height: 48px;
> div {
display: flex;
align-items: center;
justify-content: center;
}
> img {
height: 32px !important;
}
}
.description {
......@@ -510,4 +523,8 @@
justify-content: center;
align-items: center;
}
}
\ No newline at end of file
}
section {
margin-bottom: 20px;
}
import { Users } from "revolt.js/dist/api/objects";
import { Profile as ProfileI } from "revolt-api/types/Users";
import styles from "./Panes.module.scss";
import { IntlContext, Text, translate } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { Text } from "preact-i18n";
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { useTranslation } from "../../../lib/i18n";
import { UserProfile } from "../../../context/intermediate/popovers/UserProfile";
import { FileUploader } from "../../../context/revoltjs/FileUploads";
......@@ -20,28 +21,25 @@ import AutoComplete, {
import Button from "../../../components/ui/Button";
export function Profile() {
const { intl } = useContext(IntlContext);
const status = useContext(StatusContext);
const translate = useTranslation();
const client = useClient();
const [profile, setProfile] = useState<undefined | Users.Profile>(
undefined,
);
const [profile, setProfile] = useState<undefined | ProfileI>(undefined);
// ! FIXME: temporary solution
// ! we should just announce profile changes through WS
function refreshProfile() {
client.users
.fetchProfile(client.user!._id)
const refreshProfile = useCallback(() => {
client
.user!.fetchProfile()
.then((profile) => setProfile(profile ?? {}));
}
}, [client.user, setProfile]);
useEffect(() => {
if (profile === undefined && status === ClientStatus.ONLINE) {
refreshProfile();
}
}, [status]);
}, [profile, status, refreshProfile]);
const [changed, setChanged] = useState(false);
function setContent(content?: string) {
......@@ -70,7 +68,6 @@ export function Profile() {
user_id={client.user!._id}
dummy={true}
dummyProfile={profile}
onClose={() => {}}
/>
</div>
<div className={styles.row}>
......@@ -85,20 +82,15 @@ export function Profile() {
fileType="avatars"
behaviour="upload"
maxFileSize={4_000_000}
onUpload={(avatar) => client.users.editUser({ avatar })}
remove={() =>
client.users.editUser({ remove: "Avatar" })
}
defaultPreview={client.users.getAvatarURL(
client.user!._id,
onUpload={(avatar) => client.users.edit({ avatar })}
remove={() => client.users.edit({ remove: "Avatar" })}
defaultPreview={client.user!.generateAvatarURL(
{ max_side: 256 },
true,
)}
previewURL={client.users.getAvatarURL(
client.user!._id,
previewURL={client.user!.generateAvatarURL(
{ max_side: 256 },
true,
true,
)}
/>
</div>
......@@ -113,21 +105,21 @@ export function Profile() {
fileType="backgrounds"
maxFileSize={6_000_000}
onUpload={async (background) => {
await client.users.editUser({
await client.users.edit({
profile: { background },
});
refreshProfile();
}}
remove={async () => {
await client.users.editUser({
await client.users.edit({
remove: "ProfileBackground",
});
setProfile({ ...profile, background: undefined });
}}
previewURL={
profile?.background
? client.users.getBackgroundURL(
profile,
? client.generateFileURL(
profile.background,
{ width: 1000 },
true,
)
......@@ -156,8 +148,6 @@ export function Profile() {
? "fetching"
: "placeholder"
}`,
"",
(intl as any).dictionary as Record<string, unknown>,
)}
onKeyUp={onKeyUp}
onKeyDown={onKeyDown}
......@@ -169,7 +159,7 @@ export function Profile() {
contrast
onClick={() => {
setChanged(false);
client.users.editUser({
client.users.edit({
profile: { content: profile?.content },
});
}}
......
import { Chrome, Android, Apple, Windows } from "@styled-icons/boxicons-logos";
import { HelpCircle } from "@styled-icons/boxicons-regular";
import { HelpCircle, Desktop } from "@styled-icons/boxicons-regular";
import {
Safari,
Firefoxbrowser,
......@@ -50,7 +50,7 @@ export function Sessions() {
);
setSessions(data);
});
}, []);
}, [client, setSessions, deviceId]);
if (typeof sessions === "undefined") {
return (
......@@ -73,6 +73,8 @@ export function Sessions() {
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 size={32} />;
}
......@@ -121,6 +123,7 @@ export function Sessions() {
const systemIcon = getSystemIcon(session);
return (
<div
key={session.id}
className={styles.entry}
data-active={session.id === deviceId}
data-deleting={
......
......@@ -26,6 +26,7 @@ export function Component(props: Props) {
] as [SyncKeys, string][]
).map(([key, title]) => (
<Checkbox
key={key}
checked={
(props.options?.disabled ?? []).indexOf(key) === -1
}
......
import { XCircle } from "@styled-icons/boxicons-regular";
import { Servers, Users } from "revolt.js/dist/api/objects";
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 { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
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);
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(setData as any);
}, []);
server.fetchBans().then(setData);
}, [server, setData]);
return (
<div className={styles.userList}>
......@@ -42,10 +40,11 @@ export function Bans({ server }: Props) {
</div>
{typeof data === "undefined" && <Preloader type="ring" />}
{data?.bans.map((x) => {
let user = data.users.find((y) => y._id === x._id.user);
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>
......@@ -61,10 +60,7 @@ export function Bans({ server }: Props) {
onClick={async () => {
setDelete([...deleting, x._id.user]);
await client.servers.unbanUser(
server._id,
x._id.user,
);
await server.unbanUser(x._id.user);
setData({
...data,
......@@ -81,4 +77,4 @@ export function Bans({ server }: Props) {
})}
</div>
);
}
});
import { XCircle } from "@styled-icons/boxicons-regular";
import isEqual from "lodash.isequal";
import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
import { Route } from "revolt.js/dist/api/routes";
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 styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { useChannels } from "../../../context/revoltjs/hooks";
import { useState } from "preact/hooks";
import ChannelIcon from "../../../components/common/ChannelIcon";
import UserIcon from "../../../components/common/user/UserIcon";
import Button from "../../../components/ui/Button";
import ComboBox from "../../../components/ui/ComboBox";
import IconButton from "../../../components/ui/IconButton";
import InputBox from "../../../components/ui/InputBox";
import Preloader from "../../../components/ui/Preloader";
import Tip from "../../../components/ui/Tip";
interface Props {
server: Servers.Server;
server: Server;
}
// ! FIXME: really bad code
export function Categories({ server }: Props) {
const client = useContext(AppContext);
const channels = useChannels(server.channels) as (
| Channels.TextChannel
| Channels.VoiceChannel
)[];
const [cats, setCats] = useState<Servers.Category[]>(
server.categories ?? [],
);
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 (
......@@ -45,9 +30,7 @@ export function Categories({ server }: Props) {
<Button
contrast
disabled={isEqual(server.categories ?? [], cats)}
onClick={() =>
client.servers.edit(server._id, { categories: cats })
}>
onClick={() => server.edit({ categories: cats })}>
save categories
</Button>
</p>
......@@ -106,6 +89,7 @@ export function Categories({ server }: Props) {
{channels.map((channel) => {
return (
<div
key={channel!._id}
style={{
display: "flex",
gap: "12px",
......@@ -113,13 +97,13 @@ export function Categories({ server }: Props) {
}}>
<div style={{ flexShrink: 0 }}>
<ChannelIcon target={channel} size={24} />{" "}
<span>{channel.name}</span>
<span>{channel!.name}</span>
</div>
<ComboBox
style={{ flexGrow: 1 }}
value={
cats.find((x) =>
x.channels.includes(channel._id),
x.channels.includes(channel!._id),
)?.id ?? "none"
}
onChange={(e) =>
......@@ -129,11 +113,11 @@ export function Categories({ server }: Props) {
...x,
channels: [
...x.channels.filter(
(y) => y !== channel._id,
(y) => y !== channel!._id,
),
...(e.currentTarget.value ===
x.id
? [channel._id]
? [channel!._id]
: []),
],
};
......@@ -142,7 +126,9 @@ export function Categories({ server }: Props) {
}>
<option value="none">Uncategorised</option>
{cats.map((x) => (
<option value={x.id}>{x.title}</option>
<option key={x.id} value={x.id}>
{x.title}
</option>
))}
</ComboBox>
</div>
......@@ -150,4 +136,4 @@ export function Categories({ server }: Props) {
})}
</div>
);
}
});
import { XCircle } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { Invites as InvitesNS, Servers } from "revolt.js/dist/api/objects";
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 { useData } from "../../../mobx/State";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import {
useChannels,
useForceUpdate,
useUsers,
} from "../../../context/revoltjs/hooks";
import { getChannelName } from "../../../context/revoltjs/util";
import UserIcon from "../../../components/common/user/UserIcon";
......@@ -21,27 +15,24 @@ import IconButton from "../../../components/ui/IconButton";
import Preloader from "../../../components/ui/Preloader";
interface Props {
server: Servers.Server;
server: Server;
}
export const Invites = observer(({ server }: Props) => {
const [deleting, setDelete] = useState<string[]>([]);
const [invites, setInvites] = useState<
InvitesNS.ServerInvite[] | undefined
>(undefined);
const ctx = useForceUpdate();
const channels = useChannels(invites?.map((x) => x.channel) ?? [], ctx);
const [invites, setInvites] = useState<ServerInvite[] | undefined>(
undefined,
);
const store = useData();
const client = useClient();
const users = invites?.map((invite) => store.users.get(invite.creator));
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.userList}>
......@@ -62,10 +53,11 @@ export const Invites = observer(({ server }: Props) => {
{typeof invites === "undefined" && <Preloader type="ring" />}
{invites?.map((invite, index) => {
const creator = users![index];
const channel = channels.find((x) => x?._id === invite.channel);
const channel = channels![index];
return (
<div
key={invite._id}
className={styles.invite}
data-deleting={deleting.indexOf(invite._id) > -1}>
<code>{invite._id}</code>
......@@ -77,14 +69,14 @@ export const Invites = observer(({ server }: Props) => {
</span>
<span>
{channel && creator
? getChannelName(ctx.client, channel, true)
? getChannelName(channel, true)
: "#??"}
</span>
<IconButton
onClick={async () => {
setDelete([...deleting, invite._id]);
await ctx.client.deleteInvite(invite._id);
await client.deleteInvite(invite._id);
setInvites(
invites?.filter(
......
import { ChevronDown } from "@styled-icons/boxicons-regular";
import { isEqual } from "lodash";
import { observer } from "mobx-react-lite";
import { Servers } from "revolt.js/dist/api/objects";
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 { useData } from "../../../mobx/State";
import { useClient } from "../../../context/revoltjs/RevoltClient";
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";
......@@ -19,52 +16,49 @@ import IconButton from "../../../components/ui/IconButton";
import Overline from "../../../components/ui/Overline";
interface Props {
server: Servers.Server;
server: Server;
}
export const Members = observer(({ server }: Props) => {
const [selected, setSelected] = useState<undefined | string>();
const [members, setMembers] = useState<Servers.Member[] | undefined>(
undefined,
);
const store = useData();
const client = useClient();
const users = members?.map((member) => store.users.get(member._id.user));
const [data, setData] = useState<
{ members: Member[]; users: User[] } | undefined
>(undefined);
useEffect(() => {
client.members
.fetchMembers(server._id)
.then((members) => setMembers(members));
}, []);
server.fetchMembers().then(setData);
}, [server, setData]);
const [roles, setRoles] = useState<string[]>([]);
useEffect(() => {
if (selected) {
setRoles(
members!.find((x) => x._id.user === selected)?.roles ?? [],
data!.members.find((x) => x._id.user === selected)?.roles ?? [],
);
}
}, [selected]);
}, [setRoles, selected, data]);
return (
<div className={styles.userList}>
<div className={styles.subtitle}>
{members?.length ?? 0} Members
{data?.members.length ?? 0} Members
</div>
{members &&
members.length > 0 &&
members
.map((member, index) => {
{data &&
data.members.length > 0 &&
data.members
.map((member) => {
return {
member,
user: users![index],
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
key={member._id.user}
className={styles.member}
data-open={selected === member._id.user}
onClick={() =>
......@@ -86,14 +80,15 @@ export const Members = observer(({ server }: Props) => {
</div>
{selected === member._id.user && (
<div
key={"drop_" + member._id.user}
key={`drop_${member._id.user}`}
className={styles.memberView}>
<Overline type="subtle">Roles</Overline>
{Object.keys(server.roles ?? {}).map(
(key) => {
let role = server.roles![key];
const role = server.roles![key];
return (
<Checkbox
key={key}
checked={
roles.includes(key) ??
false
......@@ -130,32 +125,16 @@ export const Members = observer(({ server }: Props) => {
member.roles ?? [],
roles,
)}
onClick={async () => {
await client.members.editMember(
server._id,
member._id.user,
{
roles,
},
);
setMembers(
members.map((x) =>
x._id.user ===
member._id.user
? {
...x,
roles,
}
: x,
),
);
}}>
onClick={() =>
member.edit({
roles,
})
}>
<Text id="app.special.modals.actions.save" />
</Button>
</div>
)}
</>
</Fragment>
))}
</div>
);
......
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 { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { FileUploader } from "../../../context/revoltjs/FileUploads";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../../context/revoltjs/util";
import Button from "../../../components/ui/Button";
......@@ -16,12 +16,10 @@ 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(
......@@ -40,16 +38,14 @@ export function Overview({ server }: Props) {
const [changed, setChanged] = useState(false);
function save() {
const 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;
changes.system_messages = systemMessages ?? undefined;
client.servers.edit(server._id, changes);
server.edit(changes);
setChanged(false);
}
......@@ -63,17 +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>
......@@ -115,17 +103,9 @@ export function Overview({ server }: Props) {
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" })
}
onUpload={(banner) => server.edit({ banner })}
previewURL={server.generateBannerURL({ width: 1000 }, true)}
remove={() => server.edit({ remove: "Banner" })}
/>
<h3>
......@@ -139,6 +119,7 @@ export function Overview({ server }: Props) {
].map(([i18n, key]) => (
// ! FIXME: temporary code just so we can expose the options
<p
key={key}
style={{
display: "flex",
gap: "8px",
......@@ -170,15 +151,13 @@ export function Overview({ server }: Props) {
<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)}
{server.channels
.filter((x) => typeof x !== "undefined")
.map((channel) => (
<option key={channel!._id} value={channel!._id}>
{getChannelName(channel!, true)}
</option>
);
})}
))}
</ComboBox>
</p>
))}
......@@ -190,4 +169,4 @@ export function Overview({ server }: Props) {
</p>
</div>
);
}
});
import { Plus } from "@styled-icons/boxicons-regular";
import isEqual from "lodash.isequal";
import { Servers } from "revolt.js/dist/api/objects";
import {
ChannelPermission,
ServerPermission,
} from "revolt.js/dist/api/permissions";
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 { useContext, useEffect, useState } from "preact/hooks";
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
import ColourSwatches from "../../../components/ui/ColourSwatches";
import IconButton from "../../../components/ui/IconButton";
import InputBox from "../../../components/ui/InputBox";
import Overline from "../../../components/ui/Overline";
import Tip from "../../../components/ui/Tip";
import ButtonItem from "../../../components/navigation/items/ButtonItem";
interface Props {
server: Servers.Server;
server: Server;
}
const I32ToU32 = (arr: number[]) => arr.map((x) => x >>> 0);
// ! FIXME: bad code :)
export function Roles({ server }: Props) {
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"));
useEffect(() => setRole("default"), [role]);
return null;
}
function getPermissions(id: string) {
return I32ToU32(
id === "default"
? server.default_permissions
: roles[id].permissions,
);
}
const { name: roleName, colour: roleColour } = roles[role] ?? {};
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);
......@@ -57,7 +58,7 @@ export function Roles({ server }: Props) {
useEffect(
() => setPerm(getPermissions(role)),
[role, roles[role]?.permissions],
[getPermissions, role, permissions],
);
useEffect(() => setName(roleName), [role, roleName]);
......@@ -70,20 +71,20 @@ export function Roles({ server }: Props) {
const save = () => {
if (!isEqual(perm, getPermissions(role))) {
client.servers.setPermissions(server._id, role, {
server.setPermissions(role, {
server: perm[0],
channel: perm[1],
});
}
if (!isEqual(name, roleName) || !isEqual(colour, roleColour)) {
client.servers.editRole(server._id, role, { name, colour });
server.editRole(role, { name, colour });
}
};
const deleteRole = () => {
setRole("default");
client.servers.deleteRole(server._id, role);
server.deleteRole(role);
};
return (
......@@ -99,7 +100,7 @@ export function Roles({ server }: Props) {
openScreen({
id: "special_input",
type: "create_role",
server: server._id,
server,
callback: (id) => setRole(id),
})
}
......@@ -117,6 +118,7 @@ export function Roles({ server }: Props) {
}
return (
<ButtonItem
key={id}
active={role === id}
onClick={() => setRole(id)}
style={{ color: roles[id].colour }}>
......@@ -176,6 +178,7 @@ export function Roles({ server }: Props) {
return (
<Checkbox
key={key}
checked={(perm[0] & value) > 0}
onChange={() =>
setPerm([perm[0] ^ value, perm[1]])
......@@ -201,6 +204,7 @@ export function Roles({ server }: Props) {
return (
<Checkbox
key={key}
checked={((perm[1] >>> 0) & value) > 0}
onChange={() =>
setPerm([perm[0], perm[1] ^ value])
......@@ -227,4 +231,4 @@ export function Roles({ server }: Props) {
</div>
</div>
);
}
});