From 041c03982705570d1dfe35b2188c662597a82e5c Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 27 Feb 2022 23:44:29 +0000 Subject: [PATCH 01/60] feat(permission): implement new server / channel permission menus --- .env | 2 +- package.json | 1 + src/components/common/ServerHeader.tsx | 6 +- .../common/messaging/MessageBox.tsx | 7 +- .../messaging/bars/MessageOverlayBar.tsx | 5 +- .../navigation/items/ConnectionStatus.tsx | 5 +- .../settings/roles/OverrideSwitch.tsx | 79 ++++ .../settings/roles/PermissionList.tsx | 33 ++ .../settings/roles/PermissionSelect.tsx | 124 ++++++ .../settings/roles/RoleSelection.tsx | 35 ++ .../settings/roles/UnsavedChanges.tsx | 22 + src/components/ui/Checkbox.tsx | 2 +- src/lib/ContextMenus.tsx | 41 +- src/pages/invite/InviteBot.tsx | 5 +- src/pages/settings/channel/Permissions.tsx | 111 ++++- src/pages/settings/server/Roles.tsx | 415 +++++++----------- yarn.lock | 5 + 17 files changed, 587 insertions(+), 311 deletions(-) create mode 100644 src/components/settings/roles/OverrideSwitch.tsx create mode 100644 src/components/settings/roles/PermissionList.tsx create mode 100644 src/components/settings/roles/PermissionSelect.tsx create mode 100644 src/components/settings/roles/RoleSelection.tsx create mode 100644 src/components/settings/roles/UnsavedChanges.tsx diff --git a/.env b/.env index fd3eeb19..6fb48752 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -VITE_API_URL=https://api.revolt.chat +VITE_API_URL=http://local.revolt.chat:8000 VITE_THEMES_URL=https://themes.revolt.chat diff --git a/package.json b/package.json index f8ccd223..638dedd6 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,7 @@ "localforage": "^1.9.0", "lodash.defaultsdeep": "^4.6.1", "lodash.isequal": "^4.5.0", + "long": "^5.2.0", "markdown-it": "^12.0.6", "markdown-it-emoji": "^2.0.0", "markdown-it-sub": "^1.0.0", diff --git a/src/components/common/ServerHeader.tsx b/src/components/common/ServerHeader.tsx index 28638e38..5bdc8cd2 100644 --- a/src/components/common/ServerHeader.tsx +++ b/src/components/common/ServerHeader.tsx @@ -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) => { ) : undefined}
{server.name}
- {(server.permission & ServerPermission.ManageServer) > 0 && ( + {server.havePermission("ManageServer") && ( diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index 922b9e3c..cce10c94 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -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 ( @@ -475,7 +476,7 @@ export default observer(({ channel }: Props) => { setReplies={setReplies} /> - {channel.permission & ChannelPermission.UploadFiles ? ( + {channel.permission & Permission.UploadFiles ? ( { )} {isAuthor || (message.channel && - message.channel.permission & - ChannelPermission.ManageMessages) ? ( + message.channel.permission & Permission.ManageMessages) ? ( diff --git a/src/components/navigation/items/ConnectionStatus.tsx b/src/components/navigation/items/ConnectionStatus.tsx index 901ef62b..90b6db86 100644 --- a/src/components/navigation/items/ConnectionStatus.tsx +++ b/src/components/navigation/items/ConnectionStatus.tsx @@ -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 ( - +
+ client.websocket.connect()}>Reconnect
); } else if (status === ClientStatus.CONNECTING) { diff --git a/src/components/settings/roles/OverrideSwitch.tsx b/src/components/settings/roles/OverrideSwitch.tsx new file mode 100644 index 00000000..565a0f77 --- /dev/null +++ b/src/components/settings/roles/OverrideSwitch.tsx @@ -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 ( + + onChange("Deny")} + state="Deny" + selected={state === "Deny"}> + + + onChange("Neutral")} + state="Neutral" + selected={state === "Neutral"}> + + + onChange("Allow")} + state="Allow" + selected={state === "Allow"}> + + + + ); +} diff --git a/src/components/settings/roles/PermissionList.tsx b/src/components/settings/roles/PermissionList.tsx new file mode 100644 index 00000000..fd96caa1 --- /dev/null +++ b/src/components/settings/roles/PermissionList.tsx @@ -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) => ( + + ))} + + ); +} diff --git a/src/components/settings/roles/PermissionSelect.tsx b/src/components/settings/roles/PermissionSelect.tsx new file mode 100644 index 00000000..e049507f --- /dev/null +++ b/src/components/settings/roles/PermissionSelect.tsx @@ -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 ( + + + {id} + + + + + {typeof value === "object" ? ( + + ) : ( + + onChange( + Long.fromNumber(value, false) + .xor(permission) + .toNumber(), + ) + } + /> + )} + + ); +} diff --git a/src/components/settings/roles/RoleSelection.tsx b/src/components/settings/roles/RoleSelection.tsx new file mode 100644 index 00000000..8ffa4e1e --- /dev/null +++ b/src/components/settings/roles/RoleSelection.tsx @@ -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) => ( + onSelect(x.id)}> + {x.name} + + ))} + + ); +} diff --git a/src/components/settings/roles/UnsavedChanges.tsx b/src/components/settings/roles/UnsavedChanges.tsx new file mode 100644 index 00000000..e911bf67 --- /dev/null +++ b/src/components/settings/roles/UnsavedChanges.tsx @@ -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 ( + + + You have unsaved changes! + + + + ); +} diff --git a/src/components/ui/Checkbox.tsx b/src/components/ui/Checkbox.tsx index e681ed92..c5794b9f 100644 --- a/src/components/ui/Checkbox.tsx +++ b/src/components/ui/Checkbox.tsx @@ -89,7 +89,7 @@ export interface CheckboxProps { disabled?: boolean; contrast?: boolean; className?: string; - children: Children; + children?: Children; description?: Children; onChange: (state: boolean) => void; } diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index 851106c6..55f3ff0a 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -14,11 +14,7 @@ import { Cog, UserVoice } from "@styled-icons/boxicons-solid"; import { useHistory } from "react-router-dom"; import { Attachment } from "revolt-api/types/Autumn"; import { Presence, RelationshipStatus } from "revolt-api/types/Users"; -import { - ChannelPermission, - ServerPermission, - UserPermission, -} from "revolt.js/dist/api/permissions"; +import { Permission, UserPermission } from "revolt.js/dist/api/permissions"; import { Channel } from "revolt.js/dist/maps/Channels"; import { Message } from "revolt.js/dist/maps/Messages"; import { Server } from "revolt.js/dist/maps/Servers"; @@ -505,9 +501,8 @@ export default function ContextMenus() { if (server_list) { const server = client.servers.get(server_list)!; - const permissions = server.permission; if (server) { - if (permissions & ServerPermission.ManageChannels) { + if (server.havePermission("ManageChannel")) { generateAction({ action: "create_category", target: server, @@ -517,7 +512,8 @@ export default function ContextMenus() { target: server, }); } - if (permissions & ServerPermission.ManageServer) + + if (server.havePermission("ManageServer")) generateAction({ action: "open_server_settings", id: server_list, @@ -673,9 +669,7 @@ export default function ContextMenus() { userId !== uid && uid !== server.owner ) { - if ( - serverPermissions & ServerPermission.KickMembers - ) + if (serverPermissions & Permission.KickMembers) generateAction( { action: "kick_member", @@ -688,7 +682,7 @@ export default function ContextMenus() { "var(--error)", // the only relevant part really ); - if (serverPermissions & ServerPermission.BanMembers) + if (serverPermissions & Permission.BanMembers) generateAction( { action: "ban_member", @@ -718,8 +712,7 @@ export default function ContextMenus() { if (message && !queued) { const sendPermission = message.channel && - message.channel.permission & - ChannelPermission.SendMessage; + message.channel.permission & Permission.SendMessage; if (sendPermission) { generateAction({ @@ -759,8 +752,7 @@ export default function ContextMenus() { if ( message.author_id === userId || - channelPermissions & - ChannelPermission.ManageMessages + channelPermissions & Permission.ManageMessages ) { generateAction({ action: "delete_message", @@ -903,7 +895,7 @@ export default function ContextMenus() { case "VoiceChannel": if ( channelPermissions & - ChannelPermission.InviteOthers + Permission.InviteOthers ) { generateAction({ action: "create_invite", @@ -913,7 +905,7 @@ export default function ContextMenus() { if ( serverPermissions & - ServerPermission.ManageServer + Permission.ManageServer ) generateAction( { @@ -926,7 +918,7 @@ export default function ContextMenus() { if ( serverPermissions & - ServerPermission.ManageChannels + Permission.ManageChannel ) generateAction({ action: "delete_channel", @@ -958,20 +950,15 @@ export default function ContextMenus() { ); if ( - serverPermissions & - ServerPermission.ChangeNickname || - serverPermissions & - ServerPermission.ChangeAvatar + serverPermissions & Permission.ChangeNickname || + serverPermissions & Permission.ChangeAvatar ) generateAction( { action: "edit_identity", target: server }, "edit_identity", ); - if ( - serverPermissions & - ServerPermission.ManageServer - ) + if (serverPermissions & Permission.ManageServer) generateAction( { action: "open_server_settings", diff --git a/src/pages/invite/InviteBot.tsx b/src/pages/invite/InviteBot.tsx index 6089c826..3a5b268d 100644 --- a/src/pages/invite/InviteBot.tsx +++ b/src/pages/invite/InviteBot.tsx @@ -1,5 +1,5 @@ import { useParams } from "react-router-dom"; -import { ServerPermission } from "revolt.js"; +import { Permission } from "revolt.js"; import { Route } from "revolt.js/dist/api/routes"; import styled from "styled-components/macro"; @@ -74,8 +74,7 @@ export default function InviteBot() { {[...client.servers.values()] .filter( (x) => - x.permission & - ServerPermission.ManageServer, + x.permission & Permission.ManageServer, ) .map((server) => (