Use tabWidth 4 without actual tabs.

This commit is contained in:
Paul
2021-07-05 11:25:20 +01:00
parent 7bd33d8d34
commit b5a11d5c8f
180 changed files with 16619 additions and 16622 deletions

View File

@@ -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>
);
}

View File

@@ -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,
};
});

View File

@@ -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,
};
});

View File

@@ -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>
);
}

View File

@@ -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,
};
});

View File

@@ -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,
};
});

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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,
};
});