581 lines
15 KiB
TypeScript
581 lines
15 KiB
TypeScript
import { For, Match, Show, Switch, createSignal } from "solid-js";
|
|
|
|
import { useLingui } from "@lingui-solid/solid/macro";
|
|
import {
|
|
API,
|
|
Channel,
|
|
DEFAULT_PERMISSION_DIRECT_MESSAGE,
|
|
Server,
|
|
} from "stoat.js";
|
|
import { css } from "styled-system/css";
|
|
import { styled } from "styled-system/jsx";
|
|
|
|
import { Button, Checkbox2, OverrideSwitch, Row, Text } from "@revolt/ui";
|
|
|
|
type Props =
|
|
| { type: "server_default"; context: Server }
|
|
| { type: "server_role"; context: Server; roleId: string }
|
|
| { type: "channel_default"; context: Channel }
|
|
| { type: "channel_role"; context: Channel; roleId: string }
|
|
| { type: "group"; context: Channel };
|
|
|
|
type Context = API.Channel["channel_type"] | "Server";
|
|
|
|
/**
|
|
* Generic editor for any channel permissions
|
|
*/
|
|
export function ChannelPermissionsEditor(props: Props) {
|
|
const { t } = useLingui();
|
|
|
|
const context: Context =
|
|
// eslint-disable-next-line solid/reactivity
|
|
props.context instanceof Server ? "Server" : props.context.type;
|
|
|
|
/**
|
|
* Current permission value, normalised to [allow, deny]
|
|
* @returns [allow, deny] BigInts
|
|
*/
|
|
function currentValue() {
|
|
switch (props.type) {
|
|
case "server_default":
|
|
return [BigInt(props.context.defaultPermissions), BigInt(0)];
|
|
case "server_role":
|
|
return [
|
|
BigInt(props.context.roles?.get(props.roleId)?.permissions.a || 0),
|
|
BigInt(props.context.roles?.get(props.roleId)?.permissions.d || 0),
|
|
];
|
|
case "channel_default":
|
|
return [
|
|
BigInt(props.context.defaultPermissions?.a || 0),
|
|
BigInt(props.context.defaultPermissions?.d || 0),
|
|
];
|
|
case "channel_role":
|
|
return [
|
|
BigInt(props.context.rolePermissions?.[props.roleId]?.a || 0),
|
|
BigInt(props.context.rolePermissions?.[props.roleId]?.d || 0),
|
|
];
|
|
case "group":
|
|
return [
|
|
BigInt(
|
|
props.context.permissions ?? DEFAULT_PERMISSION_DIRECT_MESSAGE,
|
|
),
|
|
BigInt(0),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Current edited values
|
|
*/
|
|
const [value, setValue] = createSignal(currentValue());
|
|
|
|
/**
|
|
* Whether there is a pending save
|
|
*/
|
|
function unsavedChanges() {
|
|
const [a1, a2] = currentValue(),
|
|
[b1, b2] = value();
|
|
|
|
return a1 !== b1 || a2 !== b2;
|
|
}
|
|
|
|
/**
|
|
* Reset to the current value
|
|
*/
|
|
function reset() {
|
|
setValue(currentValue());
|
|
}
|
|
|
|
/**
|
|
* Commit changes
|
|
* @todo mutator
|
|
*/
|
|
function save() {
|
|
switch (props.type) {
|
|
case "server_default":
|
|
props.context.setPermissions(undefined, Number(value()[0]));
|
|
break;
|
|
case "server_role":
|
|
props.context.setPermissions(props.roleId, {
|
|
allow: Number(value()[0]),
|
|
deny: Number(value()[1]),
|
|
});
|
|
break;
|
|
case "channel_default":
|
|
props.context.setPermissions(undefined, {
|
|
allow: Number(value()[0]),
|
|
deny: Number(value()[1]),
|
|
});
|
|
break;
|
|
case "channel_role":
|
|
props.context.setPermissions(props.roleId, {
|
|
allow: Number(value()[0]),
|
|
deny: Number(value()[1]),
|
|
});
|
|
break;
|
|
case "group":
|
|
props.context.setPermissions(undefined, Number(value()[0]));
|
|
break;
|
|
}
|
|
}
|
|
|
|
const Permissions: {
|
|
heading?: string;
|
|
key: string;
|
|
value: bigint;
|
|
title: string;
|
|
description: Partial<Record<Context | "Any", string>>;
|
|
}[] = [
|
|
{
|
|
heading: t`Admin`,
|
|
key: "ManageChannel",
|
|
value: 1n ** 0n,
|
|
title: t`Manage Channel`,
|
|
description: {
|
|
Group: t`Edit group name and description`,
|
|
Any: t`Edit and delete channel`,
|
|
},
|
|
},
|
|
{
|
|
key: "ManageServer",
|
|
value: 2n ** 1n,
|
|
title: t`Manage Server`,
|
|
description: {
|
|
Server: t`Edit the server's information and settings`,
|
|
},
|
|
},
|
|
{
|
|
key: "ManagePermissions",
|
|
value: 2n ** 2n,
|
|
title: t`Manage Permissions`,
|
|
description: {
|
|
Group: t`Whether other users can edit these settings`,
|
|
TextChannel: t`Edit channel-specific role and default permissions`,
|
|
Server: t`Edit any permissions on the server`,
|
|
},
|
|
},
|
|
{
|
|
key: "ManageRole",
|
|
value: 2n ** 3n,
|
|
title: t`Manage Roles`,
|
|
description: {
|
|
Server: t`Create and edit server roles`,
|
|
},
|
|
},
|
|
{
|
|
key: "ManageCustomisation",
|
|
value: 2n ** 4n,
|
|
title: t`Manage Customisation`,
|
|
description: {
|
|
Server: t`Create server emoji`,
|
|
},
|
|
},
|
|
{
|
|
heading: t`Members`,
|
|
key: "KickMembers",
|
|
value: 2n ** 6n,
|
|
title: t`Kick Members`,
|
|
description: {
|
|
Server: t`Kick lower-ranking members from the server`,
|
|
},
|
|
},
|
|
{
|
|
key: "BanMembers",
|
|
value: 2n ** 7n,
|
|
title: t`Ban Members`,
|
|
description: {
|
|
Server: t`Ban lower-ranking members from the server`,
|
|
},
|
|
},
|
|
{
|
|
key: "TimeoutMembers",
|
|
value: 2n ** 8n,
|
|
title: t`Timeout Members`,
|
|
description: {
|
|
Server: t`Temporarily prevent lower-ranking members from interacting`,
|
|
},
|
|
},
|
|
{
|
|
key: "AssignRoles",
|
|
value: 2n ** 9n,
|
|
title: t`Assign Roles`,
|
|
description: {
|
|
Server: t`Assign lower-ranked roles to lower-ranking members`,
|
|
},
|
|
},
|
|
{
|
|
key: "ChangeNickname",
|
|
value: 2n ** 10n,
|
|
title: t`Change Nickname`,
|
|
description: {
|
|
Server: t`Change own nickname`,
|
|
},
|
|
},
|
|
{
|
|
key: "ManageNicknames",
|
|
value: 2n ** 11n,
|
|
title: t`Manage Nicknames`,
|
|
description: {
|
|
Server: t`Change other members' nicknames`,
|
|
},
|
|
},
|
|
{
|
|
key: "ChangeAvavar",
|
|
value: 2n ** 12n,
|
|
title: t`Change Avatar`,
|
|
description: {
|
|
Server: t`Change own avatar`,
|
|
},
|
|
},
|
|
{
|
|
key: "RemoveAvatars",
|
|
value: 2n ** 13n,
|
|
title: t`Remove Avatars`,
|
|
description: {
|
|
Server: t`Remove other members' avatars`,
|
|
},
|
|
},
|
|
{
|
|
heading: t`Channels`,
|
|
key: "ViewChannel",
|
|
value: 2n ** 20n,
|
|
title: t`View Channel`,
|
|
description: {
|
|
TextChannel: t`Able to access this channel`,
|
|
Server: t`Able to access channels on this server`,
|
|
},
|
|
},
|
|
{
|
|
key: "ReadMessageHistory",
|
|
value: 2n ** 21n,
|
|
title: t`Read Message History`,
|
|
description: {
|
|
TextChannel: t`Read past messages sent in channel`,
|
|
Server: t`Read past messages sent in channels`,
|
|
},
|
|
},
|
|
{
|
|
key: "SendMessage",
|
|
value: 2n ** 22n,
|
|
title: t`Send Messages`,
|
|
description: {
|
|
Group: t`Send messages in channel`,
|
|
TextChannel: t`Send messages in channel`,
|
|
Server: t`Send messages in channels`,
|
|
},
|
|
},
|
|
{
|
|
key: "ManageMessages",
|
|
value: 2n ** 23n,
|
|
title: t`Manage Messages`,
|
|
description: {
|
|
Group: t`Delete and pin messages sent by other members`,
|
|
TextChannel: t`Delete and pin messages sent by other members`,
|
|
Server: t`Delete and pin messages sent by other members`,
|
|
},
|
|
},
|
|
{
|
|
key: "ManageWebhooks",
|
|
value: 2n ** 24n,
|
|
title: t`Manage Webhooks`,
|
|
description: {
|
|
Group: t`Create and edit webhooks`,
|
|
TextChannel: t`Create and edit webhooks`,
|
|
Server: t`Create and edit webhooks`,
|
|
},
|
|
},
|
|
{
|
|
key: "InviteOthers",
|
|
value: 2n ** 25n,
|
|
title: t`Invite Others`,
|
|
description: {
|
|
Group: t`Add new members to the group`,
|
|
Any: t`Create invites for others to use`,
|
|
},
|
|
},
|
|
{
|
|
heading: t`Messaging`,
|
|
key: "SendEmbeds",
|
|
value: 2n ** 26n,
|
|
title: t`Send Embeds`,
|
|
description: {
|
|
Any: t`Send embedded content such as link embeds or custom embeds`,
|
|
},
|
|
},
|
|
{
|
|
key: "UploadFiles",
|
|
value: 2n ** 27n,
|
|
title: t`Upload Files`,
|
|
description: {
|
|
Any: t`Send attachments to chat`,
|
|
},
|
|
},
|
|
{
|
|
key: "Masquerade",
|
|
value: 2n ** 28n,
|
|
title: t`Masquerade`,
|
|
description: {
|
|
Any: t`Allow members to change name and avatar per-message`,
|
|
},
|
|
},
|
|
{
|
|
key: "React",
|
|
value: 2n ** 29n,
|
|
title: t`React`,
|
|
description: {
|
|
Any: t`React to messages with emoji`,
|
|
},
|
|
},
|
|
{
|
|
heading: t`Voice`,
|
|
key: "Connect",
|
|
value: 2n ** 30n,
|
|
title: t`Connect`,
|
|
description: {
|
|
TextChannel: t`Connect to voice channel`,
|
|
Server: t`Connect to voice channel`,
|
|
},
|
|
},
|
|
{
|
|
key: "Speak",
|
|
value: 2n ** 31n,
|
|
title: t`Speak`,
|
|
description: {
|
|
TextChannel: t`Able to speak in voice call`,
|
|
Server: t`Able to speak in voice call`,
|
|
},
|
|
},
|
|
{
|
|
key: "Video",
|
|
value: 2n ** 32n,
|
|
title: t`Video`,
|
|
description: {
|
|
TextChannel: t`Share camera or screen in voice call`,
|
|
Server: t`Share camera or screen in voice call`,
|
|
},
|
|
},
|
|
{
|
|
key: "MuteMembers",
|
|
value: 2n ** 33n,
|
|
title: t`Mute Members`,
|
|
description: {
|
|
TextChannel: t`Mute lower-ranking members in voice call`,
|
|
Server: t`Mute lower-ranking members in voice call`,
|
|
},
|
|
},
|
|
{
|
|
key: "DeafenMembers",
|
|
value: 2n ** 34n,
|
|
title: t`Deafen Members`,
|
|
description: {
|
|
TextChannel: t`Deafen lower-ranking members in voice call`,
|
|
Server: t`Deafen lower-ranking members in voice call`,
|
|
},
|
|
},
|
|
{
|
|
key: "MoveMembers",
|
|
value: 2n ** 35n,
|
|
title: t`Move Members`,
|
|
description: {
|
|
TextChannel: t`Move members between voice channels`,
|
|
Server: t`Move members between voice channels`,
|
|
},
|
|
},
|
|
{
|
|
key: "Listen",
|
|
value: 2n ** 36n,
|
|
title: t`Listen`,
|
|
description: {
|
|
TextChannel: t`Hear other people and see their video`,
|
|
Server: t`Hear other people and see their video`,
|
|
},
|
|
},
|
|
{
|
|
heading: t`Mentions`,
|
|
key: "MentionEveryone",
|
|
value: 2n ** 37n,
|
|
title: t`Mention Everyone`,
|
|
description: {
|
|
Any: t`Mention everyone and online members inside the server`,
|
|
},
|
|
},
|
|
{
|
|
key: "MentionRoles",
|
|
value: 2n ** 38n,
|
|
title: t`Mention Roles`,
|
|
description: {
|
|
Any: t`Mention specific roles`,
|
|
},
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Find description for this permission in context
|
|
* If null, don't show this permission entry
|
|
* @param entry Entry
|
|
* @returns Description or null
|
|
*/
|
|
function description(entry: (typeof Permissions)[number]) {
|
|
const desc = entry.description;
|
|
return desc[context] ?? desc.Any;
|
|
}
|
|
|
|
return (
|
|
<div class={css({ display: "flex", flexDirection: "column" })}>
|
|
<For each={Permissions}>
|
|
{(entry) => (
|
|
<Show when={description(entry)}>
|
|
<Show when={entry.heading}>
|
|
<span class={css({ marginTop: "var(--gap-md)" })}>
|
|
<Text class="label">{entry.heading}</Text>
|
|
</span>
|
|
</Show>
|
|
|
|
<Switch
|
|
fallback={
|
|
<ChannelPermissionToggle
|
|
key={entry.key}
|
|
title={entry.title}
|
|
description={description(entry) as string}
|
|
value={(value()[0] & entry.value) == entry.value}
|
|
onChange={() =>
|
|
setValue((v) => [v[0] ^ BigInt(entry.value), v[1]])
|
|
}
|
|
havePermission={
|
|
(props.context.permission & entry.value) === entry.value
|
|
}
|
|
/>
|
|
}
|
|
>
|
|
<Match when={props.type.startsWith("channel_")}>
|
|
<ChannelPermissionOverride
|
|
key={entry.key}
|
|
title={entry.title}
|
|
description={description(entry) as string}
|
|
value={
|
|
(value()[0] & entry.value) == entry.value
|
|
? "allow"
|
|
: (value()[1] & entry.value) == entry.value
|
|
? "deny"
|
|
: "neutral"
|
|
}
|
|
onChange={(target) => {
|
|
let allow = value()[0] & ~entry.value;
|
|
let deny = value()[1] & ~entry.value;
|
|
|
|
if (target === "allow") allow |= entry.value;
|
|
if (target === "deny") deny |= entry.value;
|
|
|
|
setValue([allow, deny]);
|
|
}}
|
|
havePermission={
|
|
(props.context.permission & entry.value) === entry.value
|
|
}
|
|
/>
|
|
</Match>
|
|
</Switch>
|
|
</Show>
|
|
)}
|
|
</For>
|
|
|
|
<StickyPanel>
|
|
<Row>
|
|
<Button
|
|
isDisabled={!unsavedChanges()}
|
|
variant="text"
|
|
size={unsavedChanges() ? "md" : "sm"}
|
|
onPress={reset}
|
|
>
|
|
Reset
|
|
</Button>
|
|
<Button
|
|
isDisabled={!unsavedChanges()}
|
|
size={unsavedChanges() ? "md" : "sm"}
|
|
onPress={save}
|
|
>
|
|
Save permissions
|
|
</Button>
|
|
</Row>
|
|
</StickyPanel>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const StickyPanel = styled("div", {
|
|
base: {
|
|
position: "sticky",
|
|
width: "fit-content",
|
|
padding: "var(--gap-md)",
|
|
bottom: "var(--gap-lg)",
|
|
borderRadius: "var(--borderRadius-xl)",
|
|
background: "var(--md-sys-color-surface)",
|
|
},
|
|
});
|
|
|
|
function ChannelPermissionToggle(props: {
|
|
key: string;
|
|
title: string;
|
|
description: string;
|
|
|
|
value: boolean;
|
|
onChange: (value: boolean) => void;
|
|
|
|
havePermission: boolean;
|
|
}) {
|
|
return (
|
|
<Checkbox2
|
|
name={props.key}
|
|
checked={props.value}
|
|
onChange={(event) => props.onChange(event.currentTarget.checked)}
|
|
disabled={!props.havePermission}
|
|
>
|
|
<div
|
|
class={css({
|
|
marginStart: "var(--gap-md)",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
})}
|
|
>
|
|
<Text size="large">{props.title}</Text>
|
|
<Text>{props.description}</Text>
|
|
</div>
|
|
</Checkbox2>
|
|
);
|
|
}
|
|
|
|
function ChannelPermissionOverride(props: {
|
|
key: string;
|
|
title: string;
|
|
description: string;
|
|
|
|
value: "allow" | "deny" | "neutral";
|
|
onChange: (value: "allow" | "deny" | "neutral") => void;
|
|
|
|
havePermission: boolean;
|
|
}) {
|
|
return (
|
|
<div
|
|
class={css({
|
|
gap: "var(--gap-md)",
|
|
display: "flex",
|
|
})}
|
|
>
|
|
<div
|
|
class={css({
|
|
flexGrow: 1,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
})}
|
|
>
|
|
<Text size="large">{props.title}</Text>
|
|
<Text>{props.description}</Text>
|
|
</div>
|
|
<OverrideSwitch
|
|
disabled={!props.havePermission}
|
|
value={props.value}
|
|
onChange={props.onChange}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|