mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-07 01:15:28 +00:00
Merge branch 'mobx'
This commit is contained in:
@@ -1,528 +1,53 @@
|
||||
import {
|
||||
Reset,
|
||||
Import,
|
||||
FontFamily,
|
||||
CodeAlt,
|
||||
} from "@styled-icons/boxicons-regular";
|
||||
import {
|
||||
Pencil,
|
||||
Store,
|
||||
Palette,
|
||||
HappyBeaming,
|
||||
QuoteLeft,
|
||||
} from "@styled-icons/boxicons-solid";
|
||||
import { Link } from "react-router-dom";
|
||||
// @ts-expect-error shade-blend-color does not have typings.
|
||||
import pSBC from "shade-blend-color";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import styles from "./Panes.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||
import { debounce } from "../../../lib/debounce";
|
||||
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { isExperimentEnabled } from "../../../redux/reducers/experiments";
|
||||
import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
|
||||
|
||||
import {
|
||||
DEFAULT_FONT,
|
||||
DEFAULT_MONO_FONT,
|
||||
Fonts,
|
||||
FONTS,
|
||||
FONT_KEYS,
|
||||
MonospaceFonts,
|
||||
MONOSPACE_FONTS,
|
||||
MONOSPACE_FONT_KEYS,
|
||||
Theme,
|
||||
ThemeContext,
|
||||
ThemeOptions,
|
||||
} from "../../../context/Theme";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
import CollapsibleSection from "../../../components/common/CollapsibleSection";
|
||||
import Tooltip from "../../../components/common/Tooltip";
|
||||
import Button from "../../../components/ui/Button";
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
import ColourSwatches from "../../../components/ui/ColourSwatches";
|
||||
import ComboBox from "../../../components/ui/ComboBox";
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
import CategoryButton from "../../../components/ui/fluent/CategoryButton";
|
||||
import darkSVG from "../assets/dark.svg";
|
||||
import lightSVG from "../assets/light.svg";
|
||||
import mutantSVG from "../assets/mutant_emoji.svg";
|
||||
import notoSVG from "../assets/noto_emoji.svg";
|
||||
import openmojiSVG from "../assets/openmoji_emoji.svg";
|
||||
import twemojiSVG from "../assets/twemoji_emoji.svg";
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
}
|
||||
import {
|
||||
ThemeBaseSelectorShim,
|
||||
ThemeShopShim,
|
||||
ThemeAccentShim,
|
||||
DisplayFontShim,
|
||||
DisplayMonospaceFontShim,
|
||||
DisplayLigaturesShim,
|
||||
DisplayEmojiShim,
|
||||
ThemeCustomCSSShim,
|
||||
} from "../../../components/settings/AppearanceShims";
|
||||
import ThemeOverrides from "../../../components/settings/appearance/ThemeOverrides";
|
||||
import ThemeTools from "../../../components/settings/appearance/ThemeTools";
|
||||
|
||||
// ! FIXME: code needs to be rewritten to fix jittering
|
||||
export function Component(props: Props) {
|
||||
const theme = useContext(ThemeContext);
|
||||
const { writeClipboard, openScreen } = useIntermediate();
|
||||
|
||||
function setTheme(theme: ThemeOptions) {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME",
|
||||
theme,
|
||||
});
|
||||
}
|
||||
|
||||
const pushOverride = useCallback((custom: Partial<Theme>) => {
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME_OVERRIDE",
|
||||
custom,
|
||||
});
|
||||
}, []);
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const setOverride = useCallback(
|
||||
debounce(pushOverride as (...args: unknown[]) => void, 200),
|
||||
[pushOverride],
|
||||
) as (custom: Partial<Theme>) => void;
|
||||
const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? "");
|
||||
|
||||
useEffect(() => setOverride({ css }), [setOverride, css]);
|
||||
|
||||
const selected = props.settings.theme?.base ?? "dark";
|
||||
export const Appearance = observer(() => {
|
||||
return (
|
||||
<div className={styles.appearance}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.theme" />
|
||||
</h3>
|
||||
<div className={styles.themes}>
|
||||
<div className={styles.theme}>
|
||||
<img
|
||||
loading="eager"
|
||||
src={lightSVG}
|
||||
draggable={false}
|
||||
data-active={selected === "light"}
|
||||
onClick={() =>
|
||||
selected !== "light" && setTheme({ base: "light" })
|
||||
}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
/>
|
||||
<h4>
|
||||
<Text id="app.settings.pages.appearance.color.light" />
|
||||
</h4>
|
||||
</div>
|
||||
<div className={styles.theme}>
|
||||
<img
|
||||
loading="eager"
|
||||
src={darkSVG}
|
||||
draggable={false}
|
||||
data-active={selected === "dark"}
|
||||
onClick={() =>
|
||||
selected !== "dark" && setTheme({ base: "dark" })
|
||||
}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
/>
|
||||
<h4>
|
||||
<Text id="app.settings.pages.appearance.color.dark" />
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<ThemeBaseSelectorShim />
|
||||
<ThemeShopShim />
|
||||
<ThemeAccentShim />
|
||||
|
||||
{isExperimentEnabled("theme_shop") && (
|
||||
<Link
|
||||
to="/settings/theme_shop"
|
||||
replace
|
||||
className={styles.focus}>
|
||||
<CategoryButton
|
||||
icon={<Store size={24} />}
|
||||
action="chevron"
|
||||
description={"Browse themes made by the community"}
|
||||
hover>
|
||||
<Text id="app.settings.pages.theme_shop.title" />
|
||||
</CategoryButton>
|
||||
</Link>
|
||||
)}
|
||||
<DisplayFontShim />
|
||||
<DisplayLigaturesShim />
|
||||
<DisplayEmojiShim />
|
||||
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.accent_selector" />
|
||||
</h3>
|
||||
<ColourSwatches value={theme.accent} onChange={setAccent} />
|
||||
|
||||
{/* TOFIX: Chane this checkbox to turn off the seasonal home page animations*/}
|
||||
<Checkbox
|
||||
checked={props.settings.theme?.ligatures === true}
|
||||
onChange={() =>
|
||||
setTheme({
|
||||
ligatures: !props.settings.theme?.ligatures,
|
||||
})
|
||||
}
|
||||
description={
|
||||
"Displays effects in the home tab during holiday seasons."
|
||||
}>
|
||||
Seasonal theme
|
||||
</Checkbox>
|
||||
|
||||
{/*<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>*/}
|
||||
<hr />
|
||||
|
||||
{/*<CategoryButton
|
||||
icon={<Palette size={24} />}
|
||||
description={"Customize the look of your app using themes."}
|
||||
action="chevron">
|
||||
Themes
|
||||
</CategoryButton>
|
||||
<CategoryButton
|
||||
icon={<FontFamily size={24} />}
|
||||
description={"Change the font and size used in the app."}
|
||||
action="chevron">
|
||||
{`Font & text size`}
|
||||
</CategoryButton>
|
||||
<CategoryButton
|
||||
icon={<QuoteLeft size={24} />}
|
||||
description={"Change the look of your messages."}
|
||||
action="chevron">
|
||||
Message Display
|
||||
</CategoryButton>
|
||||
<CategoryButton
|
||||
icon={<HappyBeaming size={24} />}
|
||||
description={"Personalize your client with an emoji pack."}
|
||||
action="chevron">
|
||||
Emoji Packs
|
||||
</CategoryButton>
|
||||
<h3>Advanced</h3>
|
||||
<CategoryButton
|
||||
icon={<CodeAlt size={24} />}
|
||||
description={"Customize the client CSS to your heart's content"}
|
||||
action="chevron">
|
||||
Custom CSS
|
||||
</CategoryButton>*/}
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.font" />
|
||||
</h3>
|
||||
<ComboBox
|
||||
value={theme.font ?? DEFAULT_FONT}
|
||||
onChange={(e) =>
|
||||
pushOverride({ font: e.currentTarget.value as Fonts })
|
||||
}>
|
||||
{FONT_KEYS.map((key) => (
|
||||
<option value={key} key={key}>
|
||||
{FONTS[key as keyof typeof FONTS].name}
|
||||
</option>
|
||||
))}
|
||||
</ComboBox>
|
||||
{/* TOFIX: Only show when a font with ligature support is selected, i.e.: Inter.*/}
|
||||
<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>
|
||||
<hr />
|
||||
|
||||
<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
|
||||
loading="eager"
|
||||
src={mutantSVG}
|
||||
draggable={false}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
/>
|
||||
</div>
|
||||
<h4>
|
||||
Mutant Remix{" "}
|
||||
<a
|
||||
href="https://mutant.revolt.chat"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
(by Revolt)
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("twemoji")}
|
||||
data-active={emojiPack === "twemoji"}>
|
||||
<img
|
||||
loading="eager"
|
||||
src={twemojiSVG}
|
||||
draggable={false}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
/>
|
||||
</div>
|
||||
<h4>Twemoji</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("openmoji")}
|
||||
data-active={emojiPack === "openmoji"}>
|
||||
<img
|
||||
loading="eager"
|
||||
src={openmojiSVG}
|
||||
draggable={false}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
/>
|
||||
</div>
|
||||
<h4>Openmoji</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={styles.button}
|
||||
onClick={() => setEmojiPack("noto")}
|
||||
data-active={emojiPack === "noto"}>
|
||||
<img
|
||||
loading="eager"
|
||||
src={notoSVG}
|
||||
draggable={false}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
/>
|
||||
</div>
|
||||
<h4>Noto Emoji</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<CollapsibleSection
|
||||
defaultValue={false}
|
||||
id="settings_overrides"
|
||||
summary={<Text id="app.settings.pages.appearance.overrides" />}>
|
||||
<div className={styles.actions}>
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.settings.pages.appearance.reset_overrides" />
|
||||
}>
|
||||
<Button
|
||||
contrast
|
||||
iconbutton
|
||||
onClick={() => setTheme({ custom: {} })}>
|
||||
<Reset size={22} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<div
|
||||
className={styles.code}
|
||||
onClick={() => writeClipboard(JSON.stringify(theme))}>
|
||||
<Tooltip content={<Text id="app.special.copy" />}>
|
||||
{" "}
|
||||
{/*TOFIX: Try to put the tooltip above the .code div without messing up the css challenge */}
|
||||
{JSON.stringify(theme)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.settings.pages.appearance.import" />
|
||||
}>
|
||||
<Button
|
||||
contrast
|
||||
iconbutton
|
||||
onClick={async () => {
|
||||
try {
|
||||
const text =
|
||||
await navigator.clipboard.readText();
|
||||
setOverride(JSON.parse(text));
|
||||
} catch (err) {
|
||||
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)),
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<Import size={22} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ThemeTools />
|
||||
|
||||
<h3>App</h3>
|
||||
<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}
|
||||
style={{ backgroundColor: theme[x] }}>
|
||||
<div className={styles.input}>
|
||||
<input
|
||||
type="color"
|
||||
value={theme[x]}
|
||||
onChange={(v) =>
|
||||
setOverride({
|
||||
[x]: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
style={`color: ${getContrastingColour(
|
||||
theme[x],
|
||||
theme["primary-background"],
|
||||
)}`}>
|
||||
{x}
|
||||
</span>
|
||||
<div className={styles.override}>
|
||||
<div
|
||||
className={styles.picker}
|
||||
onClick={(e) =>
|
||||
e.currentTarget.parentElement?.parentElement
|
||||
?.querySelector("input")
|
||||
?.click()
|
||||
}>
|
||||
<Pencil size={24} />
|
||||
</div>
|
||||
<InputBox
|
||||
type="text"
|
||||
className={styles.text}
|
||||
value={theme[x]}
|
||||
onChange={(y) =>
|
||||
setOverride({
|
||||
[x]: y.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ThemeOverrides />
|
||||
</CollapsibleSection>
|
||||
|
||||
<CollapsibleSection
|
||||
id="settings_advanced_appearance"
|
||||
defaultValue={false}
|
||||
summary={<Text id="app.settings.pages.appearance.advanced" />}>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.appearance.mono_font" />
|
||||
</h3>
|
||||
<ComboBox
|
||||
value={theme.monospaceFont ?? DEFAULT_MONO_FONT}
|
||||
onChange={(e) =>
|
||||
pushOverride({
|
||||
monospaceFont: e.currentTarget
|
||||
.value as MonospaceFonts,
|
||||
})
|
||||
}>
|
||||
{MONOSPACE_FONT_KEYS.map((key) => (
|
||||
<option value={key} key={key}>
|
||||
{
|
||||
MONOSPACE_FONTS[
|
||||
key as keyof typeof MONOSPACE_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)}
|
||||
/>
|
||||
<DisplayMonospaceFontShim />
|
||||
<ThemeCustomCSSShim />
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Appearance = connectState(Component, (state) => {
|
||||
return {
|
||||
settings: state.settings,
|
||||
};
|
||||
});
|
||||
|
||||
function getContrastingColour(hex: string, fallback: string): string {
|
||||
hex = hex.replace("#", "");
|
||||
const r = parseInt(hex.substr(0, 2), 16);
|
||||
const g = parseInt(hex.substr(2, 2), 16);
|
||||
const b = parseInt(hex.substr(4, 2), 16);
|
||||
const cc = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(cc))
|
||||
return getContrastingColour(fallback, "#fffff");
|
||||
return cc >= 175 ? "black" : "white";
|
||||
}
|
||||
// <DisplayCompactShim />
|
||||
|
||||
@@ -6,8 +6,6 @@ import { TextReact } from "../../../lib/i18n";
|
||||
import { stopPropagation } from "../../../lib/stopPropagation";
|
||||
import { voiceState } from "../../../lib/vortex/VoiceState";
|
||||
|
||||
import { connectState } from "../../../redux/connector";
|
||||
|
||||
import Button from "../../../components/ui/Button";
|
||||
import ComboBox from "../../../components/ui/ComboBox";
|
||||
import Overline from "../../../components/ui/Overline";
|
||||
@@ -20,7 +18,9 @@ import opusSVG from "../assets/opus_logo.svg";
|
||||
|
||||
const constraints = { audio: true };
|
||||
|
||||
export function Component() {
|
||||
// TODO: do not rewrite this code until voice is rewritten!
|
||||
|
||||
export function Audio() {
|
||||
const [mediaStream, setMediaStream] = useState<MediaStream | undefined>(
|
||||
undefined,
|
||||
);
|
||||
@@ -244,7 +244,3 @@ function changeAudioDevice(deviceId: string, deviceType: string) {
|
||||
window.localStorage.setItem("audioOutputDevice", deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
export const Audio = connectState(Component, () => {
|
||||
return;
|
||||
});
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import styles from "./Panes.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import {
|
||||
AVAILABLE_EXPERIMENTS,
|
||||
ExperimentOptions,
|
||||
EXPERIMENTS,
|
||||
isExperimentEnabled,
|
||||
} from "../../../redux/reducers/experiments";
|
||||
} from "../../../mobx/stores/Experiments";
|
||||
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
|
||||
interface Props {
|
||||
options?: ExperimentOptions;
|
||||
}
|
||||
export const ExperimentsPage = observer(() => {
|
||||
const experiments = useApplicationState().experiments;
|
||||
|
||||
export function Component(props: Props) {
|
||||
return (
|
||||
<div className={styles.experiments}>
|
||||
<h3>
|
||||
@@ -25,15 +22,8 @@ export function Component(props: Props) {
|
||||
{AVAILABLE_EXPERIMENTS.map((key) => (
|
||||
<Checkbox
|
||||
key={key}
|
||||
checked={isExperimentEnabled(key, props.options)}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: enabled
|
||||
? "EXPERIMENTS_ENABLE"
|
||||
: "EXPERIMENTS_DISABLE",
|
||||
key,
|
||||
})
|
||||
}
|
||||
checked={experiments.isEnabled(key)}
|
||||
onChange={(enabled) => experiments.setEnabled(key, enabled)}
|
||||
description={EXPERIMENTS[key].description}>
|
||||
{EXPERIMENTS[key].title}
|
||||
</Checkbox>
|
||||
@@ -45,10 +35,4 @@ export function Component(props: Props) {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ExperimentsPage = connectState(Component, (state) => {
|
||||
return {
|
||||
options: state.experiments,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import styles from "./Panes.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useMemo } from "preact/hooks";
|
||||
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import {
|
||||
Language,
|
||||
@@ -17,26 +19,25 @@ import enchantingTableWEBP from "../assets/enchanting_table.webp";
|
||||
import tamilFlagPNG from "../assets/tamil_nadu_flag.png";
|
||||
import tokiponaSVG from "../assets/toki_pona.svg";
|
||||
|
||||
type Props = {
|
||||
locale: Language;
|
||||
};
|
||||
type Key = [Language, LanguageEntry];
|
||||
|
||||
type Key = [string, LanguageEntry];
|
||||
interface Props {
|
||||
entry: Key;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
function Entry({ entry: [x, lang], locale }: { entry: Key } & Props) {
|
||||
/**
|
||||
* Component providing individual language entries.
|
||||
* @param param0 Entry data
|
||||
*/
|
||||
function Entry({ entry: [x, lang], selected, onSelect }: Props) {
|
||||
return (
|
||||
<Checkbox
|
||||
key={x}
|
||||
className={styles.entry}
|
||||
checked={locale === x}
|
||||
onChange={(v) => {
|
||||
if (v) {
|
||||
dispatch({
|
||||
type: "SET_LOCALE",
|
||||
locale: x as Language,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
checked={selected}
|
||||
onChange={onSelect}>
|
||||
<div className={styles.flag}>
|
||||
{lang.i18n === "ta" ? (
|
||||
<img
|
||||
@@ -61,36 +62,58 @@ function Entry({ entry: [x, lang], locale }: { entry: Key } & Props) {
|
||||
);
|
||||
}
|
||||
|
||||
export function Component(props: Props) {
|
||||
const languages = Object.keys(Langs).map((x) => [
|
||||
x,
|
||||
Langs[x as keyof typeof Langs],
|
||||
]) as Key[];
|
||||
/**
|
||||
* Component providing the language selection menu.
|
||||
*/
|
||||
export const Languages = observer(() => {
|
||||
const locale = useApplicationState().locale;
|
||||
const language = locale.getLanguage();
|
||||
|
||||
// Get the user's system language. Check for exact
|
||||
// matches first, otherwise check for partial matches
|
||||
const preferredLanguage =
|
||||
navigator.languages.filter((lang) =>
|
||||
languages.find((l) => l[0].replace(/_/g, "-") == lang),
|
||||
)?.[0] ||
|
||||
navigator.languages
|
||||
?.map((x) => x.split("-")[0])
|
||||
?.filter((lang) => languages.find((l) => l[0] == lang))?.[0]
|
||||
?.split("-")[0];
|
||||
// Generate languages array.
|
||||
const languages = useMemo(() => {
|
||||
const languages = Object.keys(Langs).map((x) => [
|
||||
x,
|
||||
Langs[x as keyof typeof Langs],
|
||||
]) as Key[];
|
||||
|
||||
if (preferredLanguage) {
|
||||
// This moves the user's system language to the top of the language list
|
||||
const prefLangKey = languages.find(
|
||||
(lang) => lang[0].replace(/_/g, "-") == preferredLanguage,
|
||||
);
|
||||
if (prefLangKey) {
|
||||
languages.splice(
|
||||
0,
|
||||
0,
|
||||
languages.splice(languages.indexOf(prefLangKey), 1)[0],
|
||||
// Get the user's system language. Check for exact
|
||||
// matches first, otherwise check for partial matches
|
||||
const preferredLanguage =
|
||||
navigator.languages.filter((lang) =>
|
||||
languages.find((l) => l[0].replace(/_/g, "-") == lang),
|
||||
)?.[0] ||
|
||||
navigator.languages
|
||||
?.map((x) => x.split("-")[0])
|
||||
?.filter((lang) => languages.find((l) => l[0] == lang))?.[0]
|
||||
?.split("-")[0];
|
||||
|
||||
if (preferredLanguage) {
|
||||
// This moves the user's system language to the top of the language list
|
||||
const prefLangKey = languages.find(
|
||||
(lang) => lang[0].replace(/_/g, "-") == preferredLanguage,
|
||||
);
|
||||
|
||||
if (prefLangKey) {
|
||||
languages.splice(
|
||||
0,
|
||||
0,
|
||||
languages.splice(languages.indexOf(prefLangKey), 1)[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return languages;
|
||||
}, []);
|
||||
|
||||
// Creates entries with given key.
|
||||
const EntryFactory = ([x, lang]: Key) => (
|
||||
<Entry
|
||||
key={x}
|
||||
entry={[x, lang]}
|
||||
selected={language === x}
|
||||
onSelect={() => locale.setLanguage(x)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.languages}>
|
||||
@@ -98,11 +121,7 @@ export function Component(props: Props) {
|
||||
<Text id="app.settings.pages.language.select" />
|
||||
</h3>
|
||||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => !lang.cat)
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
{languages.filter(([, lang]) => !lang.cat).map(EntryFactory)}
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.const" />
|
||||
@@ -110,9 +129,7 @@ export function Component(props: Props) {
|
||||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => lang.cat === "const")
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
.map(EntryFactory)}
|
||||
</div>
|
||||
<h3>
|
||||
<Text id="app.settings.pages.language.other" />
|
||||
@@ -120,9 +137,7 @@ export function Component(props: Props) {
|
||||
<div className={styles.list}>
|
||||
{languages
|
||||
.filter(([, lang]) => lang.cat === "alt")
|
||||
.map(([x, lang]) => (
|
||||
<Entry key={x} entry={[x, lang]} {...props} />
|
||||
))}
|
||||
.map(EntryFactory)}
|
||||
</div>
|
||||
<Tip>
|
||||
<span>
|
||||
@@ -137,10 +152,4 @@ export function Component(props: Props) {
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Languages = connectState(Component, (state) => {
|
||||
return {
|
||||
locale: state.locale,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import defaultsDeep from "lodash.defaultsdeep";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import styles from "./Panes.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
@@ -6,28 +6,17 @@ import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { urlBase64ToUint8Array } from "../../../lib/conversion";
|
||||
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import {
|
||||
DEFAULT_SOUNDS,
|
||||
NotificationOptions,
|
||||
SoundOptions,
|
||||
} from "../../../redux/reducers/settings";
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
|
||||
import { SOUNDS_ARRAY } from "../../../assets/sounds/Audio";
|
||||
|
||||
interface Props {
|
||||
options?: NotificationOptions;
|
||||
}
|
||||
|
||||
export function Component({ options }: Props) {
|
||||
export const Notifications = observer(() => {
|
||||
const client = useContext(AppContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const settings = useApplicationState().settings;
|
||||
const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
|
||||
undefined,
|
||||
);
|
||||
@@ -42,10 +31,6 @@ export function Component({ options }: Props) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const enabledSounds: SoundOptions = defaultsDeep(
|
||||
options?.sounds ?? {},
|
||||
DEFAULT_SOUNDS,
|
||||
);
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
<h3>
|
||||
@@ -53,7 +38,7 @@ export function Component({ options }: Props) {
|
||||
</h3>
|
||||
<Checkbox
|
||||
disabled={!("Notification" in window)}
|
||||
checked={options?.desktopEnabled ?? false}
|
||||
checked={settings.get("notifications:desktop", false)!}
|
||||
description={
|
||||
<Text id="app.settings.pages.notifications.descriptions.enable_desktop" />
|
||||
}
|
||||
@@ -61,6 +46,7 @@ export function Component({ options }: Props) {
|
||||
if (desktopEnabled) {
|
||||
const permission =
|
||||
await Notification.requestPermission();
|
||||
|
||||
if (permission !== "granted") {
|
||||
return openScreen({
|
||||
id: "error",
|
||||
@@ -69,10 +55,7 @@ export function Component({ options }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: { desktopEnabled },
|
||||
});
|
||||
settings.set("notifications:desktop", desktopEnabled);
|
||||
}}>
|
||||
<Text id="app.settings.pages.notifications.enable_desktop" />
|
||||
</Checkbox>
|
||||
@@ -125,32 +108,16 @@ export function Component({ options }: Props) {
|
||||
<h3>
|
||||
<Text id="app.settings.pages.notifications.sounds" />
|
||||
</h3>
|
||||
{SOUNDS_ARRAY.map((key) => (
|
||||
{settings.sounds.getState().map(({ id, enabled }) => (
|
||||
<Checkbox
|
||||
key={key}
|
||||
checked={!!enabledSounds[key]}
|
||||
key={id}
|
||||
checked={enabled}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||
options: {
|
||||
sounds: {
|
||||
...options?.sounds,
|
||||
[key]: enabled,
|
||||
},
|
||||
},
|
||||
})
|
||||
settings.sounds.setEnabled(id, enabled)
|
||||
}>
|
||||
<Text
|
||||
id={`app.settings.pages.notifications.sound.${key}`}
|
||||
/>
|
||||
<Text id={`app.settings.pages.notifications.sound.${id}`} />
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Notifications = connectState(Component, (state) => {
|
||||
return {
|
||||
options: state.settings.notification,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -461,97 +461,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actions {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
margin: 18px 0 8px 0;
|
||||
|
||||
.code {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
padding: 8px;
|
||||
font-family: var(--monospace-font);
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--secondary-background);
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overrides {
|
||||
row-gap: 8px;
|
||||
display: grid;
|
||||
column-gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
margin-bottom: 20px;
|
||||
|
||||
.entry {
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
border: 1px solid black;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 8px;
|
||||
text-transform: capitalize;
|
||||
|
||||
background: inherit;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
}
|
||||
|
||||
.override {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
|
||||
.picker {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: grid;
|
||||
cursor: pointer;
|
||||
place-items: center;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--primary-background);
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
border: none;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
top: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sessions {
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import styles from "./Panes.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { dispatch } from "../../../redux";
|
||||
import { connectState } from "../../../redux/connector";
|
||||
import { SyncKeys, SyncOptions } from "../../../redux/reducers/sync";
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { SyncKeys } from "../../../mobx/stores/Sync";
|
||||
|
||||
import Checkbox from "../../../components/ui/Checkbox";
|
||||
|
||||
interface Props {
|
||||
options?: SyncOptions;
|
||||
}
|
||||
export const Sync = observer(() => {
|
||||
const sync = useApplicationState().sync;
|
||||
|
||||
export function Component(props: Props) {
|
||||
return (
|
||||
<div className={styles.notifications}>
|
||||
{/*<h3>
|
||||
@@ -31,22 +30,13 @@ export function Component(props: Props) {
|
||||
).map(([key, title]) => (
|
||||
<Checkbox
|
||||
key={key}
|
||||
checked={
|
||||
(props.options?.disabled ?? []).indexOf(key) === -1
|
||||
}
|
||||
checked={sync.isEnabled(key)}
|
||||
description={
|
||||
<Text
|
||||
id={`app.settings.pages.sync.descriptions.${key}`}
|
||||
/>
|
||||
}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
type: enabled
|
||||
? "SYNC_ENABLE_KEY"
|
||||
: "SYNC_DISABLE_KEY",
|
||||
key,
|
||||
})
|
||||
}>
|
||||
onChange={() => sync.toggle(key)}>
|
||||
<Text id={`app.settings.pages.${title}`} />
|
||||
</Checkbox>
|
||||
))}
|
||||
@@ -55,10 +45,4 @@ export function Component(props: Props) {
|
||||
</h5>*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Sync = connectState(Component, (state) => {
|
||||
return {
|
||||
options: state.sync,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import { Plus, Check } from "@styled-icons/boxicons-regular";
|
||||
import {
|
||||
Star,
|
||||
BarChartAlt2,
|
||||
Brush,
|
||||
Bookmark,
|
||||
} from "@styled-icons/boxicons-solid";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { dispatch } from "../../../redux";
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { Theme, generateVariables, ThemeOptions } from "../../../context/Theme";
|
||||
import { Theme, generateVariables } from "../../../context/Theme";
|
||||
|
||||
import InputBox from "../../../components/ui/InputBox";
|
||||
import Tip from "../../../components/ui/Tip";
|
||||
import previewPath from "../assets/preview.svg";
|
||||
|
||||
@@ -258,6 +250,8 @@ export function ThemeShop() {
|
||||
>(null);
|
||||
const [themeData, setThemeData] = useState<Record<string, Theme>>({});
|
||||
|
||||
const themes = useApplicationState().settings.theme;
|
||||
|
||||
async function fetchThemeList() {
|
||||
const manifest = await fetchManifest();
|
||||
setThemeList(
|
||||
@@ -352,21 +346,9 @@ export function ThemeShop() {
|
||||
data-loaded={Reflect.has(themeData, slug)}>
|
||||
<button
|
||||
class="preview"
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: "THEMES_SET_THEME",
|
||||
theme: {
|
||||
slug,
|
||||
meta: theme,
|
||||
theme: themeData[slug],
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: "SETTINGS_SET_THEME",
|
||||
theme: { base: slug },
|
||||
});
|
||||
}}>
|
||||
onClick={() =>
|
||||
themes.hydrate(themeData[slug], true)
|
||||
}>
|
||||
<div class="previewBox">
|
||||
<div class="hover">Use theme</div>
|
||||
<ThemePreview
|
||||
|
||||
Reference in New Issue
Block a user