mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-07 09:25:27 +00:00
Port settings.
This commit is contained in:
95
src/pages/settings/panes/Account.tsx
Normal file
95
src/pages/settings/panes/Account.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import styles from "./Panes.module.scss";
|
||||
import Tip from "../../../components/ui/Tip";
|
||||
import Button from "../../../components/ui/Button";
|
||||
import { Users } from "revolt.js/dist/api/objects";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import Overline from "../../../components/ui/Overline";
|
||||
import { AtSign, Key, Mail } from "@styled-icons/feather";
|
||||
import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks";
|
||||
import UserIcon from "../../../components/common/UserIcon";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
export function Account() {
|
||||
const { openScreen } = useIntermediate();
|
||||
const status = useContext(StatusContext);
|
||||
|
||||
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();
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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}>
|
||||
<Link to="/settings/profile">
|
||||
<UserIcon target={user} size={72} />
|
||||
</Link>
|
||||
<div className={styles.username}>@{user.username}</div>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
{[
|
||||
["username", user.username, <AtSign size={24} />],
|
||||
["email", email, <Mail size={24} />],
|
||||
["password", "*****", <Key size={24} />]
|
||||
].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 as any
|
||||
})
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
286
src/pages/settings/panes/Appearance.tsx
Normal file
286
src/pages/settings/panes/Appearance.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import styles from "./Panes.module.scss";
|
||||
import { debounce } from "../../../lib/debounce";
|
||||
import Button from "../../../components/ui/Button";
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
import { SettingsTextArea } from "../SettingsTextArea";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { WithDispatcher } from "../../../redux/reducers";
|
||||
import ColourSwatches from "../../../components/ui/ColourSwatches";
|
||||
import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
|
||||
import { Theme, ThemeContext, ThemeOptions } from "../../../context/Theme";
|
||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
// @ts-ignore
|
||||
import pSBC from 'shade-blend-color';
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
// ! FIXME: code needs to be rewritten to fix jittering
|
||||
export function Component(props: Props & WithDispatcher) {
|
||||
const theme = useContext(ThemeContext);
|
||||
const { writeClipboard, openScreen } = useIntermediate();
|
||||
|
||||
function setTheme(theme: ThemeOptions) {
|
||||
props.dispatcher({
|
||||
type: "SETTINGS_SET_THEME",
|
||||
theme
|
||||
});
|
||||
}
|
||||
|
||||
function pushOverride(custom: Partial<Theme>) {
|
||||
props.dispatcher({
|
||||
type: "SETTINGS_SET_THEME_OVERRIDE",
|
||||
custom
|
||||
});
|
||||
}
|
||||
|
||||
function setAccent(accent: string) {
|
||||
setOverride({
|
||||
accent,
|
||||
"sidebar-active": accent,
|
||||
"scrollbar-thumb": pSBC(-0.2, accent)
|
||||
});
|
||||
}
|
||||
|
||||
const emojiPack = props.settings.appearance?.emojiPack ?? 'mutant';
|
||||
function setEmojiPack(emojiPack: EmojiPacks) {
|
||||
props.dispatcher({
|
||||
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 ?? '');
|
||||
|
||||
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="/assets/images/light.svg"
|
||||
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="/assets/images/dark.svg"
|
||||
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.message_display" />
|
||||
</h3>
|
||||
<div className={styles.display}>
|
||||
<Radio
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.display.default_description" />
|
||||
}
|
||||
checked
|
||||
>
|
||||
<Text id="app.settings.pages.appearance.display.default" />
|
||||
</Radio>
|
||||
<Radio
|
||||
description={
|
||||
<Text id="app.settings.pages.appearance.display.compact_description" />
|
||||
}
|
||||
disabled
|
||||
>
|
||||
<Text id="app.settings.pages.appearance.display.compact" />
|
||||
</Radio>
|
||||
</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="/assets/images/mutant_emoji.svg" 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="/assets/images/twemoji_emoji.svg" 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="/assets/images/openmoji_emoji.svg" draggable={false} />
|
||||
</div>
|
||||
<h4>Openmoji</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.button}
|
||||
onClick={() => setEmojiPack('noto')}
|
||||
data-active={emojiPack === 'noto'}>
|
||||
<img src="/assets/images/noto_emoji.svg" draggable={false} />
|
||||
</div>
|
||||
<h4>Noto Emoji</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<Text id="app.settings.pages.appearance.advanced" />
|
||||
<div className={styles.divider}></div>
|
||||
</summary>
|
||||
<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",
|
||||
"sidebar-active",
|
||||
"scrollbar-thumb",
|
||||
"scrollbar-track",
|
||||
"status-online",
|
||||
"status-away",
|
||||
"status-busy",
|
||||
"status-streaming",
|
||||
"status-invisible",
|
||||
"success",
|
||||
"warning",
|
||||
"error",
|
||||
"hover"
|
||||
].map(x => (
|
||||
<div className={styles.entry} key={x}>
|
||||
<span>{x}</span>
|
||||
<div className={styles.override}>
|
||||
<div className={styles.picker}
|
||||
style={{ backgroundColor: (theme as any)[x as any] }}>
|
||||
<input
|
||||
type="color"
|
||||
value={(theme as any)[x as any]}
|
||||
onChange={v =>
|
||||
setOverride({
|
||||
[x]: v.currentTarget.value
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<InputBox
|
||||
className={styles.text}
|
||||
value={(theme as any)[x as any]}
|
||||
onChange={y =>
|
||||
setOverride({
|
||||
[x]: y.currentTarget.value
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.custom_css" />
|
||||
</h3>
|
||||
<SettingsTextArea
|
||||
maxRows={20}
|
||||
minHeight={480}
|
||||
value={css}
|
||||
onChange={css => setCSS(css)}
|
||||
/>
|
||||
</details>
|
||||
|
||||
{/*<h3>
|
||||
<Text id="app.settings.pages.appearance.sync" />
|
||||
</h3>
|
||||
<p>Coming soon!</p>*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Appearance = connectState(
|
||||
Component,
|
||||
state => {
|
||||
return {
|
||||
settings: state.settings
|
||||
};
|
||||
},
|
||||
true
|
||||
);
|
||||
53
src/pages/settings/panes/Experiments.tsx
Normal file
53
src/pages/settings/panes/Experiments.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import styles from "./Panes.module.scss";
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { WithDispatcher } from "../../../redux/reducers";
|
||||
import { AVAILABLE_EXPERIMENTS, ExperimentOptions } from "../../../redux/reducers/experiments";
|
||||
|
||||
interface Props {
|
||||
options?: ExperimentOptions;
|
||||
}
|
||||
|
||||
export function Component(props: Props & WithDispatcher) {
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.experiments.features" />
|
||||
</h3>
|
||||
{
|
||||
(AVAILABLE_EXPERIMENTS).map(
|
||||
key =>
|
||||
<Checkbox
|
||||
checked={(props.options?.enabled ?? []).indexOf(key) > -1}
|
||||
onChange={enabled => {
|
||||
props.dispatcher({
|
||||
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 &&
|
||||
<Text id="app.settings.pages.experiments.not_available" />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ExperimentsPage = connectState(
|
||||
Component,
|
||||
state => {
|
||||
return {
|
||||
options: state.experiments
|
||||
};
|
||||
},
|
||||
true
|
||||
);
|
||||
95
src/pages/settings/panes/Feedback.tsx
Normal file
95
src/pages/settings/panes/Feedback.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import styles from "./Panes.module.scss";
|
||||
import { Localizer, Text } from "preact-i18n";
|
||||
import Radio from "../../../components/ui/Radio";
|
||||
import Button from "../../../components/ui/Button";
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
import { SettingsTextArea } from "../SettingsTextArea";
|
||||
import { useSelf } from "../../../context/revoltjs/hooks";
|
||||
|
||||
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");
|
||||
|
||||
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'
|
||||
}
|
||||
);
|
||||
|
||||
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>
|
||||
<Radio
|
||||
disabled={state === "sending"}
|
||||
checked={checked === "__other_option__"}
|
||||
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>
|
||||
<SettingsTextArea
|
||||
maxRows={10}
|
||||
value={description}
|
||||
id="entry.685672624"
|
||||
disabled={state === "sending"}
|
||||
onChange={value => setDescription(value)}
|
||||
/>
|
||||
<Button type="submit" contrast>
|
||||
<Text id="app.settings.pages.feedback.send" />
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
68
src/pages/settings/panes/Languages.tsx
Normal file
68
src/pages/settings/panes/Languages.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import styles from "./Panes.module.scss";
|
||||
import Tip from "../../../components/ui/Tip";
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { WithDispatcher } from "../../../redux/reducers";
|
||||
import { Emoji } from "../../../components/markdown/Emoji";
|
||||
import { Language, LanguageEntry, Languages as Langs } from "../../../context/Locale";
|
||||
|
||||
interface Props {
|
||||
locale: Language;
|
||||
}
|
||||
|
||||
export function Component({ locale, dispatcher }: Props & WithDispatcher) {
|
||||
return (
|
||||
<div className={styles.languages}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.select" />
|
||||
</h3>
|
||||
<div className={styles.list}>
|
||||
{Object.keys(Langs).map(x => {
|
||||
const l = (Langs as any)[x] as LanguageEntry;
|
||||
return (
|
||||
<Checkbox
|
||||
key={x}
|
||||
className={styles.entry}
|
||||
checked={locale === x}
|
||||
onChange={v => {
|
||||
if (v) {
|
||||
dispatcher({
|
||||
type: "SET_LOCALE",
|
||||
locale: x as Language
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={styles.flag}><Emoji size={42} emoji={l.emoji} /></div>
|
||||
<span className={styles.description}>
|
||||
{l.display}
|
||||
</span>
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
</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
|
||||
};
|
||||
},
|
||||
true
|
||||
);
|
||||
142
src/pages/settings/panes/Notifications.tsx
Normal file
142
src/pages/settings/panes/Notifications.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import styles from "./Panes.module.scss";
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { WithDispatcher } from "../../../redux/reducers";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import { urlBase64ToUint8Array } from "../../../lib/conversion";
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { NotificationOptions } from "../../../redux/reducers/settings";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
interface Props {
|
||||
options?: NotificationOptions;
|
||||
}
|
||||
|
||||
export function Component(props: Props & WithDispatcher) {
|
||||
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);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.notifications.push_notifications" />
|
||||
</h3>
|
||||
<Checkbox
|
||||
disabled={!("Notification" in window)}
|
||||
checked={props.options?.desktopEnabled ?? false}
|
||||
onChange={async desktopEnabled => {
|
||||
if (desktopEnabled) {
|
||||
let permission = await Notification.requestPermission();
|
||||
if (permission !== "granted") {
|
||||
return openScreen({
|
||||
id: "error",
|
||||
error: "DeniedNotification"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
props.dispatcher({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: { desktopEnabled }
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text id="app.settings.pages.notifications.enable_desktop" />
|
||||
<p>
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_desktop" />
|
||||
</p>
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
disabled={typeof pushEnabled === "undefined"}
|
||||
checked={pushEnabled ?? false}
|
||||
onChange={async pushEnabled => {
|
||||
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 any);
|
||||
setPushEnabled(true);
|
||||
}
|
||||
} else {
|
||||
const sub = await reg.pushManager.getSubscription();
|
||||
sub?.unsubscribe();
|
||||
setPushEnabled(false);
|
||||
|
||||
client.req("POST", "/push/unsubscribe");
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text id="app.settings.pages.notifications.enable_push" />
|
||||
<p>
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_push" />
|
||||
</p>
|
||||
</Checkbox>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.notifications.sounds" />
|
||||
</h3>
|
||||
<Checkbox
|
||||
checked={props.options?.soundEnabled ?? true}
|
||||
onChange={soundEnabled =>
|
||||
props.dispatcher({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: { soundEnabled }
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text id="app.settings.pages.notifications.enable_sound" />
|
||||
<p>
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_sound" />
|
||||
</p>
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={props.options?.outgoingSoundEnabled ?? true}
|
||||
onChange={outgoingSoundEnabled =>
|
||||
props.dispatcher({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: { outgoingSoundEnabled }
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text id="app.settings.pages.notifications.enable_outgoing_sound" />
|
||||
<p>
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_outgoing_sound" />
|
||||
</p>
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Notifications = connectState(
|
||||
Component,
|
||||
state => {
|
||||
return {
|
||||
options: state.settings.notification
|
||||
};
|
||||
},
|
||||
true
|
||||
);
|
||||
374
src/pages/settings/panes/Panes.module.scss
Normal file
374
src/pages/settings/panes/Panes.module.scss
Normal file
@@ -0,0 +1,374 @@
|
||||
.user {
|
||||
.banner {
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
border-radius: 6px;
|
||||
align-items: center;
|
||||
background: var(--secondary-header);
|
||||
|
||||
.username {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
a {
|
||||
transition: 0.2s ease filter;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
filter: brightness(80%);
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
margin-top: 1em;
|
||||
flex-direction: column;
|
||||
|
||||
> div {
|
||||
gap: 12px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.detail {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--tertiary-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
grid-template-columns: minmax(auto, 100%);
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
gap: 20px;
|
||||
display: flex;
|
||||
|
||||
.pfp {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.background {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.appearance {
|
||||
.theme {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.themes {
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: border 0.3s;
|
||||
border: 3px solid transparent;
|
||||
|
||||
&[data-active="true"] {
|
||||
cursor: default;
|
||||
border: 3px solid var(--accent);
|
||||
&:hover {
|
||||
border: 3px solid var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 3px solid var(--tertiary-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
details {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: var(--secondary-foreground);
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*summary {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
&::after {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
content: "gh";
|
||||
}
|
||||
}*/
|
||||
|
||||
/*summary::-webkit-details-marker,
|
||||
summary::marker {
|
||||
content: "";
|
||||
}*/
|
||||
}
|
||||
|
||||
.emojiPack {
|
||||
gap: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.row {
|
||||
gap: 12px;
|
||||
display: flex;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 2rem 1.5rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: border 0.3s;
|
||||
background: var(--hover);
|
||||
border: 3px solid transparent;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&[data-active="true"] {
|
||||
cursor: default;
|
||||
background: var(--secondary-background);
|
||||
border: 3px solid var(--accent);
|
||||
|
||||
&:hover {
|
||||
border: 3px solid var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--secondary-background);
|
||||
border: 3px solid var(--tertiary-background);
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
text-transform: unset;
|
||||
|
||||
a {
|
||||
opacity: 0.7;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.display {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actions {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.overrides {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
.entry {
|
||||
gap: 8px;
|
||||
padding: 2px;
|
||||
margin-top: 8px;
|
||||
|
||||
.override {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.picker {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-right: 4px;
|
||||
|
||||
//TOFIX - Looks wonky on Chromium
|
||||
border: 1px solid black;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
border-radius: 4px;
|
||||
padding: 0 4px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sessions {
|
||||
.session {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.entry {
|
||||
margin: 8px 0;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
border-radius: 6px;
|
||||
flex-direction: column;
|
||||
background: var(--secondary-header);
|
||||
|
||||
&[data-active="true"] {
|
||||
color: var(--primary-background);
|
||||
background: var(--accent);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&[data-deleting="true"] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.icon {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
padding-right: 12px;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
div svg {
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 0 6px 0;
|
||||
color: var(--primary-text);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.name {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: var(--teriary-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notifications {
|
||||
label {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
font-size: 0.9em;
|
||||
color: var(--secondary-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.languages {
|
||||
.list {
|
||||
.entry {
|
||||
padding: 2px 8px;
|
||||
height: 50px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.entry > span > span {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
|
||||
.flag {
|
||||
display: flex;
|
||||
font-size: 42px;
|
||||
line-height: 48px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--primary-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feedback .options {
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
126
src/pages/settings/panes/Profile.tsx
Normal file
126
src/pages/settings/panes/Profile.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import styles from "./Panes.module.scss";
|
||||
import Button from "../../../components/ui/Button";
|
||||
import { Users } from "revolt.js/dist/api/objects";
|
||||
import { SettingsTextArea } from "../SettingsTextArea";
|
||||
import { IntlContext, Text, translate } from "preact-i18n";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import { FileUploader } from "../../../context/revoltjs/FileUploads";
|
||||
import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks";
|
||||
import { UserProfile } from "../../../context/intermediate/popovers/UserProfile";
|
||||
import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
export function Profile() {
|
||||
const { intl } = useContext(IntlContext) as any;
|
||||
const status = useContext(StatusContext);
|
||||
|
||||
const ctx = useForceUpdate();
|
||||
const user = useSelf();
|
||||
if (!user) return null;
|
||||
|
||||
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 ?? {}));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (profile === undefined && status === ClientStatus.ONLINE) {
|
||||
refreshProfile();
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
const [ changed, setChanged ] = useState(false);
|
||||
|
||||
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>
|
||||
<SettingsTextArea
|
||||
maxRows={10}
|
||||
minHeight={200}
|
||||
maxLength={2000}
|
||||
value={profile?.content ?? ""}
|
||||
disabled={typeof profile === "undefined"}
|
||||
onChange={content => {
|
||||
setProfile({ ...profile, content })
|
||||
if (!changed) setChanged(true)
|
||||
}}
|
||||
placeholder={translate(
|
||||
`app.settings.pages.profile.${
|
||||
typeof profile === "undefined"
|
||||
? "fetching"
|
||||
: "placeholder"
|
||||
}`,
|
||||
"",
|
||||
intl.dictionary
|
||||
)}
|
||||
/>
|
||||
<Button contrast
|
||||
onClick={() => {
|
||||
setChanged(false);
|
||||
ctx.client.users.editUser({ profile: { content: profile?.content } })
|
||||
}}
|
||||
disabled={!changed}>
|
||||
<Text id="app.special.modals.actions.save" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
186
src/pages/settings/panes/Sessions.tsx
Normal file
186
src/pages/settings/panes/Sessions.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import dayjs from "dayjs";
|
||||
import { decodeTime } from "ulid";
|
||||
import { Text } from "preact-i18n";
|
||||
import styles from "./Panes.module.scss";
|
||||
import Tip from "../../../components/ui/Tip";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import Button from "../../../components/ui/Button";
|
||||
import Preloader from "../../../components/ui/Preloader";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { HelpCircle } from "@styled-icons/feather";
|
||||
import {
|
||||
Android,
|
||||
Firefoxbrowser,
|
||||
Googlechrome,
|
||||
Ios,
|
||||
Linux,
|
||||
Macos,
|
||||
Microsoftedge,
|
||||
Safari,
|
||||
Windows
|
||||
} from "@styled-icons/simple-icons";
|
||||
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
friendly_name: string;
|
||||
}
|
||||
|
||||
export function Sessions() {
|
||||
const client = useContext(AppContext);
|
||||
const deviceId = client.session?.id;
|
||||
|
||||
const [sessions, setSessions] = useState<Session[] | undefined>(undefined);
|
||||
const [attemptingDelete, setDelete] = useState<string[]>([]);
|
||||
const history = useHistory();
|
||||
|
||||
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);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (typeof sessions === "undefined") {
|
||||
return (
|
||||
<div className={styles.loader}>
|
||||
<Preloader />
|
||||
</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 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)
|
||||
};
|
||||
});
|
||||
|
||||
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)
|
||||
];
|
||||
|
||||
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 any
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
53
src/pages/settings/panes/Sync.tsx
Normal file
53
src/pages/settings/panes/Sync.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import styles from "./Panes.module.scss";
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { WithDispatcher } from "../../../redux/reducers";
|
||||
import { SyncKeys, SyncOptions } from "../../../redux/reducers/sync";
|
||||
|
||||
interface Props {
|
||||
options?: SyncOptions;
|
||||
}
|
||||
|
||||
export function Component(props: Props & WithDispatcher) {
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.sync.categories" />
|
||||
</h3>
|
||||
{
|
||||
([
|
||||
['appearance', 'appearance.title'],
|
||||
['theme', 'appearance.theme'],
|
||||
['locale', 'language.title']
|
||||
] as [ SyncKeys, string ][]).map(
|
||||
([ key, title ]) =>
|
||||
<Checkbox
|
||||
checked={(props.options?.disabled ?? []).indexOf(key) === -1}
|
||||
onChange={enabled => {
|
||||
props.dispatcher({
|
||||
type: enabled ? 'SYNC_ENABLE_KEY' : 'SYNC_DISABLE_KEY',
|
||||
key
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text id={`app.settings.pages.${title}`} />
|
||||
<p>
|
||||
<Text id={`app.settings.pages.sync.descriptions.${key}`} />
|
||||
</p>
|
||||
</Checkbox>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Sync = connectState(
|
||||
Component,
|
||||
state => {
|
||||
return {
|
||||
options: state.sync
|
||||
};
|
||||
},
|
||||
true
|
||||
);
|
||||
Reference in New Issue
Block a user