feat(permission): implement new server / channel permission menus

This commit is contained in:
Paul Makles
2022-02-27 23:44:29 +00:00
parent 3632b6b351
commit 041c039827
17 changed files with 587 additions and 311 deletions

View File

@@ -2,14 +2,12 @@ import { Check } from "@styled-icons/boxicons-regular";
import { Cog } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom";
import { ServerPermission } from "revolt.js/dist/api/permissions";
import { Permission } from "revolt.js/dist/api/permissions";
import { Server } from "revolt.js/dist/maps/Servers";
import styled, { css } from "styled-components/macro";
import { Text } from "preact-i18n";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import IconButton from "../ui/IconButton";
import Tooltip from "./Tooltip";
@@ -125,7 +123,7 @@ export default observer(({ server }: Props) => {
</Tooltip>
) : undefined}
<div className="title">{server.name}</div>
{(server.permission & ServerPermission.ManageServer) > 0 && (
{server.havePermission("ManageServer") && (
<Link to={`/server/${server._id}/settings`}>
<IconButton>
<Cog size={20} />

View File

@@ -1,7 +1,7 @@
import { Send, ShieldX, HappyBeaming, Box } from "@styled-icons/boxicons-solid";
import Axios, { CancelTokenSource } from "axios";
import { observer } from "mobx-react-lite";
import { ChannelPermission } from "revolt.js/dist/api/permissions";
import { Permission } from "revolt.js/dist/api/permissions";
import { Channel } from "revolt.js/dist/maps/Channels";
import styled, { css } from "styled-components/macro";
import { ulid } from "ulid";
@@ -125,6 +125,7 @@ const FileAction = styled.div`
display: flex;
align-items: center;
justify-content: center;
}
`;
// For sed replacement
@@ -147,7 +148,7 @@ export default observer(({ channel }: Props) => {
const renderer = getRenderer(channel);
if (!(channel.permission & ChannelPermission.SendMessage)) {
if (!(channel.permission & Permission.SendMessage)) {
return (
<Base>
<Blocked>
@@ -475,7 +476,7 @@ export default observer(({ channel }: Props) => {
setReplies={setReplies}
/>
<Base>
{channel.permission & ChannelPermission.UploadFiles ? (
{channel.permission & Permission.UploadFiles ? (
<FileAction>
<FileUploader
size={24}

View File

@@ -7,7 +7,7 @@ import {
Notification,
} from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { ChannelPermission } from "revolt.js";
import { Permission } from "revolt.js";
import { Message as MessageObject } from "revolt.js/dist/maps/Messages";
import styled from "styled-components";
@@ -131,8 +131,7 @@ export const MessageOverlayBar = observer(({ message, queued }: Props) => {
)}
{isAuthor ||
(message.channel &&
message.channel.permission &
ChannelPermission.ManageMessages) ? (
message.channel.permission & Permission.ManageMessages) ? (
<Tooltip content="Delete">
<Entry
onClick={(e) =>

View File

@@ -4,12 +4,14 @@ import { useContext } from "preact/hooks";
import {
ClientStatus,
StatusContext,
useClient,
} from "../../../context/revoltjs/RevoltClient";
import Banner from "../../ui/Banner";
export default function ConnectionStatus() {
const status = useContext(StatusContext);
const client = useClient();
if (status === ClientStatus.OFFLINE) {
return (
@@ -20,7 +22,8 @@ export default function ConnectionStatus() {
} else if (status === ClientStatus.DISCONNECTED) {
return (
<Banner>
<Text id="app.special.status.disconnected" />
<Text id="app.special.status.disconnected" /> <br />
<a onClick={() => client.websocket.connect()}>Reconnect</a>
</Banner>
);
} else if (status === ClientStatus.CONNECTING) {

View File

@@ -0,0 +1,79 @@
import { Check, Square, X } from "@styled-icons/boxicons-regular";
import styled, { css } from "styled-components";
type State = "Allow" | "Neutral" | "Deny";
const SwitchContainer = styled.div.attrs({
role: "radiogroup",
"aria-orientiation": "horizontal",
})`
flex-shrink: 0;
display: flex;
margin: 4px 16px;
overflow: hidden;
border-radius: var(--border-radius);
background: var(--secondary-background);
border: 2px solid var(--tertiary-background);
`;
const Switch = styled.div.attrs({
role: "radio",
})<{ state: State; selected: boolean }>`
padding: 4px;
cursor: pointer;
transition: 0.2s ease all;
color: ${(props) =>
props.state === "Allow"
? "var(--success)"
: props.state === "Deny"
? "var(--error)"
: "var(--tertiary-foreground)"};
${(props) =>
props.selected &&
css`
color: white;
background: ${props.state === "Allow"
? "var(--success)"
: props.state === "Deny"
? "var(--error)"
: "var(--primary-background)"};
`}
&:hover {
filter: brightness(0.8);
}
`;
interface Props {
state: State;
onChange: (state: State) => void;
}
export function OverrideSwitch({ state, onChange }: Props) {
return (
<SwitchContainer>
<Switch
onClick={() => onChange("Deny")}
state="Deny"
selected={state === "Deny"}>
<X size={24} />
</Switch>
<Switch
onClick={() => onChange("Neutral")}
state="Neutral"
selected={state === "Neutral"}>
<Square size={24} />
</Switch>
<Switch
onClick={() => onChange("Allow")}
state="Allow"
selected={state === "Allow"}>
<Check size={24} />
</Switch>
</SwitchContainer>
);
}

View File

@@ -0,0 +1,33 @@
import { OverrideField } from "revolt-api/types/_common";
import { Permission } from "revolt.js";
import { PermissionSelect } from "./PermissionSelect";
interface Props {
value: OverrideField | number;
onChange: (v: OverrideField | number) => void;
filter?: (keyof typeof Permission)[];
}
export function PermissionList({ value, onChange, filter }: Props) {
return (
<>
{(Object.keys(Permission) as (keyof typeof Permission)[])
.filter(
(key) =>
key !== "GrantAllSafe" &&
(!filter || filter.includes(key)),
)
.map((x) => (
<PermissionSelect
id={x}
key={x}
permission={Permission[x]}
value={value}
onChange={onChange}
/>
))}
</>
);
}

View File

@@ -0,0 +1,124 @@
import Long from "long";
import { OverrideField } from "revolt-api/types/_common";
import { Permission } from "revolt.js";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { useMemo } from "preact/hooks";
import Checkbox from "../../ui/Checkbox";
import { OverrideSwitch } from "./OverrideSwitch";
interface PermissionSelectProps {
id: keyof typeof Permission;
permission: number;
value: OverrideField | number;
onChange: (value: OverrideField | number) => void;
}
type State = "Allow" | "Neutral" | "Deny";
const PermissionEntry = styled.label`
width: 100%;
margin: 8px 0;
display: flex;
font-size: 1.1em;
align-items: center;
.title {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.description {
font-size: 0.8em;
color: var(--secondary-foreground);
}
`;
export function PermissionSelect({
id,
permission,
value,
onChange,
}: PermissionSelectProps) {
const state: State = useMemo(() => {
if (typeof value === "object") {
if (Long.fromNumber(value.d).and(permission).eq(permission)) {
return "Deny";
}
if (Long.fromNumber(value.a).and(permission).eq(permission)) {
return "Allow";
}
return "Neutral";
} else {
if (Long.fromNumber(value).and(permission).eq(permission)) {
return "Allow";
}
return "Neutral";
}
}, [value]);
function onSwitch(state: State) {
if (typeof value !== "object") throw "!";
// Convert to Long so we can do bitwise ops.
let allow = Long.fromNumber(value.a);
let deny = Long.fromNumber(value.d);
// Clear the current permission value.
if (allow.and(permission).eq(permission)) {
allow = allow.xor(permission);
}
if (deny.and(permission).eq(permission)) {
deny = deny.xor(permission);
}
// Apply the current permission state.
if (state === "Allow") {
allow = allow.or(permission);
}
if (state === "Deny") {
deny = deny.or(permission);
}
// Invoke state change.
onChange({
a: allow.toNumber(),
d: deny.toNumber(),
});
}
return (
<PermissionEntry>
<span class="title">
<Text id={`permissions.server.${id}.t`}>{id}</Text>
<span class="description">
<Text id={`permissions.server.${id}.d`} />
</span>
</span>
{typeof value === "object" ? (
<OverrideSwitch state={state} onChange={onSwitch} />
) : (
<Checkbox
checked={state === "Allow"}
onChange={() =>
onChange(
Long.fromNumber(value, false)
.xor(permission)
.toNumber(),
)
}
/>
)}
</PermissionEntry>
);
}

View File

@@ -0,0 +1,35 @@
import { Role } from "revolt-api/types/Servers";
import Checkbox from "../../ui/Checkbox";
export type RoleOrDefault = (
| Role
| {
name: string;
permissions: number;
colour?: string;
hoist?: boolean;
rank?: number;
}
) & { id: string };
interface Props {
selected: string;
onSelect: (id: string) => void;
roles: RoleOrDefault[];
}
export function RoleSelection({ selected, onSelect, roles }: Props) {
return (
<>
{roles.map((x) => (
<Checkbox
checked={x.id === selected}
onChange={() => onSelect(x.id)}>
{x.name}
</Checkbox>
))}
</>
);
}

View File

@@ -0,0 +1,22 @@
import Tip from "../../../components/ui/Tip";
import Button from "../../ui/Button";
interface Props {
save: () => void;
}
export function UnsavedChanges({ save }: Props) {
return (
<Tip hideSeparator>
<span
style={{
display: "flex",
alignItems: "center",
gap: "8px",
}}>
You have unsaved changes!
<Button onClick={save}>Save</Button>
</span>
</Tip>
);
}

View File

@@ -89,7 +89,7 @@ export interface CheckboxProps {
disabled?: boolean;
contrast?: boolean;
className?: string;
children: Children;
children?: Children;
description?: Children;
onChange: (state: boolean) => void;
}