mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-07 01:15:28 +00:00
feat(permission): implement new server / channel permission menus
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
79
src/components/settings/roles/OverrideSwitch.tsx
Normal file
79
src/components/settings/roles/OverrideSwitch.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
33
src/components/settings/roles/PermissionList.tsx
Normal file
33
src/components/settings/roles/PermissionList.tsx
Normal 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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
124
src/components/settings/roles/PermissionSelect.tsx
Normal file
124
src/components/settings/roles/PermissionSelect.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
35
src/components/settings/roles/RoleSelection.tsx
Normal file
35
src/components/settings/roles/RoleSelection.tsx
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
22
src/components/settings/roles/UnsavedChanges.tsx
Normal file
22
src/components/settings/roles/UnsavedChanges.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export interface CheckboxProps {
|
||||
disabled?: boolean;
|
||||
contrast?: boolean;
|
||||
className?: string;
|
||||
children: Children;
|
||||
children?: Children;
|
||||
description?: Children;
|
||||
onChange: (state: boolean) => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user