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) => (