Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 367 additions and 232 deletions
import { Servers } from "revolt.js/dist/api/objects";
import { ChevronDown } from "@styled-icons/boxicons-regular";
import { isEqual } from "lodash";
import { observer } from "mobx-react-lite";
import { Member } from "revolt.js/dist/maps/Members";
import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import { useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
import UserIcon from "../../../components/common/user/UserIcon";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
import IconButton from "../../../components/ui/IconButton";
import Overline from "../../../components/ui/Overline";
interface Props {
server: Servers.Server;
server: Server;
}
// ! FIXME: bad code :)
export function Members({ server }: Props) {
const [members, setMembers] = useState<Servers.Member[] | undefined>(
undefined,
);
export const Members = observer(({ server }: Props) => {
const [selected, setSelected] = useState<undefined | string>();
const [data, setData] = useState<
{ members: Member[]; users: User[] } | undefined
>(undefined);
const ctx = useForceUpdate();
const users = useUsers(members?.map((x) => x._id.user) ?? [], ctx);
useEffect(() => {
server.fetchMembers().then(setData);
}, [server, setData]);
const [roles, setRoles] = useState<string[]>([]);
useEffect(() => {
ctx.client.servers.members
.fetchMembers(server._id)
.then((members) => setMembers(members));
}, []);
if (selected) {
setRoles(
data!.members.find((x) => x._id.user === selected)?.roles ?? [],
);
}
}, [setRoles, selected, data]);
return (
<div className={styles.members}>
<div className={styles.userList}>
<div className={styles.subtitle}>
{members?.length ?? 0} Members
{data?.members.length ?? 0} Members
</div>
{members &&
members.length > 0 &&
users?.map(
(x) =>
x && (
<div className={styles.member}>
<div>@{x.username}</div>
{data &&
data.members.length > 0 &&
data.members
.map((member) => {
return {
member,
user: data.users.find(
(x) => x._id === member._id.user,
),
};
})
.map(({ member, user }) => (
// @ts-expect-error brokey
// eslint-disable-next-line react/jsx-no-undef
<Fragment key={member._id.user}>
<div
className={styles.member}
data-open={selected === member._id.user}
onClick={() =>
setSelected(
selected === member._id.user
? undefined
: member._id.user,
)
}>
<span>
<UserIcon target={user} size={24} />{" "}
{user?.username ?? (
<Text id="app.main.channel.unknown_user" />
)}
</span>
<IconButton className={styles.chevron}>
<ChevronDown size={24} />
</IconButton>
</div>
),
)}
{selected === member._id.user && (
<div
key={`drop_${member._id.user}`}
className={styles.memberView}>
<Overline type="subtle">Roles</Overline>
{Object.keys(server.roles ?? {}).map(
(key) => {
const role = server.roles![key];
return (
<Checkbox
key={key}
checked={
roles.includes(key) ??
false
}
onChange={(v) => {
if (v) {
setRoles([
...roles,
key,
]);
} else {
setRoles(
roles.filter(
(x) =>
x !==
key,
),
);
}
}}>
<span
style={{
color: role.colour,
}}>
{role.name}
</span>
</Checkbox>
);
},
)}
<Button
compact
disabled={isEqual(
member.roles ?? [],
roles,
)}
onClick={() =>
member.edit({
roles,
})
}>
<Text id="app.special.modals.actions.save" />
</Button>
</div>
)}
</Fragment>
))}
</div>
);
}
});
import isEqual from "lodash.isequal";
import { Servers, Server } from "revolt.js/dist/api/objects";
import { observer } from "mobx-react-lite";
import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { FileUploader } from "../../../context/revoltjs/FileUploads";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../../context/revoltjs/util";
import Button from "../../../components/ui/Button";
......@@ -16,12 +16,10 @@ import ComboBox from "../../../components/ui/ComboBox";
import InputBox from "../../../components/ui/InputBox";
interface Props {
server: Servers.Server;
server: Server;
}
export function Overview({ server }: Props) {
const client = useContext(AppContext);
export const Overview = observer(({ server }: Props) => {
const [name, setName] = useState(server.name);
const [description, setDescription] = useState(server.description ?? "");
const [systemMessages, setSystemMessages] = useState(
......@@ -40,16 +38,14 @@ export function Overview({ server }: Props) {
const [changed, setChanged] = useState(false);
function save() {
let changes: Partial<
Pick<Servers.Server, "name" | "description" | "system_messages">
> = {};
const changes: Record<string, unknown> = {};
if (name !== server.name) changes.name = name;
if (description !== server.description)
changes.description = description;
if (!isEqual(systemMessages, server.system_messages))
changes.system_messages = systemMessages;
changes.system_messages = systemMessages ?? undefined;
client.servers.edit(server._id, changes);
server.edit(changes);
setChanged(false);
}
......@@ -63,17 +59,9 @@ export function Overview({ server }: Props) {
fileType="icons"
behaviour="upload"
maxFileSize={2_500_000}
onUpload={(icon) =>
client.servers.edit(server._id, { icon })
}
previewURL={client.servers.getIconURL(
server._id,
{ max_side: 256 },
true,
)}
remove={() =>
client.servers.edit(server._id, { remove: "Icon" })
}
onUpload={(icon) => server.edit({ icon })}
previewURL={server.generateIconURL({ max_side: 256 }, true)}
remove={() => server.edit({ remove: "Icon" })}
/>
<div className={styles.name}>
<h3>
......@@ -115,17 +103,9 @@ export function Overview({ server }: Props) {
fileType="banners"
behaviour="upload"
maxFileSize={6_000_000}
onUpload={(banner) =>
client.servers.edit(server._id, { banner })
}
previewURL={client.servers.getBannerURL(
server._id,
{ width: 1000 },
true,
)}
remove={() =>
client.servers.edit(server._id, { remove: "Banner" })
}
onUpload={(banner) => server.edit({ banner })}
previewURL={server.generateBannerURL({ width: 1000 }, true)}
remove={() => server.edit({ remove: "Banner" })}
/>
<h3>
......@@ -139,6 +119,7 @@ export function Overview({ server }: Props) {
].map(([i18n, key]) => (
// ! FIXME: temporary code just so we can expose the options
<p
key={key}
style={{
display: "flex",
gap: "8px",
......@@ -170,15 +151,13 @@ export function Overview({ server }: Props) {
<option value="disabled">
<Text id="general.disabled" />
</option>
{server.channels.map((id) => {
const channel = client.channels.get(id);
if (!channel) return null;
return (
<option value={id}>
{getChannelName(client, channel, true)}
{server.channels
.filter((x) => typeof x !== "undefined")
.map((channel) => (
<option key={channel!._id} value={channel!._id}>
{getChannelName(channel!, true)}
</option>
);
})}
))}
</ComboBox>
</p>
))}
......@@ -190,4 +169,4 @@ export function Overview({ server }: Props) {
</p>
</div>
);
}
});
......@@ -17,21 +17,32 @@
}
}
.invites {
.userList {
gap: 8px;
display: flex;
flex-direction: column;
.subtitle {
gap: 8px;
display: flex;
justify-content: space-between;
font-size: 13px;
text-transform: uppercase;
color: var(--secondary-foreground);
font-weight: 700;
.reason {
text-align: center;
}
}
.reason {
flex: 2;
}
.invite {
.invite,
.ban,
.member {
gap: 8px;
padding: 10px;
display: flex;
......@@ -39,7 +50,8 @@
flex-direction: row;
background: var(--secondary-background);
code, span {
span,
code {
flex: 1;
}
......@@ -58,25 +70,23 @@
opacity: 0.5;
}
}
}
.members {
.subtitle {
display: flex;
justify-content: space-between;
font-size: 13px;
text-transform: uppercase;
color: var(--secondary-foreground);
font-weight: 700;
}
.member {
gap: 8px;
cursor: pointer;
.chevron {
transition: 0.2s ease all;
}
&:not([data-open="true"]) .chevron {
transform: rotateZ(90deg);
}
}
.memberView {
padding: 10px;
display: flex;
align-items: center;
flex-direction: row;
background: var(--secondary-background);
margin: 0 10px;
background: var(--background);
}
}
......@@ -95,10 +105,6 @@
flex-grow: 1;
padding: 0 8px;
overflow-y: scroll;
section {
margin-bottom: 1em;
}
}
.title {
......@@ -107,7 +113,8 @@
margin-bottom: 1em;
align-items: center;
h1, h2 {
h1,
h2 {
margin: 0;
min-width: 0;
flex-grow: 1;
......@@ -126,4 +133,4 @@
display: flex;
padding: 8px 0;
}
}
\ No newline at end of file
}
import { Plus } from "@styled-icons/boxicons-regular";
import isEqual from "lodash.isequal";
import { Servers } from "revolt.js/dist/api/objects";
import {
ChannelPermission,
ServerPermission,
} from "revolt.js/dist/api/permissions";
import { observer } from "mobx-react-lite";
import { ChannelPermission, ServerPermission } from "revolt.js";
import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
import IconButton from "../../../components/ui/IconButton";
import ColourSwatches from "../../../components/ui/ColourSwatches";
import InputBox from "../../../components/ui/InputBox";
import Overline from "../../../components/ui/Overline";
import Tip from "../../../components/ui/Tip";
import ButtonItem from "../../../components/navigation/items/ButtonItem";
interface Props {
server: Servers.Server;
server: Server;
}
const I32ToU32 = (arr: number[]) => arr.map((x) => x >>> 0);
// ! FIXME: bad code :)
export function Roles({ server }: Props) {
export const Roles = observer(({ server }: Props) => {
const [role, setRole] = useState("default");
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const roles = server.roles ?? {};
const roles = useMemo(() => server.roles ?? {}, [server]);
if (role !== "default" && typeof roles[role] === "undefined") {
useEffect(() => setRole("default"));
useEffect(() => setRole("default"), [role]);
return null;
}
const v = (id: string) =>
I32ToU32(
id === "default"
? server.default_permissions
: roles[id].permissions,
);
const [perm, setPerm] = useState(v(role));
useEffect(() => setPerm(v(role)), [role, roles[role]?.permissions]);
const modified = !isEqual(perm, v(role));
const save = () =>
client.servers.setPermissions(server._id, role, {
server: perm[0],
channel: perm[1],
});
const {
name: roleName,
colour: roleColour,
permissions,
} = roles[role] ?? {};
const getPermissions = useCallback(
(id: string) => {
return I32ToU32(
id === "default"
? server.default_permissions
: roles[id].permissions,
);
},
[roles, server],
);
const [perm, setPerm] = useState(getPermissions(role));
const [name, setName] = useState(roleName);
const [colour, setColour] = useState(roleColour);
useEffect(
() => setPerm(getPermissions(role)),
[getPermissions, role, permissions],
);
useEffect(() => setName(roleName), [role, roleName]);
useEffect(() => setColour(roleColour), [role, roleColour]);
const modified =
!isEqual(perm, getPermissions(role)) ||
!isEqual(name, roleName) ||
!isEqual(colour, roleColour);
const save = () => {
if (!isEqual(perm, getPermissions(role))) {
server.setPermissions(role, {
server: perm[0],
channel: perm[1],
});
}
if (!isEqual(name, roleName) || !isEqual(colour, roleColour)) {
server.editRole(role, { name, colour });
}
};
const deleteRole = () => {
setRole("default");
client.servers.deleteRole(server._id, role);
server.deleteRole(role);
};
return (
......@@ -73,7 +100,7 @@ export function Roles({ server }: Props) {
openScreen({
id: "special_input",
type: "create_role",
server: server._id,
server,
callback: (id) => setRole(id),
})
}
......@@ -88,15 +115,16 @@ export function Roles({ server }: Props) {
<Text id="app.settings.permissions.default_role" />
</ButtonItem>
);
} else {
return (
<ButtonItem
active={role === id}
onClick={() => setRole(id)}>
{roles[id].name}
</ButtonItem>
);
}
return (
<ButtonItem
key={id}
active={role === id}
onClick={() => setRole(id)}
style={{ color: roles[id].colour }}>
{roles[id].name}
</ButtonItem>
);
})}
</div>
<div className={styles.permissions}>
......@@ -112,19 +140,45 @@ export function Roles({ server }: Props) {
Save
</Button>
</div>
{role !== "default" && (
<>
<section>
<Overline type="subtle">Role Name</Overline>
<p>
<InputBox
value={name}
onChange={(e) =>
setName(e.currentTarget.value)
}
contrast
/>
</p>
</section>
<section>
<Overline type="subtle">Role Colour</Overline>
<p>
<ColourSwatches
value={colour ?? "gray"}
onChange={(value) => setColour(value)}
/>
</p>
</section>
</>
)}
<section>
<Overline type="subtle">
<Text id="app.settings.permissions.server" />
</Overline>
{Object.keys(ServerPermission).map((key) => {
if (key === "View") return;
let value =
const value =
ServerPermission[
key as keyof typeof ServerPermission
];
return (
<Checkbox
key={key}
checked={(perm[0] & value) > 0}
onChange={() =>
setPerm([perm[0] ^ value, perm[1]])
......@@ -143,13 +197,14 @@ export function Roles({ server }: Props) {
</Overline>
{Object.keys(ChannelPermission).map((key) => {
if (key === "ManageChannel") return;
let value =
const value =
ChannelPermission[
key as keyof typeof ChannelPermission
];
return (
<Checkbox
key={key}
checked={((perm[1] >>> 0) & value) > 0}
onChange={() =>
setPerm([perm[0], perm[1] ^ value])
......@@ -176,4 +231,4 @@ export function Roles({ server }: Props) {
</div>
</div>
);
}
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
/* eslint-disable */
import JSX = preact.JSX;
......@@ -11,6 +11,6 @@ export function connectState<T>(
mapKeys: (state: State, props: T) => any,
memoize?: boolean,
): ConnectedComponent<(props: any) => h.JSX.Element | null, T> {
let c = connect(mapKeys)(component);
const c = connect(mapKeys)(component);
return memoize ? memo(c) : c;
}
import localForage from "localforage";
import { createStore } from "redux";
import { Core } from "revolt.js/dist/api/objects";
import { RevoltConfiguration } from "revolt-api/types/Core";
import { Language } from "../context/Locale";
......@@ -14,17 +14,15 @@ import { QueuedMessage } from "./reducers/queue";
import { SectionToggle } from "./reducers/section_toggle";
import { Settings } from "./reducers/settings";
import { SyncOptions } from "./reducers/sync";
import { Typing } from "./reducers/typing";
import { Unreads } from "./reducers/unreads";
export type State = {
config: Core.RevoltNodeConfiguration;
config: RevoltConfiguration;
locale: Language;
auth: AuthState;
settings: Settings;
unreads: Unreads;
queue: QueuedMessage[];
typing: Typing;
drafts: Drafts;
sync: SyncOptions;
experiments: ExperimentOptions;
......
import type { Auth } from "revolt.js/dist/api/objects";
import { Session } from "revolt-api/types/Auth";
export interface AuthState {
accounts: {
[key: string]: {
session: Auth.Session;
session: Session;
};
};
active?: string;
......@@ -13,7 +13,7 @@ export type AuthAction =
| { type: undefined }
| {
type: "LOGIN";
session: Auth.Session;
session: Session;
}
| {
type: "LOGOUT";
......
export type Experiments = never;
export const AVAILABLE_EXPERIMENTS: Experiments[] = [];
export type Experiments = "search";
export const AVAILABLE_EXPERIMENTS: Experiments[] = ["search"];
export const EXPERIMENTS: {
[key in Experiments]: { title: string; description: string };
} = {
search: {
title: "Search",
description: "Allows you to search for messages in channels.",
},
};
export interface ExperimentOptions {
enabled?: Experiments[];
......
......@@ -12,7 +12,6 @@ import { sectionToggle, SectionToggleAction } from "./section_toggle";
import { config, ConfigAction } from "./server_config";
import { settings, SettingsAction } from "./settings";
import { sync, SyncAction } from "./sync";
import { typing, TypingAction } from "./typing";
import { unreads, UnreadsAction } from "./unreads";
export default combineReducers({
......@@ -22,7 +21,6 @@ export default combineReducers({
settings,
unreads,
queue,
typing,
drafts,
sync,
experiments,
......@@ -38,7 +36,6 @@ export type Action =
| SettingsAction
| UnreadsAction
| QueueAction
| TypingAction
| DraftAction
| SyncAction
| ExperimentsAction
......@@ -46,15 +43,3 @@ export type Action =
| NotificationsAction
| SectionToggleAction
| { type: "__INIT"; state: State };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function filter(obj: any, keys: string[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newObj: any = {};
for (const key of keys) {
const v = obj[key];
if (v) newObj[key] = v;
}
return newObj;
}
import type { Channel, Message } from "revolt.js";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import type { SyncUpdateAction } from "./sync";
......@@ -35,7 +36,7 @@ export function shouldNotify(
case "none":
return false;
case "mention": {
if (!message.mentions?.includes(user_id)) return false;
if (!message.mention_ids?.includes(user_id)) return false;
}
}
......
import type { MessageObject } from "../../context/revoltjs/util";
export enum QueueStatus {
SENDING = "sending",
ERRORED = "errored",
......@@ -10,7 +8,11 @@ export interface Reply {
mention: boolean;
}
export type QueuedMessageData = Omit<MessageObject, "content" | "replies"> & {
export type QueuedMessageData = {
_id: string;
author: string;
channel: string;
content: string;
replies: Reply[];
};
......
import type { Core } from "revolt.js/dist/api/objects";
import type { RevoltConfiguration } from "revolt-api/types/Core";
export type ConfigAction =
| { type: undefined }
| {
type: "SET_CONFIG";
config: Core.RevoltNodeConfiguration;
config: RevoltConfiguration;
};
export function config(
state = {} as Core.RevoltNodeConfiguration,
state = {} as RevoltConfiguration,
action: ConfigAction,
): Core.RevoltNodeConfiguration {
): RevoltConfiguration {
switch (action.type) {
case "SET_CONFIG":
return action.config;
......
......@@ -2,7 +2,6 @@ import type { Theme, ThemeOptions } from "../../context/Theme";
import { setEmojiPack } from "../../components/common/Emoji";
import { filter } from ".";
import type { Sounds } from "../../assets/sounds/Audio";
import type { SyncUpdateAction } from "./sync";
......@@ -67,7 +66,7 @@ export function settings(
return {
...state,
theme: {
...filter(state.theme, ["custom", "preset", "ligatures"]),
...state.theme,
...action.theme,
},
};
......@@ -94,7 +93,7 @@ export function settings(
return {
...state,
appearance: {
...filter(state.appearance, ["emojiPack"]),
...state.appearance,
...action.options,
},
};
......
export type TypingUser = { id: string; started: number };
export type Typing = { [key: string]: TypingUser[] };
export type TypingAction =
| { type: undefined }
| {
type: "TYPING_START";
channel: string;
user: string;
}
| {
type: "TYPING_STOP";
channel: string;
user: string;
}
| {
type: "RESET";
};
export function typing(state: Typing = {}, action: TypingAction): Typing {
switch (action.type) {
case "TYPING_START":
return {
...state,
[action.channel]: [
...(state[action.channel] ?? []).filter(
(x) => x.id !== action.user,
),
{
id: action.user,
started: +new Date(),
},
],
};
case "TYPING_STOP":
return {
...state,
[action.channel]:
state[action.channel]?.filter(
(x) => x.id !== action.user,
) ?? [],
};
case "RESET":
return {};
default:
return state;
}
}
import type { Sync } from "revolt.js/dist/api/objects";
import type { ChannelUnread } from "revolt-api/types/Sync";
export interface Unreads {
[key: string]: Partial<Omit<Sync.ChannelUnread, "_id">>;
[key: string]: Partial<Omit<ChannelUnread, "_id">>;
}
export type UnreadsAction =
......@@ -13,7 +13,7 @@ export type UnreadsAction =
}
| {
type: "UNREADS_SET";
unreads: Sync.ChannelUnread[];
unreads: ChannelUnread[];
}
| {
type: "UNREADS_MENTION";
......
export const REPO_URL = "https://gitlab.insrt.uk/revolt/revite/-/commit";
export const GIT_REVISION = "__GIT_REVISION__";
/* eslint-disable */
// Strings needs to be explictly stated here as they can cause type issues elsewhere.
export const REPO_URL: string =
"https://gitlab.insrt.uk/revolt/revite/-/commit";
export const GIT_REVISION: string = "__GIT_REVISION__";
export const GIT_BRANCH: string = "__GIT_BRANCH__";
.preact-context-menu .context-menu {
z-index: 10000;
z-index: 5000;
min-width: 190px;
padding: 6px 8px;
user-select: none;
border-radius: 4px;
font-size: .875rem;
color: var(--secondary-foreground);
border-radius: var(--border-radius);
background: var(--primary-background) !important;
box-shadow: 0px 0px 8px 8px rgba(0, 0, 0, 0.05);
......@@ -48,9 +48,17 @@
flex-grow: 1;
display: flex;
flex-direction: column;
.username {
> div {
cursor: pointer;
width: fit-content;
}
}
}
.status {
cursor: pointer;
max-width: 132px;
font-size: .625rem;
color: var(--secondary-foreground);
......
......@@ -16,6 +16,10 @@
background: var(--scrollbar-thumb);
}
::-webkit-scrollbar-corner {
background: transparent;
}
::selection {
background: var(--accent);
color: var(--foreground);
......@@ -44,3 +48,7 @@ hr {
height: 1px;
flex-grow: 1;
}
foreignObject > svg {
vertical-align: top !important;
}
:root {
/**
* Appearance
*/
--ligatures: none;
--text-size: 14px;
--font: "Open Sans";
--app-height: 100vh;
--codeblock-font: "Fira Code";
--sidebar-active: var(--secondary-background);
--input-border-width: 2px;
/**
* Native
*/
--titlebar-height: 29px;
--titlebar-action-padding: 8px;
--titlebar-logo-color: var(--secondary-foreground);
/**
* Layout
*/
--app-height: 100vh;
--border-radius: 6px;
--input-border-width: 2px;
--textarea-padding: 16px;
--textarea-line-height: 20px;
--message-box-padding: 14px 14px 14px 0;
--attachment-max-width: 480px;
--attachment-max-height: 640px;
--attachment-default-width: 400px;
--attachment-max-text-width: 800px;
--bottom-navigation-height: 50px;
/**
* Experimental
*/
--background-rgb: (
25,
25,
25
); //THIS IS SO THAT WE CAN HAVE CUSTOM BACKGROUNDS FOR THE CLIENT, CONVERTS THE HEX TO AN RGB VALUE FROM --background
--background-rgba: rgba(
var(--background-rgb),
0.8
); //make the opacity also customizable
}