stoat-for-desktop/components/app/interface/settings/channel/permissions/ChannelPermissionsEditor.tsx

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>
);
}