forked from abner/for-legacy-web
Use tabWidth 4 without actual tabs.
This commit is contained in:
@@ -13,75 +13,75 @@ import Overview from "./channel/Overview";
|
||||
import Permissions from "./channel/Permissions";
|
||||
|
||||
export default function ChannelSettings() {
|
||||
const { channel: cid } = useParams<{ channel: string }>();
|
||||
const ctx = useForceUpdate();
|
||||
const channel = useChannel(cid, ctx);
|
||||
if (!channel) return null;
|
||||
if (
|
||||
channel.channel_type === "SavedMessages" ||
|
||||
channel.channel_type === "DirectMessage"
|
||||
)
|
||||
return null;
|
||||
const { channel: cid } = useParams<{ channel: string }>();
|
||||
const ctx = useForceUpdate();
|
||||
const channel = useChannel(cid, ctx);
|
||||
if (!channel) return null;
|
||||
if (
|
||||
channel.channel_type === "SavedMessages" ||
|
||||
channel.channel_type === "DirectMessage"
|
||||
)
|
||||
return null;
|
||||
|
||||
const history = useHistory();
|
||||
function switchPage(to?: string) {
|
||||
let base_url;
|
||||
switch (channel?.channel_type) {
|
||||
case "TextChannel":
|
||||
case "VoiceChannel":
|
||||
base_url = `/server/${channel.server}/channel/${cid}/settings`;
|
||||
break;
|
||||
default:
|
||||
base_url = `/channel/${cid}/settings`;
|
||||
}
|
||||
const history = useHistory();
|
||||
function switchPage(to?: string) {
|
||||
let base_url;
|
||||
switch (channel?.channel_type) {
|
||||
case "TextChannel":
|
||||
case "VoiceChannel":
|
||||
base_url = `/server/${channel.server}/channel/${cid}/settings`;
|
||||
break;
|
||||
default:
|
||||
base_url = `/channel/${cid}/settings`;
|
||||
}
|
||||
|
||||
if (to) {
|
||||
history.replace(`${base_url}/${to}`);
|
||||
} else {
|
||||
history.replace(base_url);
|
||||
}
|
||||
}
|
||||
if (to) {
|
||||
history.replace(`${base_url}/${to}`);
|
||||
} else {
|
||||
history.replace(base_url);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericSettings
|
||||
pages={[
|
||||
{
|
||||
category: (
|
||||
<Category
|
||||
variant="uniform"
|
||||
text={getChannelName(ctx.client, channel, true)}
|
||||
/>
|
||||
),
|
||||
id: "overview",
|
||||
icon: <ListUl size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.channel_pages.overview.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "permissions",
|
||||
icon: <ListCheck size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.channel_pages.permissions.title" />
|
||||
),
|
||||
},
|
||||
]}
|
||||
children={[
|
||||
<Route path="/server/:server/channel/:channel/settings/permissions">
|
||||
<Permissions channel={channel} />
|
||||
</Route>,
|
||||
<Route path="/channel/:channel/settings/permissions">
|
||||
<Permissions channel={channel} />
|
||||
</Route>,
|
||||
return (
|
||||
<GenericSettings
|
||||
pages={[
|
||||
{
|
||||
category: (
|
||||
<Category
|
||||
variant="uniform"
|
||||
text={getChannelName(ctx.client, channel, true)}
|
||||
/>
|
||||
),
|
||||
id: "overview",
|
||||
icon: <ListUl size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.channel_pages.overview.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "permissions",
|
||||
icon: <ListCheck size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.channel_pages.permissions.title" />
|
||||
),
|
||||
},
|
||||
]}
|
||||
children={[
|
||||
<Route path="/server/:server/channel/:channel/settings/permissions">
|
||||
<Permissions channel={channel} />
|
||||
</Route>,
|
||||
<Route path="/channel/:channel/settings/permissions">
|
||||
<Permissions channel={channel} />
|
||||
</Route>,
|
||||
|
||||
<Route path="/">
|
||||
<Overview channel={channel} />
|
||||
</Route>,
|
||||
]}
|
||||
category="channel_pages"
|
||||
switchPage={switchPage}
|
||||
defaultPage="overview"
|
||||
showExitButton
|
||||
/>
|
||||
);
|
||||
<Route path="/">
|
||||
<Overview channel={channel} />
|
||||
</Route>,
|
||||
]}
|
||||
category="channel_pages"
|
||||
switchPage={switchPage}
|
||||
defaultPage="overview"
|
||||
showExitButton
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,140 +19,140 @@ import ButtonItem from "../../components/navigation/items/ButtonItem";
|
||||
import { Children } from "../../types/Preact";
|
||||
|
||||
interface Props {
|
||||
pages: {
|
||||
category?: Children;
|
||||
divider?: boolean;
|
||||
id: string;
|
||||
icon: Children;
|
||||
title: Children;
|
||||
hideTitle?: boolean;
|
||||
}[];
|
||||
custom?: Children;
|
||||
children: Children;
|
||||
defaultPage: string;
|
||||
showExitButton?: boolean;
|
||||
switchPage: (to?: string) => void;
|
||||
category: "pages" | "channel_pages" | "server_pages";
|
||||
pages: {
|
||||
category?: Children;
|
||||
divider?: boolean;
|
||||
id: string;
|
||||
icon: Children;
|
||||
title: Children;
|
||||
hideTitle?: boolean;
|
||||
}[];
|
||||
custom?: Children;
|
||||
children: Children;
|
||||
defaultPage: string;
|
||||
showExitButton?: boolean;
|
||||
switchPage: (to?: string) => void;
|
||||
category: "pages" | "channel_pages" | "server_pages";
|
||||
}
|
||||
|
||||
export function GenericSettings({
|
||||
pages,
|
||||
switchPage,
|
||||
category,
|
||||
custom,
|
||||
children,
|
||||
defaultPage,
|
||||
showExitButton,
|
||||
pages,
|
||||
switchPage,
|
||||
category,
|
||||
custom,
|
||||
children,
|
||||
defaultPage,
|
||||
showExitButton,
|
||||
}: Props) {
|
||||
const history = useHistory();
|
||||
const theme = useContext(ThemeContext);
|
||||
const { page } = useParams<{ page: string }>();
|
||||
const history = useHistory();
|
||||
const theme = useContext(ThemeContext);
|
||||
const { page } = useParams<{ page: string }>();
|
||||
|
||||
function exitSettings() {
|
||||
if (history.length > 0) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push("/");
|
||||
}
|
||||
}
|
||||
function exitSettings() {
|
||||
if (history.length > 0) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push("/");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function keyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
exitSettings();
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
function keyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
exitSettings();
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener("keydown", keyDown);
|
||||
return () => document.body.removeEventListener("keydown", keyDown);
|
||||
}, []);
|
||||
document.body.addEventListener("keydown", keyDown);
|
||||
return () => document.body.removeEventListener("keydown", keyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.settings} data-mobile={isTouchscreenDevice}>
|
||||
<Helmet>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content={
|
||||
isTouchscreenDevice
|
||||
? theme["primary-header"]
|
||||
: theme["secondary-background"]
|
||||
}
|
||||
/>
|
||||
</Helmet>
|
||||
{isTouchscreenDevice && (
|
||||
<Header placement="primary">
|
||||
{typeof page === "undefined" ? (
|
||||
<>
|
||||
{showExitButton && (
|
||||
<IconButton onClick={exitSettings}>
|
||||
<X size={24} />
|
||||
</IconButton>
|
||||
)}
|
||||
<Text id="app.settings.title" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconButton onClick={() => switchPage()}>
|
||||
<ArrowBack size={24} />
|
||||
</IconButton>
|
||||
<Text
|
||||
id={`app.settings.${category}.${page}.title`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Header>
|
||||
)}
|
||||
{(!isTouchscreenDevice || typeof page === "undefined") && (
|
||||
<div className={styles.sidebar}>
|
||||
<div className={styles.container}>
|
||||
{pages.map((entry, i) => (
|
||||
<>
|
||||
{entry.category && (
|
||||
<Category
|
||||
variant="uniform"
|
||||
text={entry.category}
|
||||
/>
|
||||
)}
|
||||
<ButtonItem
|
||||
active={
|
||||
page === entry.id ||
|
||||
(i === 0 &&
|
||||
!isTouchscreenDevice &&
|
||||
typeof page === "undefined")
|
||||
}
|
||||
onClick={() => switchPage(entry.id)}
|
||||
compact>
|
||||
{entry.icon} {entry.title}
|
||||
</ButtonItem>
|
||||
{entry.divider && <LineDivider />}
|
||||
</>
|
||||
))}
|
||||
{custom}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(!isTouchscreenDevice || typeof page === "string") && (
|
||||
<div className={styles.content}>
|
||||
{!isTouchscreenDevice &&
|
||||
!pages.find((x) => x.id === page && x.hideTitle) && (
|
||||
<h1>
|
||||
<Text
|
||||
id={`app.settings.${category}.${
|
||||
page ?? defaultPage
|
||||
}.title`}
|
||||
/>
|
||||
</h1>
|
||||
)}
|
||||
<Switch>{children}</Switch>
|
||||
</div>
|
||||
)}
|
||||
{!isTouchscreenDevice && (
|
||||
<div className={styles.action}>
|
||||
<IconButton onClick={exitSettings}>
|
||||
<XCircle size={48} />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.settings} data-mobile={isTouchscreenDevice}>
|
||||
<Helmet>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content={
|
||||
isTouchscreenDevice
|
||||
? theme["primary-header"]
|
||||
: theme["secondary-background"]
|
||||
}
|
||||
/>
|
||||
</Helmet>
|
||||
{isTouchscreenDevice && (
|
||||
<Header placement="primary">
|
||||
{typeof page === "undefined" ? (
|
||||
<>
|
||||
{showExitButton && (
|
||||
<IconButton onClick={exitSettings}>
|
||||
<X size={24} />
|
||||
</IconButton>
|
||||
)}
|
||||
<Text id="app.settings.title" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconButton onClick={() => switchPage()}>
|
||||
<ArrowBack size={24} />
|
||||
</IconButton>
|
||||
<Text
|
||||
id={`app.settings.${category}.${page}.title`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Header>
|
||||
)}
|
||||
{(!isTouchscreenDevice || typeof page === "undefined") && (
|
||||
<div className={styles.sidebar}>
|
||||
<div className={styles.container}>
|
||||
{pages.map((entry, i) => (
|
||||
<>
|
||||
{entry.category && (
|
||||
<Category
|
||||
variant="uniform"
|
||||
text={entry.category}
|
||||
/>
|
||||
)}
|
||||
<ButtonItem
|
||||
active={
|
||||
page === entry.id ||
|
||||
(i === 0 &&
|
||||
!isTouchscreenDevice &&
|
||||
typeof page === "undefined")
|
||||
}
|
||||
onClick={() => switchPage(entry.id)}
|
||||
compact>
|
||||
{entry.icon} {entry.title}
|
||||
</ButtonItem>
|
||||
{entry.divider && <LineDivider />}
|
||||
</>
|
||||
))}
|
||||
{custom}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(!isTouchscreenDevice || typeof page === "string") && (
|
||||
<div className={styles.content}>
|
||||
{!isTouchscreenDevice &&
|
||||
!pages.find((x) => x.id === page && x.hideTitle) && (
|
||||
<h1>
|
||||
<Text
|
||||
id={`app.settings.${category}.${
|
||||
page ?? defaultPage
|
||||
}.title`}
|
||||
/>
|
||||
</h1>
|
||||
)}
|
||||
<Switch>{children}</Switch>
|
||||
</div>
|
||||
)}
|
||||
{!isTouchscreenDevice && (
|
||||
<div className={styles.action}>
|
||||
<IconButton onClick={exitSettings}>
|
||||
<XCircle size={48} />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
ListUl,
|
||||
Share,
|
||||
Group,
|
||||
ListCheck,
|
||||
ListUl,
|
||||
Share,
|
||||
Group,
|
||||
ListCheck,
|
||||
} from "@styled-icons/boxicons-regular";
|
||||
import { XSquare } from "@styled-icons/boxicons-solid";
|
||||
import { Route, useHistory, useParams } from "react-router-dom";
|
||||
@@ -22,85 +22,85 @@ import { Overview } from "./server/Overview";
|
||||
import { Roles } from "./server/Roles";
|
||||
|
||||
export default function ServerSettings() {
|
||||
const { server: sid } = useParams<{ server: string }>();
|
||||
const server = useServer(sid);
|
||||
if (!server) return null;
|
||||
const { server: sid } = useParams<{ server: string }>();
|
||||
const server = useServer(sid);
|
||||
if (!server) return null;
|
||||
|
||||
const history = useHistory();
|
||||
function switchPage(to?: string) {
|
||||
if (to) {
|
||||
history.replace(`/server/${sid}/settings/${to}`);
|
||||
} else {
|
||||
history.replace(`/server/${sid}/settings`);
|
||||
}
|
||||
}
|
||||
const history = useHistory();
|
||||
function switchPage(to?: string) {
|
||||
if (to) {
|
||||
history.replace(`/server/${sid}/settings/${to}`);
|
||||
} else {
|
||||
history.replace(`/server/${sid}/settings`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericSettings
|
||||
pages={[
|
||||
{
|
||||
category: <Category variant="uniform" text={server.name} />, //TOFIX: Just add the server.name as a string, otherwise it makes a duplicate category
|
||||
id: "overview",
|
||||
icon: <ListUl size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.server_pages.overview.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "members",
|
||||
icon: <Group size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.server_pages.members.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "invites",
|
||||
icon: <Share size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.server_pages.invites.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "bans",
|
||||
icon: <XSquare size={20} />,
|
||||
title: <Text id="app.settings.server_pages.bans.title" />,
|
||||
},
|
||||
{
|
||||
id: "roles",
|
||||
icon: <ListCheck size={20} />,
|
||||
title: <Text id="app.settings.server_pages.roles.title" />,
|
||||
hideTitle: true,
|
||||
},
|
||||
]}
|
||||
children={[
|
||||
<Route path="/server/:server/settings/members">
|
||||
<RequiresOnline>
|
||||
<Members server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/server/:server/settings/invites">
|
||||
<RequiresOnline>
|
||||
<Invites server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/server/:server/settings/bans">
|
||||
<RequiresOnline>
|
||||
<Bans server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/server/:server/settings/roles">
|
||||
<RequiresOnline>
|
||||
<Roles server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/">
|
||||
<Overview server={server} />
|
||||
</Route>,
|
||||
]}
|
||||
category="server_pages"
|
||||
switchPage={switchPage}
|
||||
defaultPage="overview"
|
||||
showExitButton
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<GenericSettings
|
||||
pages={[
|
||||
{
|
||||
category: <Category variant="uniform" text={server.name} />, //TOFIX: Just add the server.name as a string, otherwise it makes a duplicate category
|
||||
id: "overview",
|
||||
icon: <ListUl size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.server_pages.overview.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "members",
|
||||
icon: <Group size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.server_pages.members.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "invites",
|
||||
icon: <Share size={20} />,
|
||||
title: (
|
||||
<Text id="app.settings.server_pages.invites.title" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "bans",
|
||||
icon: <XSquare size={20} />,
|
||||
title: <Text id="app.settings.server_pages.bans.title" />,
|
||||
},
|
||||
{
|
||||
id: "roles",
|
||||
icon: <ListCheck size={20} />,
|
||||
title: <Text id="app.settings.server_pages.roles.title" />,
|
||||
hideTitle: true,
|
||||
},
|
||||
]}
|
||||
children={[
|
||||
<Route path="/server/:server/settings/members">
|
||||
<RequiresOnline>
|
||||
<Members server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/server/:server/settings/invites">
|
||||
<RequiresOnline>
|
||||
<Invites server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/server/:server/settings/bans">
|
||||
<RequiresOnline>
|
||||
<Bans server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/server/:server/settings/roles">
|
||||
<RequiresOnline>
|
||||
<Roles server={server} />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/">
|
||||
<Overview server={server} />
|
||||
</Route>,
|
||||
]}
|
||||
category="server_pages"
|
||||
switchPage={switchPage}
|
||||
defaultPage="overview"
|
||||
showExitButton
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Gitlab } from "@styled-icons/boxicons-logos";
|
||||
import {
|
||||
Sync as SyncIcon,
|
||||
Globe,
|
||||
LogOut,
|
||||
Sync as SyncIcon,
|
||||
Globe,
|
||||
LogOut,
|
||||
} from "@styled-icons/boxicons-regular";
|
||||
import {
|
||||
Bell,
|
||||
Palette,
|
||||
Coffee,
|
||||
IdCard,
|
||||
CheckShield,
|
||||
Flask,
|
||||
User,
|
||||
Megaphone,
|
||||
Bell,
|
||||
Palette,
|
||||
Coffee,
|
||||
IdCard,
|
||||
CheckShield,
|
||||
Flask,
|
||||
User,
|
||||
Megaphone,
|
||||
} from "@styled-icons/boxicons-solid";
|
||||
import { Route, useHistory } from "react-router-dom";
|
||||
import { LIBRARY_VERSION } from "revolt.js";
|
||||
@@ -23,8 +23,8 @@ import { useContext } from "preact/hooks";
|
||||
|
||||
import RequiresOnline from "../../context/revoltjs/RequiresOnline";
|
||||
import {
|
||||
AppContext,
|
||||
OperationsContext,
|
||||
AppContext,
|
||||
OperationsContext,
|
||||
} from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import LineDivider from "../../components/ui/LineDivider";
|
||||
@@ -44,159 +44,159 @@ import { Sessions } from "./panes/Sessions";
|
||||
import { Sync } from "./panes/Sync";
|
||||
|
||||
export default function Settings() {
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
const operations = useContext(OperationsContext);
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
const operations = useContext(OperationsContext);
|
||||
|
||||
function switchPage(to?: string) {
|
||||
if (to) {
|
||||
history.replace(`/settings/${to}`);
|
||||
} else {
|
||||
history.replace(`/settings`);
|
||||
}
|
||||
}
|
||||
function switchPage(to?: string) {
|
||||
if (to) {
|
||||
history.replace(`/settings/${to}`);
|
||||
} else {
|
||||
history.replace(`/settings`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericSettings
|
||||
pages={[
|
||||
{
|
||||
category: (
|
||||
<Text id="app.settings.categories.user_settings" />
|
||||
),
|
||||
id: "account",
|
||||
icon: <User size={20} />,
|
||||
title: <Text id="app.settings.pages.account.title" />,
|
||||
},
|
||||
{
|
||||
id: "profile",
|
||||
icon: <IdCard size={20} />,
|
||||
title: <Text id="app.settings.pages.profile.title" />,
|
||||
},
|
||||
{
|
||||
id: "sessions",
|
||||
icon: <CheckShield size={20} />,
|
||||
title: <Text id="app.settings.pages.sessions.title" />,
|
||||
},
|
||||
{
|
||||
category: (
|
||||
<Text id="app.settings.categories.client_settings" />
|
||||
),
|
||||
id: "appearance",
|
||||
icon: <Palette size={20} />,
|
||||
title: <Text id="app.settings.pages.appearance.title" />,
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
icon: <Bell size={20} />,
|
||||
title: <Text id="app.settings.pages.notifications.title" />,
|
||||
},
|
||||
{
|
||||
id: "language",
|
||||
icon: <Globe size={20} />,
|
||||
title: <Text id="app.settings.pages.language.title" />,
|
||||
},
|
||||
{
|
||||
id: "sync",
|
||||
icon: <SyncIcon size={20} />,
|
||||
title: <Text id="app.settings.pages.sync.title" />,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
id: "experiments",
|
||||
icon: <Flask size={20} />,
|
||||
title: <Text id="app.settings.pages.experiments.title" />,
|
||||
},
|
||||
{
|
||||
id: "feedback",
|
||||
icon: <Megaphone size={20} />,
|
||||
title: <Text id="app.settings.pages.feedback.title" />,
|
||||
},
|
||||
]}
|
||||
children={[
|
||||
<Route path="/settings/profile">
|
||||
<Profile />
|
||||
</Route>,
|
||||
<Route path="/settings/sessions">
|
||||
<RequiresOnline>
|
||||
<Sessions />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/settings/appearance">
|
||||
<Appearance />
|
||||
</Route>,
|
||||
<Route path="/settings/notifications">
|
||||
<Notifications />
|
||||
</Route>,
|
||||
<Route path="/settings/language">
|
||||
<Languages />
|
||||
</Route>,
|
||||
<Route path="/settings/sync">
|
||||
<Sync />
|
||||
</Route>,
|
||||
<Route path="/settings/experiments">
|
||||
<ExperimentsPage />
|
||||
</Route>,
|
||||
<Route path="/settings/feedback">
|
||||
<Feedback />
|
||||
</Route>,
|
||||
<Route path="/">
|
||||
<Account />
|
||||
</Route>,
|
||||
]}
|
||||
defaultPage="account"
|
||||
switchPage={switchPage}
|
||||
category="pages"
|
||||
custom={[
|
||||
<a href="https://gitlab.insrt.uk/revolt" target="_blank">
|
||||
<ButtonItem compact>
|
||||
<Gitlab size={20} />
|
||||
<Text id="app.settings.pages.source_code" />
|
||||
</ButtonItem>
|
||||
</a>,
|
||||
<a href="https://ko-fi.com/insertish" target="_blank">
|
||||
<ButtonItem className={styles.donate} compact>
|
||||
<Coffee size={20} />
|
||||
<Text id="app.settings.pages.donate.title" />
|
||||
</ButtonItem>
|
||||
</a>,
|
||||
<LineDivider />,
|
||||
<ButtonItem
|
||||
onClick={() => operations.logout()}
|
||||
className={styles.logOut}
|
||||
compact>
|
||||
<LogOut size={20} />
|
||||
<Text id="app.settings.pages.logOut" />
|
||||
</ButtonItem>,
|
||||
<div className={styles.version}>
|
||||
<div>
|
||||
<span className={styles.revision}>
|
||||
<a
|
||||
href={`${REPO_URL}/${GIT_REVISION}`}
|
||||
target="_blank">
|
||||
{GIT_REVISION.substr(0, 7)}
|
||||
</a>
|
||||
{` `}
|
||||
<a
|
||||
href={
|
||||
GIT_BRANCH !== "DETACHED"
|
||||
? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}`
|
||||
: undefined
|
||||
}
|
||||
target="_blank">
|
||||
({GIT_BRANCH})
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
{GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "}
|
||||
{APP_VERSION}
|
||||
</span>
|
||||
<span>
|
||||
API: {client.configuration?.revolt ?? "N/A"}
|
||||
</span>
|
||||
<span>revolt.js: {LIBRARY_VERSION}</span>
|
||||
</div>
|
||||
</div>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<GenericSettings
|
||||
pages={[
|
||||
{
|
||||
category: (
|
||||
<Text id="app.settings.categories.user_settings" />
|
||||
),
|
||||
id: "account",
|
||||
icon: <User size={20} />,
|
||||
title: <Text id="app.settings.pages.account.title" />,
|
||||
},
|
||||
{
|
||||
id: "profile",
|
||||
icon: <IdCard size={20} />,
|
||||
title: <Text id="app.settings.pages.profile.title" />,
|
||||
},
|
||||
{
|
||||
id: "sessions",
|
||||
icon: <CheckShield size={20} />,
|
||||
title: <Text id="app.settings.pages.sessions.title" />,
|
||||
},
|
||||
{
|
||||
category: (
|
||||
<Text id="app.settings.categories.client_settings" />
|
||||
),
|
||||
id: "appearance",
|
||||
icon: <Palette size={20} />,
|
||||
title: <Text id="app.settings.pages.appearance.title" />,
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
icon: <Bell size={20} />,
|
||||
title: <Text id="app.settings.pages.notifications.title" />,
|
||||
},
|
||||
{
|
||||
id: "language",
|
||||
icon: <Globe size={20} />,
|
||||
title: <Text id="app.settings.pages.language.title" />,
|
||||
},
|
||||
{
|
||||
id: "sync",
|
||||
icon: <SyncIcon size={20} />,
|
||||
title: <Text id="app.settings.pages.sync.title" />,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
id: "experiments",
|
||||
icon: <Flask size={20} />,
|
||||
title: <Text id="app.settings.pages.experiments.title" />,
|
||||
},
|
||||
{
|
||||
id: "feedback",
|
||||
icon: <Megaphone size={20} />,
|
||||
title: <Text id="app.settings.pages.feedback.title" />,
|
||||
},
|
||||
]}
|
||||
children={[
|
||||
<Route path="/settings/profile">
|
||||
<Profile />
|
||||
</Route>,
|
||||
<Route path="/settings/sessions">
|
||||
<RequiresOnline>
|
||||
<Sessions />
|
||||
</RequiresOnline>
|
||||
</Route>,
|
||||
<Route path="/settings/appearance">
|
||||
<Appearance />
|
||||
</Route>,
|
||||
<Route path="/settings/notifications">
|
||||
<Notifications />
|
||||
</Route>,
|
||||
<Route path="/settings/language">
|
||||
<Languages />
|
||||
</Route>,
|
||||
<Route path="/settings/sync">
|
||||
<Sync />
|
||||
</Route>,
|
||||
<Route path="/settings/experiments">
|
||||
<ExperimentsPage />
|
||||
</Route>,
|
||||
<Route path="/settings/feedback">
|
||||
<Feedback />
|
||||
</Route>,
|
||||
<Route path="/">
|
||||
<Account />
|
||||
</Route>,
|
||||
]}
|
||||
defaultPage="account"
|
||||
switchPage={switchPage}
|
||||
category="pages"
|
||||
custom={[
|
||||
<a href="https://gitlab.insrt.uk/revolt" target="_blank">
|
||||
<ButtonItem compact>
|
||||
<Gitlab size={20} />
|
||||
<Text id="app.settings.pages.source_code" />
|
||||
</ButtonItem>
|
||||
</a>,
|
||||
<a href="https://ko-fi.com/insertish" target="_blank">
|
||||
<ButtonItem className={styles.donate} compact>
|
||||
<Coffee size={20} />
|
||||
<Text id="app.settings.pages.donate.title" />
|
||||
</ButtonItem>
|
||||
</a>,
|
||||
<LineDivider />,
|
||||
<ButtonItem
|
||||
onClick={() => operations.logout()}
|
||||
className={styles.logOut}
|
||||
compact>
|
||||
<LogOut size={20} />
|
||||
<Text id="app.settings.pages.logOut" />
|
||||
</ButtonItem>,
|
||||
<div className={styles.version}>
|
||||
<div>
|
||||
<span className={styles.revision}>
|
||||
<a
|
||||
href={`${REPO_URL}/${GIT_REVISION}`}
|
||||
target="_blank">
|
||||
{GIT_REVISION.substr(0, 7)}
|
||||
</a>
|
||||
{` `}
|
||||
<a
|
||||
href={
|
||||
GIT_BRANCH !== "DETACHED"
|
||||
? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}`
|
||||
: undefined
|
||||
}
|
||||
target="_blank">
|
||||
({GIT_BRANCH})
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
{GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "}
|
||||
{APP_VERSION}
|
||||
</span>
|
||||
<span>
|
||||
API: {client.configuration?.revolt ?? "N/A"}
|
||||
</span>
|
||||
<span>revolt.js: {LIBRARY_VERSION}</span>
|
||||
</div>
|
||||
</div>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,105 +13,105 @@ import Button from "../../../components/ui/Button";
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
|
||||
interface Props {
|
||||
channel:
|
||||
| Channels.GroupChannel
|
||||
| Channels.TextChannel
|
||||
| Channels.VoiceChannel;
|
||||
channel:
|
||||
| Channels.GroupChannel
|
||||
| Channels.TextChannel
|
||||
| Channels.VoiceChannel;
|
||||
}
|
||||
|
||||
export default function Overview({ channel }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const [name, setName] = useState(channel.name);
|
||||
const [description, setDescription] = useState(channel.description ?? "");
|
||||
const [name, setName] = useState(channel.name);
|
||||
const [description, setDescription] = useState(channel.description ?? "");
|
||||
|
||||
useEffect(() => setName(channel.name), [channel.name]);
|
||||
useEffect(
|
||||
() => setDescription(channel.description ?? ""),
|
||||
[channel.description],
|
||||
);
|
||||
useEffect(() => setName(channel.name), [channel.name]);
|
||||
useEffect(
|
||||
() => setDescription(channel.description ?? ""),
|
||||
[channel.description],
|
||||
);
|
||||
|
||||
const [changed, setChanged] = useState(false);
|
||||
function save() {
|
||||
let changes: any = {};
|
||||
if (name !== channel.name) changes.name = name;
|
||||
if (description !== channel.description)
|
||||
changes.description = description;
|
||||
const [changed, setChanged] = useState(false);
|
||||
function save() {
|
||||
let changes: any = {};
|
||||
if (name !== channel.name) changes.name = name;
|
||||
if (description !== channel.description)
|
||||
changes.description = description;
|
||||
|
||||
client.channels.edit(channel._id, changes);
|
||||
setChanged(false);
|
||||
}
|
||||
client.channels.edit(channel._id, changes);
|
||||
setChanged(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.overview}>
|
||||
<div className={styles.row}>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
fileType="icons"
|
||||
behaviour="upload"
|
||||
maxFileSize={2_500_000}
|
||||
onUpload={(icon) =>
|
||||
client.channels.edit(channel._id, { icon })
|
||||
}
|
||||
previewURL={client.channels.getIconURL(
|
||||
channel._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
)}
|
||||
remove={() =>
|
||||
client.channels.edit(channel._id, { remove: "Icon" })
|
||||
}
|
||||
defaultPreview={
|
||||
channel.channel_type === "Group"
|
||||
? "/assets/group.png"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className={styles.name}>
|
||||
<h3>
|
||||
{channel.channel_type === "Group" ? (
|
||||
<Text id="app.main.groups.name" />
|
||||
) : (
|
||||
<Text id="app.main.servers.channel_name" />
|
||||
)}
|
||||
</h3>
|
||||
<InputBox
|
||||
contrast
|
||||
value={name}
|
||||
maxLength={32}
|
||||
onChange={(e) => {
|
||||
setName(e.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.overview}>
|
||||
<div className={styles.row}>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
fileType="icons"
|
||||
behaviour="upload"
|
||||
maxFileSize={2_500_000}
|
||||
onUpload={(icon) =>
|
||||
client.channels.edit(channel._id, { icon })
|
||||
}
|
||||
previewURL={client.channels.getIconURL(
|
||||
channel._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
)}
|
||||
remove={() =>
|
||||
client.channels.edit(channel._id, { remove: "Icon" })
|
||||
}
|
||||
defaultPreview={
|
||||
channel.channel_type === "Group"
|
||||
? "/assets/group.png"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className={styles.name}>
|
||||
<h3>
|
||||
{channel.channel_type === "Group" ? (
|
||||
<Text id="app.main.groups.name" />
|
||||
) : (
|
||||
<Text id="app.main.servers.channel_name" />
|
||||
)}
|
||||
</h3>
|
||||
<InputBox
|
||||
contrast
|
||||
value={name}
|
||||
maxLength={32}
|
||||
onChange={(e) => {
|
||||
setName(e.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
{channel.channel_type === "Group" ? (
|
||||
<Text id="app.main.groups.description" />
|
||||
) : (
|
||||
<Text id="app.main.servers.channel_description" />
|
||||
)}
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={10}
|
||||
minHeight={60}
|
||||
maxLength={1024}
|
||||
value={description}
|
||||
placeholder={"Add a description..."}
|
||||
onChange={(ev) => {
|
||||
setDescription(ev.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
<p>
|
||||
<Button onClick={save} contrast disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
<h3>
|
||||
{channel.channel_type === "Group" ? (
|
||||
<Text id="app.main.groups.description" />
|
||||
) : (
|
||||
<Text id="app.main.servers.channel_description" />
|
||||
)}
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={10}
|
||||
minHeight={60}
|
||||
maxLength={1024}
|
||||
value={description}
|
||||
placeholder={"Add a description..."}
|
||||
onChange={(ev) => {
|
||||
setDescription(ev.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
<p>
|
||||
<Button onClick={save} contrast disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,100 +12,100 @@ import Tip from "../../../components/ui/Tip";
|
||||
|
||||
// ! FIXME: export from revolt.js
|
||||
const DEFAULT_PERMISSION_DM =
|
||||
ChannelPermission.View +
|
||||
ChannelPermission.SendMessage +
|
||||
ChannelPermission.ManageChannel +
|
||||
ChannelPermission.VoiceCall +
|
||||
ChannelPermission.InviteOthers +
|
||||
ChannelPermission.EmbedLinks +
|
||||
ChannelPermission.UploadFiles;
|
||||
ChannelPermission.View +
|
||||
ChannelPermission.SendMessage +
|
||||
ChannelPermission.ManageChannel +
|
||||
ChannelPermission.VoiceCall +
|
||||
ChannelPermission.InviteOthers +
|
||||
ChannelPermission.EmbedLinks +
|
||||
ChannelPermission.UploadFiles;
|
||||
|
||||
interface Props {
|
||||
channel:
|
||||
| Channels.GroupChannel
|
||||
| Channels.TextChannel
|
||||
| Channels.VoiceChannel;
|
||||
channel:
|
||||
| Channels.GroupChannel
|
||||
| Channels.TextChannel
|
||||
| Channels.VoiceChannel;
|
||||
}
|
||||
|
||||
// ! FIXME: bad code :)
|
||||
export default function Permissions({ channel }: Props) {
|
||||
const [selected, setSelected] = useState("default");
|
||||
const client = useContext(AppContext);
|
||||
const [selected, setSelected] = useState("default");
|
||||
const client = useContext(AppContext);
|
||||
|
||||
type R = { name: string; permissions: number };
|
||||
let roles: { [key: string]: R } = {};
|
||||
if (channel.channel_type !== "Group") {
|
||||
const server = useServer(channel.server);
|
||||
const a = server?.roles ?? {};
|
||||
for (let b of Object.keys(a)) {
|
||||
roles[b] = {
|
||||
name: a[b].name,
|
||||
permissions: a[b].permissions[1],
|
||||
};
|
||||
}
|
||||
}
|
||||
type R = { name: string; permissions: number };
|
||||
let roles: { [key: string]: R } = {};
|
||||
if (channel.channel_type !== "Group") {
|
||||
const server = useServer(channel.server);
|
||||
const a = server?.roles ?? {};
|
||||
for (let b of Object.keys(a)) {
|
||||
roles[b] = {
|
||||
name: a[b].name,
|
||||
permissions: a[b].permissions[1],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const keys = ["default", ...Object.keys(roles)];
|
||||
const keys = ["default", ...Object.keys(roles)];
|
||||
|
||||
const defaultRole = {
|
||||
name: "Default",
|
||||
permissions:
|
||||
(channel.channel_type === "Group"
|
||||
? channel.permissions
|
||||
: channel.default_permissions) ?? DEFAULT_PERMISSION_DM,
|
||||
};
|
||||
const selectedRole = selected === "default" ? defaultRole : roles[selected];
|
||||
const defaultRole = {
|
||||
name: "Default",
|
||||
permissions:
|
||||
(channel.channel_type === "Group"
|
||||
? channel.permissions
|
||||
: channel.default_permissions) ?? DEFAULT_PERMISSION_DM,
|
||||
};
|
||||
const selectedRole = selected === "default" ? defaultRole : roles[selected];
|
||||
|
||||
if (!selectedRole) {
|
||||
useEffect(() => setSelected("default"), []);
|
||||
return null;
|
||||
}
|
||||
if (!selectedRole) {
|
||||
useEffect(() => setSelected("default"), []);
|
||||
return null;
|
||||
}
|
||||
|
||||
const [p, setPerm] = useState(selectedRole.permissions >>> 0);
|
||||
const [p, setPerm] = useState(selectedRole.permissions >>> 0);
|
||||
|
||||
useEffect(() => {
|
||||
setPerm(selectedRole.permissions >>> 0);
|
||||
}, [selected, selectedRole.permissions]);
|
||||
useEffect(() => {
|
||||
setPerm(selectedRole.permissions >>> 0);
|
||||
}, [selected, selectedRole.permissions]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tip warning>This section is under construction.</Tip>
|
||||
<h2>select role</h2>
|
||||
{selected}
|
||||
{keys.map((id) => {
|
||||
let role: R = id === "default" ? defaultRole : roles[id];
|
||||
return (
|
||||
<div>
|
||||
<Tip warning>This section is under construction.</Tip>
|
||||
<h2>select role</h2>
|
||||
{selected}
|
||||
{keys.map((id) => {
|
||||
let role: R = id === "default" ? defaultRole : roles[id];
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={selected === id}
|
||||
onChange={(selected) => selected && setSelected(id)}>
|
||||
{role.name}
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
<h2>channel per??issions</h2>
|
||||
{Object.keys(ChannelPermission).map((perm) => {
|
||||
let value =
|
||||
ChannelPermission[perm as keyof typeof ChannelPermission];
|
||||
if (value & DEFAULT_PERMISSION_DM) {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={(p & value) > 0}
|
||||
onChange={(c) =>
|
||||
setPerm(c ? p | value : p ^ value)
|
||||
}>
|
||||
{perm}
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
})}
|
||||
<Button
|
||||
contrast
|
||||
onClick={() => {
|
||||
client.channels.setPermissions(channel._id, selected, p);
|
||||
}}>
|
||||
click here to save permissions for role
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Checkbox
|
||||
checked={selected === id}
|
||||
onChange={(selected) => selected && setSelected(id)}>
|
||||
{role.name}
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
<h2>channel per??issions</h2>
|
||||
{Object.keys(ChannelPermission).map((perm) => {
|
||||
let value =
|
||||
ChannelPermission[perm as keyof typeof ChannelPermission];
|
||||
if (value & DEFAULT_PERMISSION_DM) {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={(p & value) > 0}
|
||||
onChange={(c) =>
|
||||
setPerm(c ? p | value : p ^ value)
|
||||
}>
|
||||
{perm}
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
})}
|
||||
<Button
|
||||
contrast
|
||||
onClick={() => {
|
||||
client.channels.setPermissions(channel._id, selected, p);
|
||||
}}>
|
||||
click here to save permissions for role
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import {
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks";
|
||||
|
||||
@@ -20,87 +20,87 @@ import Overline from "../../../components/ui/Overline";
|
||||
import Tip from "../../../components/ui/Tip";
|
||||
|
||||
export function Account() {
|
||||
const { openScreen } = useIntermediate();
|
||||
const status = useContext(StatusContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const status = useContext(StatusContext);
|
||||
|
||||
const ctx = useForceUpdate();
|
||||
const user = useSelf(ctx);
|
||||
if (!user) return null;
|
||||
const ctx = useForceUpdate();
|
||||
const user = useSelf(ctx);
|
||||
if (!user) return null;
|
||||
|
||||
const [email, setEmail] = useState("...");
|
||||
const [profile, setProfile] = useState<undefined | Users.Profile>(
|
||||
undefined,
|
||||
);
|
||||
const history = useHistory();
|
||||
const [email, setEmail] = useState("...");
|
||||
const [profile, setProfile] = useState<undefined | Users.Profile>(
|
||||
undefined,
|
||||
);
|
||||
const history = useHistory();
|
||||
|
||||
function switchPage(to: string) {
|
||||
history.replace(`/settings/${to}`);
|
||||
}
|
||||
function switchPage(to: string) {
|
||||
history.replace(`/settings/${to}`);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (email === "..." && status === ClientStatus.ONLINE) {
|
||||
ctx.client
|
||||
.req("GET", "/auth/user")
|
||||
.then((account) => setEmail(account.email));
|
||||
}
|
||||
useEffect(() => {
|
||||
if (email === "..." && status === ClientStatus.ONLINE) {
|
||||
ctx.client
|
||||
.req("GET", "/auth/user")
|
||||
.then((account) => setEmail(account.email));
|
||||
}
|
||||
|
||||
if (profile === undefined && status === ClientStatus.ONLINE) {
|
||||
ctx.client.users
|
||||
.fetchProfile(user._id)
|
||||
.then((profile) => setProfile(profile ?? {}));
|
||||
}
|
||||
}, [status]);
|
||||
if (profile === undefined && status === ClientStatus.ONLINE) {
|
||||
ctx.client.users
|
||||
.fetchProfile(user._id)
|
||||
.then((profile) => setProfile(profile ?? {}));
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<div className={styles.user}>
|
||||
<div className={styles.banner}>
|
||||
<UserIcon
|
||||
className={styles.avatar}
|
||||
target={user}
|
||||
size={72}
|
||||
onClick={() => switchPage("profile")}
|
||||
/>
|
||||
<div className={styles.username}>@{user.username}</div>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
{(
|
||||
[
|
||||
["username", user.username, <At size={24} />],
|
||||
["email", email, <Envelope size={24} />],
|
||||
["password", "*****", <Key size={24} />],
|
||||
] as const
|
||||
).map(([field, value, icon]) => (
|
||||
<div>
|
||||
{icon}
|
||||
<div className={styles.detail}>
|
||||
<Overline>
|
||||
<Text id={`login.${field}`} />
|
||||
</Overline>
|
||||
<p>{value}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "modify_account",
|
||||
field: field,
|
||||
})
|
||||
}
|
||||
contrast>
|
||||
<Text id="app.settings.pages.account.change_field" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Tip>
|
||||
<span>
|
||||
<Text id="app.settings.tips.account.a" />
|
||||
</span>{" "}
|
||||
<a onClick={() => switchPage("profile")}>
|
||||
<Text id="app.settings.tips.account.b" />
|
||||
</a>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.user}>
|
||||
<div className={styles.banner}>
|
||||
<UserIcon
|
||||
className={styles.avatar}
|
||||
target={user}
|
||||
size={72}
|
||||
onClick={() => switchPage("profile")}
|
||||
/>
|
||||
<div className={styles.username}>@{user.username}</div>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
{(
|
||||
[
|
||||
["username", user.username, <At size={24} />],
|
||||
["email", email, <Envelope size={24} />],
|
||||
["password", "*****", <Key size={24} />],
|
||||
] as const
|
||||
).map(([field, value, icon]) => (
|
||||
<div>
|
||||
{icon}
|
||||
<div className={styles.detail}>
|
||||
<Overline>
|
||||
<Text id={`login.${field}`} />
|
||||
</Overline>
|
||||
<p>{value}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "modify_account",
|
||||
field: field,
|
||||
})
|
||||
}
|
||||
contrast>
|
||||
<Text id="app.settings.pages.account.change_field" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Tip>
|
||||
<span>
|
||||
<Text id="app.settings.tips.account.a" />
|
||||
</span>{" "}
|
||||
<a onClick={() => switchPage("profile")}>
|
||||
<Text id="app.settings.tips.account.b" />
|
||||
</a>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,15 +13,15 @@ import { connectState } from "../../../redux/connector";
|
||||
import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
|
||||
|
||||
import {
|
||||
DEFAULT_FONT,
|
||||
DEFAULT_MONO_FONT,
|
||||
FONTS,
|
||||
FONT_KEYS,
|
||||
MONOSCAPE_FONTS,
|
||||
MONOSCAPE_FONT_KEYS,
|
||||
Theme,
|
||||
ThemeContext,
|
||||
ThemeOptions,
|
||||
DEFAULT_FONT,
|
||||
DEFAULT_MONO_FONT,
|
||||
FONTS,
|
||||
FONT_KEYS,
|
||||
MONOSCAPE_FONTS,
|
||||
MONOSCAPE_FONT_KEYS,
|
||||
Theme,
|
||||
ThemeContext,
|
||||
ThemeOptions,
|
||||
} from "../../../context/Theme";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
@@ -39,92 +39,92 @@ import openmojiSVG from "../assets/openmoji_emoji.svg";
|
||||
import twemojiSVG from "../assets/twemoji_emoji.svg";
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
// ! FIXME: code needs to be rewritten to fix jittering
|
||||
export function Component(props: Props) {
|
||||
const theme = useContext(ThemeContext);
|
||||
const { writeClipboard, openScreen } = useIntermediate();
|
||||
const theme = useContext(ThemeContext);
|
||||
const { writeClipboard, openScreen } = useIntermediate();
|
||||
|
||||
function setTheme(theme: ThemeOptions) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME",
|
||||
theme,
|
||||
});
|
||||
}
|
||||
function setTheme(theme: ThemeOptions) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME",
|
||||
theme,
|
||||
});
|
||||
}
|
||||
|
||||
function pushOverride(custom: Partial<Theme>) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME_OVERRIDE",
|
||||
custom,
|
||||
});
|
||||
}
|
||||
function pushOverride(custom: Partial<Theme>) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME_OVERRIDE",
|
||||
custom,
|
||||
});
|
||||
}
|
||||
|
||||
function setAccent(accent: string) {
|
||||
setOverride({
|
||||
accent,
|
||||
"scrollbar-thumb": pSBC(-0.2, accent),
|
||||
});
|
||||
}
|
||||
function setAccent(accent: string) {
|
||||
setOverride({
|
||||
accent,
|
||||
"scrollbar-thumb": pSBC(-0.2, accent),
|
||||
});
|
||||
}
|
||||
|
||||
const emojiPack = props.settings.appearance?.emojiPack ?? "mutant";
|
||||
function setEmojiPack(emojiPack: EmojiPacks) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_APPEARANCE",
|
||||
options: {
|
||||
emojiPack,
|
||||
},
|
||||
});
|
||||
}
|
||||
const emojiPack = props.settings.appearance?.emojiPack ?? "mutant";
|
||||
function setEmojiPack(emojiPack: EmojiPacks) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_APPEARANCE",
|
||||
options: {
|
||||
emojiPack,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const setOverride = useCallback(debounce(pushOverride, 200), []) as (
|
||||
custom: Partial<Theme>,
|
||||
) => void;
|
||||
const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? "");
|
||||
const setOverride = useCallback(debounce(pushOverride, 200), []) as (
|
||||
custom: Partial<Theme>,
|
||||
) => void;
|
||||
const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? "");
|
||||
|
||||
useEffect(() => setOverride({ css }), [css]);
|
||||
useEffect(() => setOverride({ css }), [css]);
|
||||
|
||||
const selected = props.settings.theme?.preset ?? "dark";
|
||||
return (
|
||||
<div className={styles.appearance}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.theme" />
|
||||
</h3>
|
||||
<div className={styles.themes}>
|
||||
<div className={styles.theme}>
|
||||
<img
|
||||
src={lightSVG}
|
||||
data-active={selected === "light"}
|
||||
onClick={() =>
|
||||
selected !== "light" &&
|
||||
setTheme({ preset: "light" })
|
||||
}
|
||||
/>
|
||||
<h4>
|
||||
<Text id="app.settings.pages.appearance.color.light" />
|
||||
</h4>
|
||||
</div>
|
||||
<div className={styles.theme}>
|
||||
<img
|
||||
src={darkSVG}
|
||||
data-active={selected === "dark"}
|
||||
onClick={() =>
|
||||
selected !== "dark" && setTheme({ preset: "dark" })
|
||||
}
|
||||
/>
|
||||
<h4>
|
||||
<Text id="app.settings.pages.appearance.color.dark" />
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
const selected = props.settings.theme?.preset ?? "dark";
|
||||
return (
|
||||
<div className={styles.appearance}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.theme" />
|
||||
</h3>
|
||||
<div className={styles.themes}>
|
||||
<div className={styles.theme}>
|
||||
<img
|
||||
src={lightSVG}
|
||||
data-active={selected === "light"}
|
||||
onClick={() =>
|
||||
selected !== "light" &&
|
||||
setTheme({ preset: "light" })
|
||||
}
|
||||
/>
|
||||
<h4>
|
||||
<Text id="app.settings.pages.appearance.color.light" />
|
||||
</h4>
|
||||
</div>
|
||||
<div className={styles.theme}>
|
||||
<img
|
||||
src={darkSVG}
|
||||
data-active={selected === "dark"}
|
||||
onClick={() =>
|
||||
selected !== "dark" && setTheme({ preset: "dark" })
|
||||
}
|
||||
/>
|
||||
<h4>
|
||||
<Text id="app.settings.pages.appearance.color.dark" />
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.accent_selector" />
|
||||
</h3>
|
||||
<ColourSwatches value={theme.accent} onChange={setAccent} />
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.accent_selector" />
|
||||
</h3>
|
||||
<ColourSwatches value={theme.accent} onChange={setAccent} />
|
||||
|
||||
{/*<h3>
|
||||
{/*<h3>
|
||||
<Text id="app.settings.pages.appearance.message_display" />
|
||||
</h3>
|
||||
<div className={styles.display}>
|
||||
@@ -146,229 +146,229 @@ export function Component(props: Props) {
|
||||
</Radio>
|
||||
</div>*/}
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.font" />
|
||||
</h3>
|
||||
<ComboBox
|
||||
value={theme.font ?? DEFAULT_FONT}
|
||||
onChange={(e) =>
|
||||
setTheme({ custom: { font: e.currentTarget.value as any } })
|
||||
}>
|
||||
{FONT_KEYS.map((key) => (
|
||||
<option value={key}>
|
||||
{FONTS[key as keyof typeof FONTS].name}
|
||||
</option>
|
||||
))}
|
||||
</ComboBox>
|
||||
<p>
|
||||
<Checkbox
|
||||
checked={props.settings.theme?.ligatures === true}
|
||||
onChange={() =>
|
||||
setTheme({
|
||||
ligatures: !props.settings.theme?.ligatures,
|
||||
})
|
||||
}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.ligatures_desc" />
|
||||
}>
|
||||
<Text id="app.settings.pages.appearance.ligatures" />
|
||||
</Checkbox>
|
||||
</p>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.font" />
|
||||
</h3>
|
||||
<ComboBox
|
||||
value={theme.font ?? DEFAULT_FONT}
|
||||
onChange={(e) =>
|
||||
setTheme({ custom: { font: e.currentTarget.value as any } })
|
||||
}>
|
||||
{FONT_KEYS.map((key) => (
|
||||
<option value={key}>
|
||||
{FONTS[key as keyof typeof FONTS].name}
|
||||
</option>
|
||||
))}
|
||||
</ComboBox>
|
||||
<p>
|
||||
<Checkbox
|
||||
checked={props.settings.theme?.ligatures === true}
|
||||
onChange={() =>
|
||||
setTheme({
|
||||
ligatures: !props.settings.theme?.ligatures,
|
||||
})
|
||||
}
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.ligatures_desc" />
|
||||
}>
|
||||
<Text id="app.settings.pages.appearance.ligatures" />
|
||||
</Checkbox>
|
||||
</p>
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.emoji_pack" />
|
||||
</h3>
|
||||
<div className={styles.emojiPack}>
|
||||
<div className={styles.row}>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("mutant")}
|
||||
data-active={emojiPack === "mutant"}>
|
||||
<img src={mutantSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>
|
||||
Mutant Remix{" "}
|
||||
<a
|
||||
href="https://mutant.revolt.chat"
|
||||
target="_blank">
|
||||
(by Revolt)
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("twemoji")}
|
||||
data-active={emojiPack === "twemoji"}>
|
||||
<img src={twemojiSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>Twemoji</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("openmoji")}
|
||||
data-active={emojiPack === "openmoji"}>
|
||||
<img src={openmojiSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>Openmoji</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("noto")}
|
||||
data-active={emojiPack === "noto"}>
|
||||
<img src={notoSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>Noto Emoji</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.emoji_pack" />
|
||||
</h3>
|
||||
<div className={styles.emojiPack}>
|
||||
<div className={styles.row}>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("mutant")}
|
||||
data-active={emojiPack === "mutant"}>
|
||||
<img src={mutantSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>
|
||||
Mutant Remix{" "}
|
||||
<a
|
||||
href="https://mutant.revolt.chat"
|
||||
target="_blank">
|
||||
(by Revolt)
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("twemoji")}
|
||||
data-active={emojiPack === "twemoji"}>
|
||||
<img src={twemojiSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>Twemoji</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("openmoji")}
|
||||
data-active={emojiPack === "openmoji"}>
|
||||
<img src={openmojiSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>Openmoji</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("noto")}
|
||||
data-active={emojiPack === "noto"}>
|
||||
<img src={notoSVG} draggable={false} />
|
||||
</div>
|
||||
<h4>Noto Emoji</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollapsibleSection
|
||||
id="settings_advanced_appearance"
|
||||
defaultValue={false}
|
||||
summary={<Text id="app.settings.pages.appearance.advanced" />}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.overrides" />
|
||||
</h3>
|
||||
<div className={styles.actions}>
|
||||
<Button contrast onClick={() => setTheme({ custom: {} })}>
|
||||
<Text id="app.settings.pages.appearance.reset_overrides" />
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={() => writeClipboard(JSON.stringify(theme))}>
|
||||
<Text id="app.settings.pages.appearance.export_clipboard" />
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={async () => {
|
||||
const text = await navigator.clipboard.readText();
|
||||
setOverride(JSON.parse(text));
|
||||
}}>
|
||||
<Text id="app.settings.pages.appearance.import_clipboard" />
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={async () => {
|
||||
openScreen({
|
||||
id: "_input",
|
||||
question: (
|
||||
<Text id="app.settings.pages.appearance.import_theme" />
|
||||
),
|
||||
field: (
|
||||
<Text id="app.settings.pages.appearance.theme_data" />
|
||||
),
|
||||
callback: async (string) =>
|
||||
setOverride(JSON.parse(string)),
|
||||
});
|
||||
}}>
|
||||
<Text id="app.settings.pages.appearance.import_manual" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.overrides}>
|
||||
{(
|
||||
[
|
||||
"accent",
|
||||
"background",
|
||||
"foreground",
|
||||
"primary-background",
|
||||
"primary-header",
|
||||
"secondary-background",
|
||||
"secondary-foreground",
|
||||
"secondary-header",
|
||||
"tertiary-background",
|
||||
"tertiary-foreground",
|
||||
"block",
|
||||
"message-box",
|
||||
"mention",
|
||||
"scrollbar-thumb",
|
||||
"scrollbar-track",
|
||||
"status-online",
|
||||
"status-away",
|
||||
"status-busy",
|
||||
"status-streaming",
|
||||
"status-invisible",
|
||||
"success",
|
||||
"warning",
|
||||
"error",
|
||||
"hover",
|
||||
] as const
|
||||
).map((x) => (
|
||||
<div className={styles.entry} key={x}>
|
||||
<span>{x}</span>
|
||||
<div className={styles.override}>
|
||||
<div
|
||||
className={styles.picker}
|
||||
style={{ backgroundColor: theme[x] }}>
|
||||
<input
|
||||
type="color"
|
||||
value={theme[x]}
|
||||
onChange={(v) =>
|
||||
setOverride({
|
||||
[x]: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<InputBox
|
||||
className={styles.text}
|
||||
value={theme[x]}
|
||||
onChange={(y) =>
|
||||
setOverride({
|
||||
[x]: y.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<CollapsibleSection
|
||||
id="settings_advanced_appearance"
|
||||
defaultValue={false}
|
||||
summary={<Text id="app.settings.pages.appearance.advanced" />}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.overrides" />
|
||||
</h3>
|
||||
<div className={styles.actions}>
|
||||
<Button contrast onClick={() => setTheme({ custom: {} })}>
|
||||
<Text id="app.settings.pages.appearance.reset_overrides" />
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={() => writeClipboard(JSON.stringify(theme))}>
|
||||
<Text id="app.settings.pages.appearance.export_clipboard" />
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={async () => {
|
||||
const text = await navigator.clipboard.readText();
|
||||
setOverride(JSON.parse(text));
|
||||
}}>
|
||||
<Text id="app.settings.pages.appearance.import_clipboard" />
|
||||
</Button>
|
||||
<Button
|
||||
contrast
|
||||
onClick={async () => {
|
||||
openScreen({
|
||||
id: "_input",
|
||||
question: (
|
||||
<Text id="app.settings.pages.appearance.import_theme" />
|
||||
),
|
||||
field: (
|
||||
<Text id="app.settings.pages.appearance.theme_data" />
|
||||
),
|
||||
callback: async (string) =>
|
||||
setOverride(JSON.parse(string)),
|
||||
});
|
||||
}}>
|
||||
<Text id="app.settings.pages.appearance.import_manual" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.overrides}>
|
||||
{(
|
||||
[
|
||||
"accent",
|
||||
"background",
|
||||
"foreground",
|
||||
"primary-background",
|
||||
"primary-header",
|
||||
"secondary-background",
|
||||
"secondary-foreground",
|
||||
"secondary-header",
|
||||
"tertiary-background",
|
||||
"tertiary-foreground",
|
||||
"block",
|
||||
"message-box",
|
||||
"mention",
|
||||
"scrollbar-thumb",
|
||||
"scrollbar-track",
|
||||
"status-online",
|
||||
"status-away",
|
||||
"status-busy",
|
||||
"status-streaming",
|
||||
"status-invisible",
|
||||
"success",
|
||||
"warning",
|
||||
"error",
|
||||
"hover",
|
||||
] as const
|
||||
).map((x) => (
|
||||
<div className={styles.entry} key={x}>
|
||||
<span>{x}</span>
|
||||
<div className={styles.override}>
|
||||
<div
|
||||
className={styles.picker}
|
||||
style={{ backgroundColor: theme[x] }}>
|
||||
<input
|
||||
type="color"
|
||||
value={theme[x]}
|
||||
onChange={(v) =>
|
||||
setOverride({
|
||||
[x]: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<InputBox
|
||||
className={styles.text}
|
||||
value={theme[x]}
|
||||
onChange={(y) =>
|
||||
setOverride({
|
||||
[x]: y.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.mono_font" />
|
||||
</h3>
|
||||
<ComboBox
|
||||
value={theme.monoscapeFont ?? DEFAULT_MONO_FONT}
|
||||
onChange={(e) =>
|
||||
setTheme({
|
||||
custom: {
|
||||
monoscapeFont: e.currentTarget.value as any,
|
||||
},
|
||||
})
|
||||
}>
|
||||
{MONOSCAPE_FONT_KEYS.map((key) => (
|
||||
<option value={key}>
|
||||
{
|
||||
MONOSCAPE_FONTS[
|
||||
key as keyof typeof MONOSCAPE_FONTS
|
||||
].name
|
||||
}
|
||||
</option>
|
||||
))}
|
||||
</ComboBox>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.mono_font" />
|
||||
</h3>
|
||||
<ComboBox
|
||||
value={theme.monoscapeFont ?? DEFAULT_MONO_FONT}
|
||||
onChange={(e) =>
|
||||
setTheme({
|
||||
custom: {
|
||||
monoscapeFont: e.currentTarget.value as any,
|
||||
},
|
||||
})
|
||||
}>
|
||||
{MONOSCAPE_FONT_KEYS.map((key) => (
|
||||
<option value={key}>
|
||||
{
|
||||
MONOSCAPE_FONTS[
|
||||
key as keyof typeof MONOSCAPE_FONTS
|
||||
].name
|
||||
}
|
||||
</option>
|
||||
))}
|
||||
</ComboBox>
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.custom_css" />
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={20}
|
||||
minHeight={480}
|
||||
code
|
||||
value={css}
|
||||
onChange={(ev) => setCSS(ev.currentTarget.value)}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
);
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.custom_css" />
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={20}
|
||||
minHeight={480}
|
||||
code
|
||||
value={css}
|
||||
onChange={(ev) => setCSS(ev.currentTarget.value)}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Appearance = connectState(Component, (state) => {
|
||||
return {
|
||||
settings: state.settings,
|
||||
};
|
||||
return {
|
||||
settings: state.settings,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,52 +4,52 @@ import { Text } from "preact-i18n";
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import {
|
||||
AVAILABLE_EXPERIMENTS,
|
||||
ExperimentOptions,
|
||||
AVAILABLE_EXPERIMENTS,
|
||||
ExperimentOptions,
|
||||
} from "../../../redux/reducers/experiments";
|
||||
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
|
||||
interface Props {
|
||||
options?: ExperimentOptions;
|
||||
options?: ExperimentOptions;
|
||||
}
|
||||
|
||||
export function Component(props: Props) {
|
||||
return (
|
||||
<div className={styles.experiments}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.experiments.features" />
|
||||
</h3>
|
||||
{AVAILABLE_EXPERIMENTS.map((key) => (
|
||||
<Checkbox
|
||||
checked={(props.options?.enabled ?? []).indexOf(key) > -1}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: enabled
|
||||
? "EXPERIMENTS_ENABLE"
|
||||
: "EXPERIMENTS_DISABLE",
|
||||
key,
|
||||
})
|
||||
}>
|
||||
<Text id={`app.settings.pages.experiments.titles.${key}`} />
|
||||
<p>
|
||||
<Text
|
||||
id={`app.settings.pages.experiments.descriptions.${key}`}
|
||||
/>
|
||||
</p>
|
||||
</Checkbox>
|
||||
))}
|
||||
{AVAILABLE_EXPERIMENTS.length === 0 && (
|
||||
<div className={styles.empty}>
|
||||
<Text id="app.settings.pages.experiments.not_available" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.experiments}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.experiments.features" />
|
||||
</h3>
|
||||
{AVAILABLE_EXPERIMENTS.map((key) => (
|
||||
<Checkbox
|
||||
checked={(props.options?.enabled ?? []).indexOf(key) > -1}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: enabled
|
||||
? "EXPERIMENTS_ENABLE"
|
||||
: "EXPERIMENTS_DISABLE",
|
||||
key,
|
||||
})
|
||||
}>
|
||||
<Text id={`app.settings.pages.experiments.titles.${key}`} />
|
||||
<p>
|
||||
<Text
|
||||
id={`app.settings.pages.experiments.descriptions.${key}`}
|
||||
/>
|
||||
</p>
|
||||
</Checkbox>
|
||||
))}
|
||||
{AVAILABLE_EXPERIMENTS.length === 0 && (
|
||||
<div className={styles.empty}>
|
||||
<Text id="app.settings.pages.experiments.not_available" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ExperimentsPage = connectState(Component, (state) => {
|
||||
return {
|
||||
options: state.experiments,
|
||||
};
|
||||
return {
|
||||
options: state.experiments,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -10,101 +10,101 @@ import Radio from "../../../components/ui/Radio";
|
||||
import TextArea from "../../../components/ui/TextArea";
|
||||
|
||||
export function Feedback() {
|
||||
const user = useSelf();
|
||||
const [other, setOther] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [state, setState] = useState<"ready" | "sending" | "sent">("ready");
|
||||
const [checked, setChecked] = useState<
|
||||
"Bug" | "Feature Request" | "__other_option__"
|
||||
>("Bug");
|
||||
const user = useSelf();
|
||||
const [other, setOther] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [state, setState] = useState<"ready" | "sending" | "sent">("ready");
|
||||
const [checked, setChecked] = useState<
|
||||
"Bug" | "Feature Request" | "__other_option__"
|
||||
>("Bug");
|
||||
|
||||
async function onSubmit(ev: JSX.TargetedEvent<HTMLFormElement, Event>) {
|
||||
ev.preventDefault();
|
||||
setState("sending");
|
||||
async function onSubmit(ev: JSX.TargetedEvent<HTMLFormElement, Event>) {
|
||||
ev.preventDefault();
|
||||
setState("sending");
|
||||
|
||||
await fetch(`https://workers.revolt.chat/feedback`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
checked,
|
||||
other,
|
||||
description,
|
||||
name: user?.username ?? "Unknown User",
|
||||
}),
|
||||
mode: "no-cors",
|
||||
});
|
||||
await fetch(`https://workers.revolt.chat/feedback`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
checked,
|
||||
other,
|
||||
description,
|
||||
name: user?.username ?? "Unknown User",
|
||||
}),
|
||||
mode: "no-cors",
|
||||
});
|
||||
|
||||
setState("sent");
|
||||
setChecked("Bug");
|
||||
setDescription("");
|
||||
setOther("");
|
||||
}
|
||||
setState("sent");
|
||||
setChecked("Bug");
|
||||
setDescription("");
|
||||
setOther("");
|
||||
}
|
||||
|
||||
return (
|
||||
<form className={styles.feedback} onSubmit={onSubmit}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.feedback.report" />
|
||||
</h3>
|
||||
<div className={styles.options}>
|
||||
<Radio
|
||||
checked={checked === "Bug"}
|
||||
disabled={state === "sending"}
|
||||
onSelect={() => setChecked("Bug")}>
|
||||
<Text id="app.settings.pages.feedback.bug" />
|
||||
</Radio>
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={checked === "Feature Request"}
|
||||
onSelect={() => setChecked("Feature Request")}>
|
||||
<Text id="app.settings.pages.feedback.feature" />
|
||||
</Radio>
|
||||
{(location.hostname === "vite.revolt.chat" ||
|
||||
location.hostname === "local.revolt.chat") && (
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={other === "Revite"}
|
||||
onSelect={() => {
|
||||
setChecked("__other_option__");
|
||||
setOther("Revite");
|
||||
}}>
|
||||
Issues with Revite
|
||||
</Radio>
|
||||
)}
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={
|
||||
checked === "__other_option__" && other !== "Revite"
|
||||
}
|
||||
onSelect={() => setChecked("__other_option__")}>
|
||||
<Localizer>
|
||||
<InputBox
|
||||
value={other}
|
||||
disabled={state === "sending"}
|
||||
name="entry.1151440373.other_option_response"
|
||||
onChange={(e) => setOther(e.currentTarget.value)}
|
||||
placeholder={
|
||||
(
|
||||
<Text id="app.settings.pages.feedback.other" />
|
||||
) as any
|
||||
}
|
||||
/>
|
||||
</Localizer>
|
||||
</Radio>
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.feedback.describe" />
|
||||
</h3>
|
||||
<TextArea
|
||||
// maxRows={10}
|
||||
value={description}
|
||||
id="entry.685672624"
|
||||
disabled={state === "sending"}
|
||||
onChange={(ev) => setDescription(ev.currentTarget.value)}
|
||||
/>
|
||||
<p>
|
||||
<Button type="submit" contrast>
|
||||
<Text id="app.settings.pages.feedback.send" />
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
return (
|
||||
<form className={styles.feedback} onSubmit={onSubmit}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.feedback.report" />
|
||||
</h3>
|
||||
<div className={styles.options}>
|
||||
<Radio
|
||||
checked={checked === "Bug"}
|
||||
disabled={state === "sending"}
|
||||
onSelect={() => setChecked("Bug")}>
|
||||
<Text id="app.settings.pages.feedback.bug" />
|
||||
</Radio>
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={checked === "Feature Request"}
|
||||
onSelect={() => setChecked("Feature Request")}>
|
||||
<Text id="app.settings.pages.feedback.feature" />
|
||||
</Radio>
|
||||
{(location.hostname === "vite.revolt.chat" ||
|
||||
location.hostname === "local.revolt.chat") && (
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={other === "Revite"}
|
||||
onSelect={() => {
|
||||
setChecked("__other_option__");
|
||||
setOther("Revite");
|
||||
}}>
|
||||
Issues with Revite
|
||||
</Radio>
|
||||
)}
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={
|
||||
checked === "__other_option__" && other !== "Revite"
|
||||
}
|
||||
onSelect={() => setChecked("__other_option__")}>
|
||||
<Localizer>
|
||||
<InputBox
|
||||
value={other}
|
||||
disabled={state === "sending"}
|
||||
name="entry.1151440373.other_option_response"
|
||||
onChange={(e) => setOther(e.currentTarget.value)}
|
||||
placeholder={
|
||||
(
|
||||
<Text id="app.settings.pages.feedback.other" />
|
||||
) as any
|
||||
}
|
||||
/>
|
||||
</Localizer>
|
||||
</Radio>
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.feedback.describe" />
|
||||
</h3>
|
||||
<TextArea
|
||||
// maxRows={10}
|
||||
value={description}
|
||||
id="entry.685672624"
|
||||
disabled={state === "sending"}
|
||||
onChange={(ev) => setDescription(ev.currentTarget.value)}
|
||||
/>
|
||||
<p>
|
||||
<Button type="submit" contrast>
|
||||
<Text id="app.settings.pages.feedback.send" />
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
|
||||
import {
|
||||
Language,
|
||||
LanguageEntry,
|
||||
Languages as Langs,
|
||||
Language,
|
||||
LanguageEntry,
|
||||
Languages as Langs,
|
||||
} from "../../../context/Locale";
|
||||
|
||||
import Emoji from "../../../components/common/Emoji";
|
||||
@@ -15,77 +15,77 @@ import Checkbox from "../../../components/ui/Checkbox";
|
||||
import Tip from "../../../components/ui/Tip";
|
||||
|
||||
type Props = {
|
||||
locale: Language;
|
||||
locale: Language;
|
||||
};
|
||||
|
||||
type Key = [string, LanguageEntry];
|
||||
|
||||
function Entry({ entry: [x, lang], locale }: { entry: Key } & Props) {
|
||||
return (
|
||||
<Checkbox
|
||||
key={x}
|
||||
className={styles.entry}
|
||||
checked={locale === x}
|
||||
onChange={(v) => {
|
||||
if (v) {
|
||||
dispatch({
|
||||
type: "SET_LOCALE",
|
||||
locale: x as Language,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<div className={styles.flag}>
|
||||
<Emoji size={42} emoji={lang.emoji} />
|
||||
</div>
|
||||
<span className={styles.description}>{lang.display}</span>
|
||||
</Checkbox>
|
||||
);
|
||||
return (
|
||||
<Checkbox
|
||||
key={x}
|
||||
className={styles.entry}
|
||||
checked={locale === x}
|
||||
onChange={(v) => {
|
||||
if (v) {
|
||||
dispatch({
|
||||
type: "SET_LOCALE",
|
||||
locale: x as Language,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<div className={styles.flag}>
|
||||
<Emoji size={42} emoji={lang.emoji} />
|
||||
</div>
|
||||
<span className={styles.description}>{lang.display}</span>
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
|
||||
export function Component(props: Props) {
|
||||
const languages = Object.keys(Langs).map((x) => [
|
||||
x,
|
||||
Langs[x as keyof typeof Langs],
|
||||
]) as Key[];
|
||||
const languages = Object.keys(Langs).map((x) => [
|
||||
x,
|
||||
Langs[x as keyof typeof Langs],
|
||||
]) as Key[];
|
||||
|
||||
return (
|
||||
<div className={styles.languages}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.select" />
|
||||
</h3>
|
||||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => !lang.alt)
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.other" />
|
||||
</h3>
|
||||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => lang.alt)
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
</div>
|
||||
<Tip>
|
||||
<span>
|
||||
<Text id="app.settings.tips.languages.a" />
|
||||
</span>{" "}
|
||||
<a
|
||||
href="https://weblate.insrt.uk/engage/revolt/?utm_source=widget"
|
||||
target="_blank">
|
||||
<Text id="app.settings.tips.languages.b" />
|
||||
</a>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.languages}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.select" />
|
||||
</h3>
|
||||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => !lang.alt)
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.other" />
|
||||
</h3>
|
||||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => lang.alt)
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
</div>
|
||||
<Tip>
|
||||
<span>
|
||||
<Text id="app.settings.tips.languages.a" />
|
||||
</span>{" "}
|
||||
<a
|
||||
href="https://weblate.insrt.uk/engage/revolt/?utm_source=widget"
|
||||
target="_blank">
|
||||
<Text id="app.settings.tips.languages.b" />
|
||||
</a>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Languages = connectState(Component, (state) => {
|
||||
return {
|
||||
locale: state.locale,
|
||||
};
|
||||
return {
|
||||
locale: state.locale,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,9 +9,9 @@ import { urlBase64ToUint8Array } from "../../../lib/conversion";
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import {
|
||||
DEFAULT_SOUNDS,
|
||||
NotificationOptions,
|
||||
SoundOptions,
|
||||
DEFAULT_SOUNDS,
|
||||
NotificationOptions,
|
||||
SoundOptions,
|
||||
} from "../../../redux/reducers/settings";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
@@ -22,133 +22,133 @@ import Checkbox from "../../../components/ui/Checkbox";
|
||||
import { SOUNDS_ARRAY } from "../../../assets/sounds/Audio";
|
||||
|
||||
interface Props {
|
||||
options?: NotificationOptions;
|
||||
options?: NotificationOptions;
|
||||
}
|
||||
|
||||
export function Component({ options }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
|
||||
undefined,
|
||||
);
|
||||
const client = useContext(AppContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Load current state of pushManager.
|
||||
useEffect(() => {
|
||||
navigator.serviceWorker
|
||||
?.getRegistration()
|
||||
.then(async (registration) => {
|
||||
const sub = await registration?.pushManager?.getSubscription();
|
||||
setPushEnabled(sub !== null && sub !== undefined);
|
||||
});
|
||||
}, []);
|
||||
// Load current state of pushManager.
|
||||
useEffect(() => {
|
||||
navigator.serviceWorker
|
||||
?.getRegistration()
|
||||
.then(async (registration) => {
|
||||
const sub = await registration?.pushManager?.getSubscription();
|
||||
setPushEnabled(sub !== null && sub !== undefined);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const enabledSounds: SoundOptions = defaultsDeep(
|
||||
options?.sounds ?? {},
|
||||
DEFAULT_SOUNDS,
|
||||
);
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.notifications.push_notifications" />
|
||||
</h3>
|
||||
<Checkbox
|
||||
disabled={!("Notification" in window)}
|
||||
checked={options?.desktopEnabled ?? false}
|
||||
description={
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_desktop" />
|
||||
}
|
||||
onChange={async (desktopEnabled) => {
|
||||
if (desktopEnabled) {
|
||||
let permission = await Notification.requestPermission();
|
||||
if (permission !== "granted") {
|
||||
return openScreen({
|
||||
id: "error",
|
||||
error: "DeniedNotification",
|
||||
});
|
||||
}
|
||||
}
|
||||
const enabledSounds: SoundOptions = defaultsDeep(
|
||||
options?.sounds ?? {},
|
||||
DEFAULT_SOUNDS,
|
||||
);
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.notifications.push_notifications" />
|
||||
</h3>
|
||||
<Checkbox
|
||||
disabled={!("Notification" in window)}
|
||||
checked={options?.desktopEnabled ?? false}
|
||||
description={
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_desktop" />
|
||||
}
|
||||
onChange={async (desktopEnabled) => {
|
||||
if (desktopEnabled) {
|
||||
let permission = await Notification.requestPermission();
|
||||
if (permission !== "granted") {
|
||||
return openScreen({
|
||||
id: "error",
|
||||
error: "DeniedNotification",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: { desktopEnabled },
|
||||
});
|
||||
}}>
|
||||
<Text id="app.settings.pages.notifications.enable_desktop" />
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
disabled={typeof pushEnabled === "undefined"}
|
||||
checked={pushEnabled ?? false}
|
||||
description={
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_push" />
|
||||
}
|
||||
onChange={async (pushEnabled) => {
|
||||
try {
|
||||
const reg =
|
||||
await navigator.serviceWorker?.getRegistration();
|
||||
if (reg) {
|
||||
if (pushEnabled) {
|
||||
const sub = await reg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(
|
||||
client.configuration!.vapid,
|
||||
),
|
||||
});
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: { desktopEnabled },
|
||||
});
|
||||
}}>
|
||||
<Text id="app.settings.pages.notifications.enable_desktop" />
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
disabled={typeof pushEnabled === "undefined"}
|
||||
checked={pushEnabled ?? false}
|
||||
description={
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_push" />
|
||||
}
|
||||
onChange={async (pushEnabled) => {
|
||||
try {
|
||||
const reg =
|
||||
await navigator.serviceWorker?.getRegistration();
|
||||
if (reg) {
|
||||
if (pushEnabled) {
|
||||
const sub = await reg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(
|
||||
client.configuration!.vapid,
|
||||
),
|
||||
});
|
||||
|
||||
// tell the server we just subscribed
|
||||
const json = sub.toJSON();
|
||||
if (json.keys) {
|
||||
client.req("POST", "/push/subscribe", {
|
||||
endpoint: sub.endpoint,
|
||||
...(json.keys as {
|
||||
p256dh: string;
|
||||
auth: string;
|
||||
}),
|
||||
});
|
||||
setPushEnabled(true);
|
||||
}
|
||||
} else {
|
||||
const sub =
|
||||
await reg.pushManager.getSubscription();
|
||||
sub?.unsubscribe();
|
||||
setPushEnabled(false);
|
||||
// tell the server we just subscribed
|
||||
const json = sub.toJSON();
|
||||
if (json.keys) {
|
||||
client.req("POST", "/push/subscribe", {
|
||||
endpoint: sub.endpoint,
|
||||
...(json.keys as {
|
||||
p256dh: string;
|
||||
auth: string;
|
||||
}),
|
||||
});
|
||||
setPushEnabled(true);
|
||||
}
|
||||
} else {
|
||||
const sub =
|
||||
await reg.pushManager.getSubscription();
|
||||
sub?.unsubscribe();
|
||||
setPushEnabled(false);
|
||||
|
||||
client.req("POST", "/push/unsubscribe");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to enable push!", err);
|
||||
}
|
||||
}}>
|
||||
<Text id="app.settings.pages.notifications.enable_push" />
|
||||
</Checkbox>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.notifications.sounds" />
|
||||
</h3>
|
||||
{SOUNDS_ARRAY.map((key) => (
|
||||
<Checkbox
|
||||
checked={enabledSounds[key] ? true : false}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: {
|
||||
sounds: {
|
||||
...options?.sounds,
|
||||
[key]: enabled,
|
||||
},
|
||||
},
|
||||
})
|
||||
}>
|
||||
<Text
|
||||
id={`app.settings.pages.notifications.sound.${key}`}
|
||||
/>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
client.req("POST", "/push/unsubscribe");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to enable push!", err);
|
||||
}
|
||||
}}>
|
||||
<Text id="app.settings.pages.notifications.enable_push" />
|
||||
</Checkbox>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.notifications.sounds" />
|
||||
</h3>
|
||||
{SOUNDS_ARRAY.map((key) => (
|
||||
<Checkbox
|
||||
checked={enabledSounds[key] ? true : false}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: {
|
||||
sounds: {
|
||||
...options?.sounds,
|
||||
[key]: enabled,
|
||||
},
|
||||
},
|
||||
})
|
||||
}>
|
||||
<Text
|
||||
id={`app.settings.pages.notifications.sound.${key}`}
|
||||
/>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Notifications = connectState(Component, (state) => {
|
||||
return {
|
||||
options: state.settings.notification,
|
||||
};
|
||||
return {
|
||||
options: state.settings.notification,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,178 +9,178 @@ import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||
import { UserProfile } from "../../../context/intermediate/popovers/UserProfile";
|
||||
import { FileUploader } from "../../../context/revoltjs/FileUploads";
|
||||
import {
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks";
|
||||
|
||||
import AutoComplete, {
|
||||
useAutoComplete,
|
||||
useAutoComplete,
|
||||
} from "../../../components/common/AutoComplete";
|
||||
import Button from "../../../components/ui/Button";
|
||||
|
||||
export function Profile() {
|
||||
const { intl } = useContext(IntlContext);
|
||||
const status = useContext(StatusContext);
|
||||
const { intl } = useContext(IntlContext);
|
||||
const status = useContext(StatusContext);
|
||||
|
||||
const ctx = useForceUpdate();
|
||||
const user = useSelf();
|
||||
if (!user) return null;
|
||||
const ctx = useForceUpdate();
|
||||
const user = useSelf();
|
||||
if (!user) return null;
|
||||
|
||||
const [profile, setProfile] = useState<undefined | Users.Profile>(
|
||||
undefined,
|
||||
);
|
||||
const [profile, setProfile] = useState<undefined | Users.Profile>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// ! FIXME: temporary solution
|
||||
// ! we should just announce profile changes through WS
|
||||
function refreshProfile() {
|
||||
ctx.client.users
|
||||
.fetchProfile(user!._id)
|
||||
.then((profile) => setProfile(profile ?? {}));
|
||||
}
|
||||
// ! FIXME: temporary solution
|
||||
// ! we should just announce profile changes through WS
|
||||
function refreshProfile() {
|
||||
ctx.client.users
|
||||
.fetchProfile(user!._id)
|
||||
.then((profile) => setProfile(profile ?? {}));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (profile === undefined && status === ClientStatus.ONLINE) {
|
||||
refreshProfile();
|
||||
}
|
||||
}, [status]);
|
||||
useEffect(() => {
|
||||
if (profile === undefined && status === ClientStatus.ONLINE) {
|
||||
refreshProfile();
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
const [changed, setChanged] = useState(false);
|
||||
function setContent(content?: string) {
|
||||
setProfile({ ...profile, content });
|
||||
if (!changed) setChanged(true);
|
||||
}
|
||||
const [changed, setChanged] = useState(false);
|
||||
function setContent(content?: string) {
|
||||
setProfile({ ...profile, content });
|
||||
if (!changed) setChanged(true);
|
||||
}
|
||||
|
||||
const {
|
||||
onChange,
|
||||
onKeyUp,
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
...autoCompleteProps
|
||||
} = useAutoComplete(setContent, {
|
||||
users: { type: "all" },
|
||||
});
|
||||
const {
|
||||
onChange,
|
||||
onKeyUp,
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
...autoCompleteProps
|
||||
} = useAutoComplete(setContent, {
|
||||
users: { type: "all" },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.user}>
|
||||
<h3>
|
||||
<Text id="app.special.modals.actions.preview" />
|
||||
</h3>
|
||||
<div className={styles.preview}>
|
||||
<UserProfile
|
||||
user_id={user._id}
|
||||
dummy={true}
|
||||
dummyProfile={profile}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.pfp}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.profile.profile_picture" />
|
||||
</h3>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
fileType="avatars"
|
||||
behaviour="upload"
|
||||
maxFileSize={4_000_000}
|
||||
onUpload={(avatar) =>
|
||||
ctx.client.users.editUser({ avatar })
|
||||
}
|
||||
remove={() =>
|
||||
ctx.client.users.editUser({ remove: "Avatar" })
|
||||
}
|
||||
defaultPreview={ctx.client.users.getAvatarURL(
|
||||
user._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
)}
|
||||
previewURL={ctx.client.users.getAvatarURL(
|
||||
user._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
true,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.background}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.profile.custom_background" />
|
||||
</h3>
|
||||
<FileUploader
|
||||
height={92}
|
||||
style="banner"
|
||||
behaviour="upload"
|
||||
fileType="backgrounds"
|
||||
maxFileSize={6_000_000}
|
||||
onUpload={async (background) => {
|
||||
await ctx.client.users.editUser({
|
||||
profile: { background },
|
||||
});
|
||||
refreshProfile();
|
||||
}}
|
||||
remove={async () => {
|
||||
await ctx.client.users.editUser({
|
||||
remove: "ProfileBackground",
|
||||
});
|
||||
setProfile({ ...profile, background: undefined });
|
||||
}}
|
||||
previewURL={
|
||||
profile?.background
|
||||
? ctx.client.users.getBackgroundURL(
|
||||
profile,
|
||||
{ width: 1000 },
|
||||
true,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.profile.info" />
|
||||
</h3>
|
||||
<AutoComplete detached {...autoCompleteProps} />
|
||||
<TextAreaAutoSize
|
||||
maxRows={10}
|
||||
minHeight={200}
|
||||
maxLength={2000}
|
||||
value={profile?.content ?? ""}
|
||||
disabled={typeof profile === "undefined"}
|
||||
onChange={(ev) => {
|
||||
onChange(ev);
|
||||
setContent(ev.currentTarget.value);
|
||||
}}
|
||||
placeholder={translate(
|
||||
`app.settings.pages.profile.${
|
||||
typeof profile === "undefined"
|
||||
? "fetching"
|
||||
: "placeholder"
|
||||
}`,
|
||||
"",
|
||||
(intl as any).dictionary as Record<string, unknown>,
|
||||
)}
|
||||
onKeyUp={onKeyUp}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<p>
|
||||
<Button
|
||||
contrast
|
||||
onClick={() => {
|
||||
setChanged(false);
|
||||
ctx.client.users.editUser({
|
||||
profile: { content: profile?.content },
|
||||
});
|
||||
}}
|
||||
disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.user}>
|
||||
<h3>
|
||||
<Text id="app.special.modals.actions.preview" />
|
||||
</h3>
|
||||
<div className={styles.preview}>
|
||||
<UserProfile
|
||||
user_id={user._id}
|
||||
dummy={true}
|
||||
dummyProfile={profile}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.pfp}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.profile.profile_picture" />
|
||||
</h3>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
fileType="avatars"
|
||||
behaviour="upload"
|
||||
maxFileSize={4_000_000}
|
||||
onUpload={(avatar) =>
|
||||
ctx.client.users.editUser({ avatar })
|
||||
}
|
||||
remove={() =>
|
||||
ctx.client.users.editUser({ remove: "Avatar" })
|
||||
}
|
||||
defaultPreview={ctx.client.users.getAvatarURL(
|
||||
user._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
)}
|
||||
previewURL={ctx.client.users.getAvatarURL(
|
||||
user._id,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
true,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.background}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.profile.custom_background" />
|
||||
</h3>
|
||||
<FileUploader
|
||||
height={92}
|
||||
style="banner"
|
||||
behaviour="upload"
|
||||
fileType="backgrounds"
|
||||
maxFileSize={6_000_000}
|
||||
onUpload={async (background) => {
|
||||
await ctx.client.users.editUser({
|
||||
profile: { background },
|
||||
});
|
||||
refreshProfile();
|
||||
}}
|
||||
remove={async () => {
|
||||
await ctx.client.users.editUser({
|
||||
remove: "ProfileBackground",
|
||||
});
|
||||
setProfile({ ...profile, background: undefined });
|
||||
}}
|
||||
previewURL={
|
||||
profile?.background
|
||||
? ctx.client.users.getBackgroundURL(
|
||||
profile,
|
||||
{ width: 1000 },
|
||||
true,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.profile.info" />
|
||||
</h3>
|
||||
<AutoComplete detached {...autoCompleteProps} />
|
||||
<TextAreaAutoSize
|
||||
maxRows={10}
|
||||
minHeight={200}
|
||||
maxLength={2000}
|
||||
value={profile?.content ?? ""}
|
||||
disabled={typeof profile === "undefined"}
|
||||
onChange={(ev) => {
|
||||
onChange(ev);
|
||||
setContent(ev.currentTarget.value);
|
||||
}}
|
||||
placeholder={translate(
|
||||
`app.settings.pages.profile.${
|
||||
typeof profile === "undefined"
|
||||
? "fetching"
|
||||
: "placeholder"
|
||||
}`,
|
||||
"",
|
||||
(intl as any).dictionary as Record<string, unknown>,
|
||||
)}
|
||||
onKeyUp={onKeyUp}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<p>
|
||||
<Button
|
||||
contrast
|
||||
onClick={() => {
|
||||
setChanged(false);
|
||||
ctx.client.users.editUser({
|
||||
profile: { content: profile?.content },
|
||||
});
|
||||
}}
|
||||
disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { HelpCircle } from "@styled-icons/boxicons-regular";
|
||||
import {
|
||||
Android,
|
||||
Firefoxbrowser,
|
||||
Googlechrome,
|
||||
Ios,
|
||||
Linux,
|
||||
Macos,
|
||||
Microsoftedge,
|
||||
Safari,
|
||||
Windows,
|
||||
Android,
|
||||
Firefoxbrowser,
|
||||
Googlechrome,
|
||||
Ios,
|
||||
Linux,
|
||||
Macos,
|
||||
Microsoftedge,
|
||||
Safari,
|
||||
Windows,
|
||||
} from "@styled-icons/simple-icons";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
@@ -28,159 +28,159 @@ import Tip from "../../../components/ui/Tip";
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
friendly_name: string;
|
||||
id: string;
|
||||
friendly_name: string;
|
||||
}
|
||||
|
||||
export function Sessions() {
|
||||
const client = useContext(AppContext);
|
||||
const deviceId = client.session?.id;
|
||||
const client = useContext(AppContext);
|
||||
const deviceId = client.session?.id;
|
||||
|
||||
const [sessions, setSessions] = useState<Session[] | undefined>(undefined);
|
||||
const [attemptingDelete, setDelete] = useState<string[]>([]);
|
||||
const history = useHistory();
|
||||
const [sessions, setSessions] = useState<Session[] | undefined>(undefined);
|
||||
const [attemptingDelete, setDelete] = useState<string[]>([]);
|
||||
const history = useHistory();
|
||||
|
||||
function switchPage(to: string) {
|
||||
history.replace(`/settings/${to}`);
|
||||
}
|
||||
function switchPage(to: string) {
|
||||
history.replace(`/settings/${to}`);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
client.req("GET", "/auth/sessions").then((data) => {
|
||||
data.sort(
|
||||
(a, b) =>
|
||||
(b.id === deviceId ? 1 : 0) - (a.id === deviceId ? 1 : 0),
|
||||
);
|
||||
setSessions(data);
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
client.req("GET", "/auth/sessions").then((data) => {
|
||||
data.sort(
|
||||
(a, b) =>
|
||||
(b.id === deviceId ? 1 : 0) - (a.id === deviceId ? 1 : 0),
|
||||
);
|
||||
setSessions(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (typeof sessions === "undefined") {
|
||||
return (
|
||||
<div className={styles.loader}>
|
||||
<Preloader type="ring" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (typeof sessions === "undefined") {
|
||||
return (
|
||||
<div className={styles.loader}>
|
||||
<Preloader type="ring" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getIcon(session: Session) {
|
||||
const name = session.friendly_name;
|
||||
switch (true) {
|
||||
case /firefox/i.test(name):
|
||||
return <Firefoxbrowser />;
|
||||
case /chrome/i.test(name):
|
||||
return <Googlechrome />;
|
||||
case /safari/i.test(name):
|
||||
return <Safari />;
|
||||
case /edge/i.test(name):
|
||||
return <Microsoftedge />;
|
||||
default:
|
||||
return <HelpCircle />;
|
||||
}
|
||||
}
|
||||
function getIcon(session: Session) {
|
||||
const name = session.friendly_name;
|
||||
switch (true) {
|
||||
case /firefox/i.test(name):
|
||||
return <Firefoxbrowser />;
|
||||
case /chrome/i.test(name):
|
||||
return <Googlechrome />;
|
||||
case /safari/i.test(name):
|
||||
return <Safari />;
|
||||
case /edge/i.test(name):
|
||||
return <Microsoftedge />;
|
||||
default:
|
||||
return <HelpCircle />;
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemIcon(session: Session) {
|
||||
const name = session.friendly_name;
|
||||
switch (true) {
|
||||
case /linux/i.test(name):
|
||||
return <Linux />;
|
||||
case /android/i.test(name):
|
||||
return <Android />;
|
||||
case /mac.*os/i.test(name):
|
||||
return <Macos />;
|
||||
case /ios/i.test(name):
|
||||
return <Ios />;
|
||||
case /windows/i.test(name):
|
||||
return <Windows />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function getSystemIcon(session: Session) {
|
||||
const name = session.friendly_name;
|
||||
switch (true) {
|
||||
case /linux/i.test(name):
|
||||
return <Linux />;
|
||||
case /android/i.test(name):
|
||||
return <Android />;
|
||||
case /mac.*os/i.test(name):
|
||||
return <Macos />;
|
||||
case /ios/i.test(name):
|
||||
return <Ios />;
|
||||
case /windows/i.test(name):
|
||||
return <Windows />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const mapped = sessions.map((session) => {
|
||||
return {
|
||||
...session,
|
||||
timestamp: decodeTime(session.id),
|
||||
};
|
||||
});
|
||||
const mapped = sessions.map((session) => {
|
||||
return {
|
||||
...session,
|
||||
timestamp: decodeTime(session.id),
|
||||
};
|
||||
});
|
||||
|
||||
mapped.sort((a, b) => b.timestamp - a.timestamp);
|
||||
let id = mapped.findIndex((x) => x.id === deviceId);
|
||||
mapped.sort((a, b) => b.timestamp - a.timestamp);
|
||||
let id = mapped.findIndex((x) => x.id === deviceId);
|
||||
|
||||
const render = [
|
||||
mapped[id],
|
||||
...mapped.slice(0, id),
|
||||
...mapped.slice(id + 1, mapped.length),
|
||||
];
|
||||
const render = [
|
||||
mapped[id],
|
||||
...mapped.slice(0, id),
|
||||
...mapped.slice(id + 1, mapped.length),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.sessions}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.sessions.active_sessions" />
|
||||
</h3>
|
||||
{render.map((session) => (
|
||||
<div
|
||||
className={styles.entry}
|
||||
data-active={session.id === deviceId}
|
||||
data-deleting={attemptingDelete.indexOf(session.id) > -1}>
|
||||
{deviceId === session.id && (
|
||||
<span className={styles.label}>
|
||||
<Text id="app.settings.pages.sessions.this_device" />{" "}
|
||||
</span>
|
||||
)}
|
||||
<div className={styles.session}>
|
||||
<div className={styles.icon}>
|
||||
{getIcon(session)}
|
||||
<div>{getSystemIcon(session)}</div>
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<span className={styles.name}>
|
||||
{session.friendly_name}
|
||||
</span>
|
||||
<span className={styles.time}>
|
||||
<Text
|
||||
id="app.settings.pages.sessions.created"
|
||||
fields={{
|
||||
time_ago: dayjs(
|
||||
session.timestamp,
|
||||
).fromNow(),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{deviceId !== session.id && (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setDelete([
|
||||
...attemptingDelete,
|
||||
session.id,
|
||||
]);
|
||||
await client.req(
|
||||
"DELETE",
|
||||
`/auth/sessions/${session.id}` as "/auth/sessions",
|
||||
);
|
||||
setSessions(
|
||||
sessions?.filter(
|
||||
(x) => x.id !== session.id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
disabled={
|
||||
attemptingDelete.indexOf(session.id) > -1
|
||||
}>
|
||||
<Text id="app.settings.pages.logOut" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Tip>
|
||||
<span>
|
||||
<Text id="app.settings.tips.sessions.a" />
|
||||
</span>{" "}
|
||||
<a onClick={() => switchPage("account")}>
|
||||
<Text id="app.settings.tips.sessions.b" />
|
||||
</a>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.sessions}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.sessions.active_sessions" />
|
||||
</h3>
|
||||
{render.map((session) => (
|
||||
<div
|
||||
className={styles.entry}
|
||||
data-active={session.id === deviceId}
|
||||
data-deleting={attemptingDelete.indexOf(session.id) > -1}>
|
||||
{deviceId === session.id && (
|
||||
<span className={styles.label}>
|
||||
<Text id="app.settings.pages.sessions.this_device" />{" "}
|
||||
</span>
|
||||
)}
|
||||
<div className={styles.session}>
|
||||
<div className={styles.icon}>
|
||||
{getIcon(session)}
|
||||
<div>{getSystemIcon(session)}</div>
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<span className={styles.name}>
|
||||
{session.friendly_name}
|
||||
</span>
|
||||
<span className={styles.time}>
|
||||
<Text
|
||||
id="app.settings.pages.sessions.created"
|
||||
fields={{
|
||||
time_ago: dayjs(
|
||||
session.timestamp,
|
||||
).fromNow(),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{deviceId !== session.id && (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setDelete([
|
||||
...attemptingDelete,
|
||||
session.id,
|
||||
]);
|
||||
await client.req(
|
||||
"DELETE",
|
||||
`/auth/sessions/${session.id}` as "/auth/sessions",
|
||||
);
|
||||
setSessions(
|
||||
sessions?.filter(
|
||||
(x) => x.id !== session.id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
disabled={
|
||||
attemptingDelete.indexOf(session.id) > -1
|
||||
}>
|
||||
<Text id="app.settings.pages.logOut" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Tip>
|
||||
<span>
|
||||
<Text id="app.settings.tips.sessions.a" />
|
||||
</span>{" "}
|
||||
<a onClick={() => switchPage("account")}>
|
||||
<Text id="app.settings.tips.sessions.b" />
|
||||
</a>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,49 +8,49 @@ import { SyncKeys, SyncOptions } from "../../../redux/reducers/sync";
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
|
||||
interface Props {
|
||||
options?: SyncOptions;
|
||||
options?: SyncOptions;
|
||||
}
|
||||
|
||||
export function Component(props: Props) {
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.sync.categories" />
|
||||
</h3>
|
||||
{(
|
||||
[
|
||||
["appearance", "appearance.title"],
|
||||
["theme", "appearance.theme"],
|
||||
["locale", "language.title"],
|
||||
// notifications sync is always-on
|
||||
] as [SyncKeys, string][]
|
||||
).map(([key, title]) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
(props.options?.disabled ?? []).indexOf(key) === -1
|
||||
}
|
||||
description={
|
||||
<Text
|
||||
id={`app.settings.pages.sync.descriptions.${key}`}
|
||||
/>
|
||||
}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: enabled
|
||||
? "SYNC_ENABLE_KEY"
|
||||
: "SYNC_DISABLE_KEY",
|
||||
key,
|
||||
})
|
||||
}>
|
||||
<Text id={`app.settings.pages.${title}`} />
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.sync.categories" />
|
||||
</h3>
|
||||
{(
|
||||
[
|
||||
["appearance", "appearance.title"],
|
||||
["theme", "appearance.theme"],
|
||||
["locale", "language.title"],
|
||||
// notifications sync is always-on
|
||||
] as [SyncKeys, string][]
|
||||
).map(([key, title]) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
(props.options?.disabled ?? []).indexOf(key) === -1
|
||||
}
|
||||
description={
|
||||
<Text
|
||||
id={`app.settings.pages.sync.descriptions.${key}`}
|
||||
/>
|
||||
}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: enabled
|
||||
? "SYNC_ENABLE_KEY"
|
||||
: "SYNC_DISABLE_KEY",
|
||||
key,
|
||||
})
|
||||
}>
|
||||
<Text id={`app.settings.pages.${title}`} />
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Sync = connectState(Component, (state) => {
|
||||
return {
|
||||
options: state.sync,
|
||||
};
|
||||
return {
|
||||
options: state.sync,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,31 +7,31 @@ import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import Tip from "../../../components/ui/Tip";
|
||||
|
||||
interface Props {
|
||||
server: Servers.Server;
|
||||
server: Servers.Server;
|
||||
}
|
||||
|
||||
export function Bans({ server }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const [bans, setBans] = useState<Servers.Ban[] | undefined>(undefined);
|
||||
const client = useContext(AppContext);
|
||||
const [bans, setBans] = useState<Servers.Ban[] | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
client.servers.fetchBans(server._id).then((bans) => setBans(bans));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
client.servers.fetchBans(server._id).then((bans) => setBans(bans));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tip warning>This section is under construction.</Tip>
|
||||
{bans?.map((x) => (
|
||||
<div>
|
||||
{x._id.user}: {x.reason ?? "no reason"}{" "}
|
||||
<button
|
||||
onClick={() =>
|
||||
client.servers.unbanUser(server._id, x._id.user)
|
||||
}>
|
||||
unban
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<Tip warning>This section is under construction.</Tip>
|
||||
{bans?.map((x) => (
|
||||
<div>
|
||||
{x._id.user}: {x.reason ?? "no reason"}{" "}
|
||||
<button
|
||||
onClick={() =>
|
||||
client.servers.unbanUser(server._id, x._id.user)
|
||||
}>
|
||||
unban
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Text } from "preact-i18n";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import {
|
||||
useChannels,
|
||||
useForceUpdate,
|
||||
useUsers,
|
||||
useChannels,
|
||||
useForceUpdate,
|
||||
useUsers,
|
||||
} from "../../../context/revoltjs/hooks";
|
||||
import { getChannelName } from "../../../context/revoltjs/util";
|
||||
|
||||
@@ -17,70 +17,70 @@ import IconButton from "../../../components/ui/IconButton";
|
||||
import Preloader from "../../../components/ui/Preloader";
|
||||
|
||||
interface Props {
|
||||
server: Servers.Server;
|
||||
server: Servers.Server;
|
||||
}
|
||||
|
||||
export function Invites({ server }: Props) {
|
||||
const [invites, setInvites] = useState<
|
||||
InvitesNS.ServerInvite[] | undefined
|
||||
>(undefined);
|
||||
const [invites, setInvites] = useState<
|
||||
InvitesNS.ServerInvite[] | undefined
|
||||
>(undefined);
|
||||
|
||||
const ctx = useForceUpdate();
|
||||
const [deleting, setDelete] = useState<string[]>([]);
|
||||
const users = useUsers(invites?.map((x) => x.creator) ?? [], ctx);
|
||||
const channels = useChannels(invites?.map((x) => x.channel) ?? [], ctx);
|
||||
const ctx = useForceUpdate();
|
||||
const [deleting, setDelete] = useState<string[]>([]);
|
||||
const users = useUsers(invites?.map((x) => x.creator) ?? [], ctx);
|
||||
const channels = useChannels(invites?.map((x) => x.channel) ?? [], ctx);
|
||||
|
||||
useEffect(() => {
|
||||
ctx.client.servers
|
||||
.fetchInvites(server._id)
|
||||
.then((invites) => setInvites(invites));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
ctx.client.servers
|
||||
.fetchInvites(server._id)
|
||||
.then((invites) => setInvites(invites));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.invites}>
|
||||
<div className={styles.subtitle}>
|
||||
<span>Invite Code</span>
|
||||
<span>Invitor</span>
|
||||
<span>Channel</span>
|
||||
<span>Revoke</span>
|
||||
</div>
|
||||
{typeof invites === "undefined" && <Preloader type="ring" />}
|
||||
{invites?.map((invite) => {
|
||||
let creator = users.find((x) => x?._id === invite.creator);
|
||||
let channel = channels.find((x) => x?._id === invite.channel);
|
||||
return (
|
||||
<div className={styles.invites}>
|
||||
<div className={styles.subtitle}>
|
||||
<span>Invite Code</span>
|
||||
<span>Invitor</span>
|
||||
<span>Channel</span>
|
||||
<span>Revoke</span>
|
||||
</div>
|
||||
{typeof invites === "undefined" && <Preloader type="ring" />}
|
||||
{invites?.map((invite) => {
|
||||
let creator = users.find((x) => x?._id === invite.creator);
|
||||
let channel = channels.find((x) => x?._id === invite.channel);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.invite}
|
||||
data-deleting={deleting.indexOf(invite._id) > -1}>
|
||||
<code>{invite._id}</code>
|
||||
<span>
|
||||
<UserIcon target={creator} size={24} />{" "}
|
||||
{creator?.username ?? "unknown"}
|
||||
</span>
|
||||
<span>
|
||||
{channel && creator
|
||||
? getChannelName(ctx.client, channel, true)
|
||||
: "#unknown"}
|
||||
</span>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
setDelete([...deleting, invite._id]);
|
||||
return (
|
||||
<div
|
||||
className={styles.invite}
|
||||
data-deleting={deleting.indexOf(invite._id) > -1}>
|
||||
<code>{invite._id}</code>
|
||||
<span>
|
||||
<UserIcon target={creator} size={24} />{" "}
|
||||
{creator?.username ?? "unknown"}
|
||||
</span>
|
||||
<span>
|
||||
{channel && creator
|
||||
? getChannelName(ctx.client, channel, true)
|
||||
: "#unknown"}
|
||||
</span>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
setDelete([...deleting, invite._id]);
|
||||
|
||||
await ctx.client.deleteInvite(invite._id);
|
||||
await ctx.client.deleteInvite(invite._id);
|
||||
|
||||
setInvites(
|
||||
invites?.filter(
|
||||
(x) => x._id !== invite._id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
disabled={deleting.indexOf(invite._id) > -1}>
|
||||
<XCircle size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
setInvites(
|
||||
invites?.filter(
|
||||
(x) => x._id !== invite._id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
disabled={deleting.indexOf(invite._id) > -1}>
|
||||
<XCircle size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,39 +6,39 @@ import { useEffect, useState } from "preact/hooks";
|
||||
import { useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
|
||||
|
||||
interface Props {
|
||||
server: Servers.Server;
|
||||
server: Servers.Server;
|
||||
}
|
||||
|
||||
// ! FIXME: bad code :)
|
||||
export function Members({ server }: Props) {
|
||||
const [members, setMembers] = useState<Servers.Member[] | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [members, setMembers] = useState<Servers.Member[] | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const ctx = useForceUpdate();
|
||||
const users = useUsers(members?.map((x) => x._id.user) ?? [], ctx);
|
||||
const ctx = useForceUpdate();
|
||||
const users = useUsers(members?.map((x) => x._id.user) ?? [], ctx);
|
||||
|
||||
useEffect(() => {
|
||||
ctx.client.servers.members
|
||||
.fetchMembers(server._id)
|
||||
.then((members) => setMembers(members));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
ctx.client.servers.members
|
||||
.fetchMembers(server._id)
|
||||
.then((members) => setMembers(members));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.members}>
|
||||
<div className={styles.subtitle}>
|
||||
{members?.length ?? 0} Members
|
||||
</div>
|
||||
{members &&
|
||||
members.length > 0 &&
|
||||
users?.map(
|
||||
(x) =>
|
||||
x && (
|
||||
<div className={styles.member}>
|
||||
<div>@{x.username}</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.members}>
|
||||
<div className={styles.subtitle}>
|
||||
{members?.length ?? 0} Members
|
||||
</div>
|
||||
{members &&
|
||||
members.length > 0 &&
|
||||
users?.map(
|
||||
(x) =>
|
||||
x && (
|
||||
<div className={styles.member}>
|
||||
<div>@{x.username}</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,178 +16,178 @@ import ComboBox from "../../../components/ui/ComboBox";
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
|
||||
interface Props {
|
||||
server: Servers.Server;
|
||||
server: Servers.Server;
|
||||
}
|
||||
|
||||
export function Overview({ server }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const [name, setName] = useState(server.name);
|
||||
const [description, setDescription] = useState(server.description ?? "");
|
||||
const [systemMessages, setSystemMessages] = useState(
|
||||
server.system_messages,
|
||||
);
|
||||
const [name, setName] = useState(server.name);
|
||||
const [description, setDescription] = useState(server.description ?? "");
|
||||
const [systemMessages, setSystemMessages] = useState(
|
||||
server.system_messages,
|
||||
);
|
||||
|
||||
useEffect(() => setName(server.name), [server.name]);
|
||||
useEffect(
|
||||
() => setDescription(server.description ?? ""),
|
||||
[server.description],
|
||||
);
|
||||
useEffect(
|
||||
() => setSystemMessages(server.system_messages),
|
||||
[server.system_messages],
|
||||
);
|
||||
useEffect(() => setName(server.name), [server.name]);
|
||||
useEffect(
|
||||
() => setDescription(server.description ?? ""),
|
||||
[server.description],
|
||||
);
|
||||
useEffect(
|
||||
() => setSystemMessages(server.system_messages),
|
||||
[server.system_messages],
|
||||
);
|
||||
|
||||
const [changed, setChanged] = useState(false);
|
||||
function save() {
|
||||
let changes: Partial<
|
||||
Pick<Servers.Server, "name" | "description" | "system_messages">
|
||||
> = {};
|
||||
if (name !== server.name) changes.name = name;
|
||||
if (description !== server.description)
|
||||
changes.description = description;
|
||||
if (!isEqual(systemMessages, server.system_messages))
|
||||
changes.system_messages = systemMessages;
|
||||
const [changed, setChanged] = useState(false);
|
||||
function save() {
|
||||
let changes: Partial<
|
||||
Pick<Servers.Server, "name" | "description" | "system_messages">
|
||||
> = {};
|
||||
if (name !== server.name) changes.name = name;
|
||||
if (description !== server.description)
|
||||
changes.description = description;
|
||||
if (!isEqual(systemMessages, server.system_messages))
|
||||
changes.system_messages = systemMessages;
|
||||
|
||||
client.servers.edit(server._id, changes);
|
||||
setChanged(false);
|
||||
}
|
||||
client.servers.edit(server._id, changes);
|
||||
setChanged(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.overview}>
|
||||
<div className={styles.row}>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
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" })
|
||||
}
|
||||
/>
|
||||
<div className={styles.name}>
|
||||
<h3>
|
||||
<Text id="app.main.servers.name" />
|
||||
</h3>
|
||||
<InputBox
|
||||
contrast
|
||||
value={name}
|
||||
maxLength={32}
|
||||
onChange={(e) => {
|
||||
setName(e.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.overview}>
|
||||
<div className={styles.row}>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
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" })
|
||||
}
|
||||
/>
|
||||
<div className={styles.name}>
|
||||
<h3>
|
||||
<Text id="app.main.servers.name" />
|
||||
</h3>
|
||||
<InputBox
|
||||
contrast
|
||||
value={name}
|
||||
maxLength={32}
|
||||
onChange={(e) => {
|
||||
setName(e.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<Text id="app.main.servers.description" />
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={10}
|
||||
minHeight={60}
|
||||
maxLength={1024}
|
||||
value={description}
|
||||
placeholder={"Add a topic..."}
|
||||
onChange={(ev) => {
|
||||
setDescription(ev.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
<h3>
|
||||
<Text id="app.main.servers.description" />
|
||||
</h3>
|
||||
<TextAreaAutoSize
|
||||
maxRows={10}
|
||||
minHeight={60}
|
||||
maxLength={1024}
|
||||
value={description}
|
||||
placeholder={"Add a topic..."}
|
||||
onChange={(ev) => {
|
||||
setDescription(ev.currentTarget.value);
|
||||
if (!changed) setChanged(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<h3>
|
||||
<Text id="app.main.servers.custom_banner" />
|
||||
</h3>
|
||||
<FileUploader
|
||||
height={160}
|
||||
style="banner"
|
||||
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" })
|
||||
}
|
||||
/>
|
||||
<h3>
|
||||
<Text id="app.main.servers.custom_banner" />
|
||||
</h3>
|
||||
<FileUploader
|
||||
height={160}
|
||||
style="banner"
|
||||
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" })
|
||||
}
|
||||
/>
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.server_pages.overview.system_messages" />
|
||||
</h3>
|
||||
{[
|
||||
["User Joined", "user_joined"],
|
||||
["User Left", "user_left"],
|
||||
["User Kicked", "user_kicked"],
|
||||
["User Banned", "user_banned"],
|
||||
].map(([i18n, key]) => (
|
||||
// ! FIXME: temporary code just so we can expose the options
|
||||
<p
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<span style={{ flexShrink: "0", flex: `25%` }}>{i18n}</span>
|
||||
<ComboBox
|
||||
value={
|
||||
systemMessages?.[
|
||||
key as keyof typeof systemMessages
|
||||
] ?? "disabled"
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (!changed) setChanged(true);
|
||||
const v = e.currentTarget.value;
|
||||
if (v === "disabled") {
|
||||
const {
|
||||
[key as keyof typeof systemMessages]: _,
|
||||
...other
|
||||
} = systemMessages;
|
||||
setSystemMessages(other);
|
||||
} else {
|
||||
setSystemMessages({
|
||||
...systemMessages,
|
||||
[key]: v,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<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)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</ComboBox>
|
||||
</p>
|
||||
))}
|
||||
<h3>
|
||||
<Text id="app.settings.server_pages.overview.system_messages" />
|
||||
</h3>
|
||||
{[
|
||||
["User Joined", "user_joined"],
|
||||
["User Left", "user_left"],
|
||||
["User Kicked", "user_kicked"],
|
||||
["User Banned", "user_banned"],
|
||||
].map(([i18n, key]) => (
|
||||
// ! FIXME: temporary code just so we can expose the options
|
||||
<p
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<span style={{ flexShrink: "0", flex: `25%` }}>{i18n}</span>
|
||||
<ComboBox
|
||||
value={
|
||||
systemMessages?.[
|
||||
key as keyof typeof systemMessages
|
||||
] ?? "disabled"
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (!changed) setChanged(true);
|
||||
const v = e.currentTarget.value;
|
||||
if (v === "disabled") {
|
||||
const {
|
||||
[key as keyof typeof systemMessages]: _,
|
||||
...other
|
||||
} = systemMessages;
|
||||
setSystemMessages(other);
|
||||
} else {
|
||||
setSystemMessages({
|
||||
...systemMessages,
|
||||
[key]: v,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<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)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</ComboBox>
|
||||
</p>
|
||||
))}
|
||||
|
||||
<p>
|
||||
<Button onClick={save} contrast disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
<p>
|
||||
<Button onClick={save} contrast disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Plus } from "@styled-icons/boxicons-regular";
|
||||
import isEqual from "lodash.isequal";
|
||||
import { Servers } from "revolt.js/dist/api/objects";
|
||||
import {
|
||||
ChannelPermission,
|
||||
ServerPermission,
|
||||
ChannelPermission,
|
||||
ServerPermission,
|
||||
} from "revolt.js/dist/api/permissions";
|
||||
|
||||
import styles from "./Panes.module.scss";
|
||||
@@ -23,157 +23,157 @@ import Tip from "../../../components/ui/Tip";
|
||||
import ButtonItem from "../../../components/navigation/items/ButtonItem";
|
||||
|
||||
interface Props {
|
||||
server: Servers.Server;
|
||||
server: Servers.Server;
|
||||
}
|
||||
|
||||
const I32ToU32 = (arr: number[]) => arr.map((x) => x >>> 0);
|
||||
|
||||
// ! FIXME: bad code :)
|
||||
export function Roles({ server }: Props) {
|
||||
const [role, setRole] = useState("default");
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
const roles = server.roles ?? {};
|
||||
const [role, setRole] = useState("default");
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
const roles = server.roles ?? {};
|
||||
|
||||
if (role !== "default" && typeof roles[role] === "undefined") {
|
||||
useEffect(() => setRole("default"));
|
||||
return null;
|
||||
}
|
||||
if (role !== "default" && typeof roles[role] === "undefined") {
|
||||
useEffect(() => setRole("default"));
|
||||
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 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 deleteRole = () => {
|
||||
setRole("default");
|
||||
client.servers.deleteRole(server._id, role);
|
||||
};
|
||||
const modified = !isEqual(perm, v(role));
|
||||
const save = () =>
|
||||
client.servers.setPermissions(server._id, role, {
|
||||
server: perm[0],
|
||||
channel: perm[1],
|
||||
});
|
||||
const deleteRole = () => {
|
||||
setRole("default");
|
||||
client.servers.deleteRole(server._id, role);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.roles}>
|
||||
<div className={styles.list}>
|
||||
<div className={styles.title}>
|
||||
<h1>
|
||||
<Text id="app.settings.server_pages.roles.title" />
|
||||
</h1>
|
||||
<Plus
|
||||
size={22}
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
type: "create_role",
|
||||
server: server._id,
|
||||
callback: (id) => setRole(id),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{["default", ...Object.keys(roles)].map((id) => {
|
||||
if (id === "default") {
|
||||
return (
|
||||
<ButtonItem
|
||||
active={role === "default"}
|
||||
onClick={() => setRole("default")}>
|
||||
<Text id="app.settings.permissions.default_role" />
|
||||
</ButtonItem>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ButtonItem
|
||||
active={role === id}
|
||||
onClick={() => setRole(id)}>
|
||||
{roles[id].name}
|
||||
</ButtonItem>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.permissions}>
|
||||
<div className={styles.title}>
|
||||
<h2>
|
||||
{role === "default" ? (
|
||||
<Text id="app.settings.permissions.default_role" />
|
||||
) : (
|
||||
roles[role].name
|
||||
)}
|
||||
</h2>
|
||||
<Button contrast disabled={!modified} onClick={save}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<section>
|
||||
<Overline type="subtle">
|
||||
<Text id="app.settings.permissions.server" />
|
||||
</Overline>
|
||||
{Object.keys(ServerPermission).map((key) => {
|
||||
if (key === "View") return;
|
||||
let value =
|
||||
ServerPermission[
|
||||
key as keyof typeof ServerPermission
|
||||
];
|
||||
return (
|
||||
<div className={styles.roles}>
|
||||
<div className={styles.list}>
|
||||
<div className={styles.title}>
|
||||
<h1>
|
||||
<Text id="app.settings.server_pages.roles.title" />
|
||||
</h1>
|
||||
<Plus
|
||||
size={22}
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
type: "create_role",
|
||||
server: server._id,
|
||||
callback: (id) => setRole(id),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{["default", ...Object.keys(roles)].map((id) => {
|
||||
if (id === "default") {
|
||||
return (
|
||||
<ButtonItem
|
||||
active={role === "default"}
|
||||
onClick={() => setRole("default")}>
|
||||
<Text id="app.settings.permissions.default_role" />
|
||||
</ButtonItem>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ButtonItem
|
||||
active={role === id}
|
||||
onClick={() => setRole(id)}>
|
||||
{roles[id].name}
|
||||
</ButtonItem>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.permissions}>
|
||||
<div className={styles.title}>
|
||||
<h2>
|
||||
{role === "default" ? (
|
||||
<Text id="app.settings.permissions.default_role" />
|
||||
) : (
|
||||
roles[role].name
|
||||
)}
|
||||
</h2>
|
||||
<Button contrast disabled={!modified} onClick={save}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<section>
|
||||
<Overline type="subtle">
|
||||
<Text id="app.settings.permissions.server" />
|
||||
</Overline>
|
||||
{Object.keys(ServerPermission).map((key) => {
|
||||
if (key === "View") return;
|
||||
let value =
|
||||
ServerPermission[
|
||||
key as keyof typeof ServerPermission
|
||||
];
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={(perm[0] & value) > 0}
|
||||
onChange={() =>
|
||||
setPerm([perm[0] ^ value, perm[1]])
|
||||
}
|
||||
description={
|
||||
<Text id={`permissions.server.${key}.d`} />
|
||||
}>
|
||||
<Text id={`permissions.server.${key}.t`} />
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
<section>
|
||||
<Overline type="subtle">
|
||||
<Text id="app.settings.permissions.channel" />
|
||||
</Overline>
|
||||
{Object.keys(ChannelPermission).map((key) => {
|
||||
if (key === "ManageChannel") return;
|
||||
let value =
|
||||
ChannelPermission[
|
||||
key as keyof typeof ChannelPermission
|
||||
];
|
||||
return (
|
||||
<Checkbox
|
||||
checked={(perm[0] & value) > 0}
|
||||
onChange={() =>
|
||||
setPerm([perm[0] ^ value, perm[1]])
|
||||
}
|
||||
description={
|
||||
<Text id={`permissions.server.${key}.d`} />
|
||||
}>
|
||||
<Text id={`permissions.server.${key}.t`} />
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
<section>
|
||||
<Overline type="subtle">
|
||||
<Text id="app.settings.permissions.channel" />
|
||||
</Overline>
|
||||
{Object.keys(ChannelPermission).map((key) => {
|
||||
if (key === "ManageChannel") return;
|
||||
let value =
|
||||
ChannelPermission[
|
||||
key as keyof typeof ChannelPermission
|
||||
];
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={((perm[1] >>> 0) & value) > 0}
|
||||
onChange={() =>
|
||||
setPerm([perm[0], perm[1] ^ value])
|
||||
}
|
||||
disabled={key === "View"}
|
||||
description={
|
||||
<Text id={`permissions.channel.${key}.d`} />
|
||||
}>
|
||||
<Text id={`permissions.channel.${key}.t`} />
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
<div className={styles.actions}>
|
||||
<Button contrast disabled={!modified} onClick={save}>
|
||||
Save
|
||||
</Button>
|
||||
{role !== "default" && (
|
||||
<Button contrast error onClick={deleteRole}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Checkbox
|
||||
checked={((perm[1] >>> 0) & value) > 0}
|
||||
onChange={() =>
|
||||
setPerm([perm[0], perm[1] ^ value])
|
||||
}
|
||||
disabled={key === "View"}
|
||||
description={
|
||||
<Text id={`permissions.channel.${key}.d`} />
|
||||
}>
|
||||
<Text id={`permissions.channel.${key}.t`} />
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
<div className={styles.actions}>
|
||||
<Button contrast disabled={!modified} onClick={save}>
|
||||
Save
|
||||
</Button>
|
||||
{role !== "default" && (
|
||||
<Button contrast error onClick={deleteRole}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user