feat(mobx): rewrite appearance menu

This commit is contained in:
Paul 2021-12-15 18:23:05 +00:00
parent 65be047dc6
commit c7df0088fc
19 changed files with 558 additions and 403 deletions

View file

@ -146,7 +146,6 @@
"revolt.js": "5.1.0-alpha.15", "revolt.js": "5.1.0-alpha.15",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "^1.35.1", "sass": "^1.35.1",
"shade-blend-color": "^1.0.0",
"styled-components": "^5.3.0", "styled-components": "^5.3.0",
"typescript": "^4.4.2", "typescript": "^4.4.2",
"ulid": "^2.3.0", "ulid": "^2.3.0",

View file

@ -3,7 +3,7 @@ import { EmojiPacks } from "../../redux/reducers/settings";
let EMOJI_PACK = "mutant"; let EMOJI_PACK = "mutant";
const REVISION = 3; const REVISION = 3;
export function setEmojiPack(pack: EmojiPacks) { export function setGlobalEmojiPack(pack: EmojiPacks) {
EMOJI_PACK = pack; EMOJI_PACK = pack;
} }

View file

@ -1,13 +1,14 @@
import { Store } from "@styled-icons/boxicons-regular"; import { Store } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
// @ts-expect-error shade-blend-color does not have typings.
import pSBC from "shade-blend-color";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import TextAreaAutoSize from "../../lib/TextAreaAutoSize"; import TextAreaAutoSize from "../../lib/TextAreaAutoSize";
import { useApplicationState } from "../../mobx/State"; import { useApplicationState } from "../../mobx/State";
import { EmojiPack } from "../../mobx/stores/Settings";
import { import {
Fonts, Fonts,
@ -23,13 +24,13 @@ import ColourSwatches from "../ui/ColourSwatches";
import ComboBox from "../ui/ComboBox"; import ComboBox from "../ui/ComboBox";
import Radio from "../ui/Radio"; import Radio from "../ui/Radio";
import CategoryButton from "../ui/fluent/CategoryButton"; import CategoryButton from "../ui/fluent/CategoryButton";
import mutantSVG from "./mutant_emoji.svg";
import notoSVG from "./noto_emoji.svg";
import openmojiSVG from "./openmoji_emoji.svg";
import twemojiSVG from "./twemoji_emoji.svg";
import { EmojiSelector } from "./appearance/EmojiSelector";
import { ThemeBaseSelector } from "./appearance/ThemeBaseSelector"; import { ThemeBaseSelector } from "./appearance/ThemeBaseSelector";
/**
* Component providing a way to switch the base theme being used.
*/
export const ThemeBaseSelectorShim = observer(() => { export const ThemeBaseSelectorShim = observer(() => {
const theme = useApplicationState().settings.theme; const theme = useApplicationState().settings.theme;
return ( return (
@ -37,6 +38,11 @@ export const ThemeBaseSelectorShim = observer(() => {
); );
}); });
/**
* Component providing a link to the theme shop.
* Only appears if experiment is enabled.
* TODO: stabilise
*/
export const ThemeShopShim = () => { export const ThemeShopShim = () => {
if (!useApplicationState().experiments.isEnabled("theme_shop")) return null; if (!useApplicationState().experiments.isEnabled("theme_shop")) return null;
@ -49,6 +55,9 @@ export const ThemeShopShim = () => {
); );
}; };
/**
* Component providing a way to change current accent colour.
*/
export const ThemeAccentShim = observer(() => { export const ThemeAccentShim = observer(() => {
const theme = useApplicationState().settings.theme; const theme = useApplicationState().settings.theme;
return ( return (
@ -58,12 +67,18 @@ export const ThemeAccentShim = observer(() => {
</h3> </h3>
<ColourSwatches <ColourSwatches
value={theme.getVariable("accent")} value={theme.getVariable("accent")}
onChange={(colour) => theme.setVariable("accent", colour)} onChange={(colour) => {
theme.setVariable("accent", colour as string);
theme.setVariable("scrollbar-thumb", pSBC(-0.2, colour));
}}
/> />
</> </>
); );
}); });
/**
* Component providing a way to edit custom CSS.
*/
export const ThemeCustomCSSShim = observer(() => { export const ThemeCustomCSSShim = observer(() => {
const theme = useApplicationState().settings.theme; const theme = useApplicationState().settings.theme;
return ( return (
@ -82,7 +97,15 @@ export const ThemeCustomCSSShim = observer(() => {
); );
}); });
export const ThemeImporterShim = observer(() => {
return <a></a>;
});
/**
* Component providing a way to switch between compact and normal message view.
*/
export const DisplayCompactShim = () => { export const DisplayCompactShim = () => {
// TODO: WIP feature
return ( return (
<> <>
<h3> <h3>
@ -108,6 +131,9 @@ export const DisplayCompactShim = () => {
); );
}; };
/**
* Component providing a way to change primary text font.
*/
export const DisplayFontShim = observer(() => { export const DisplayFontShim = observer(() => {
const theme = useApplicationState().settings.theme; const theme = useApplicationState().settings.theme;
return ( return (
@ -128,6 +154,9 @@ export const DisplayFontShim = observer(() => {
); );
}); });
/**
* Component providing a way to change secondary, monospace text font.
*/
export const DisplayMonospaceFontShim = observer(() => { export const DisplayMonospaceFontShim = observer(() => {
const theme = useApplicationState().settings.theme; const theme = useApplicationState().settings.theme;
return ( return (
@ -155,6 +184,9 @@ export const DisplayMonospaceFontShim = observer(() => {
); );
}); });
/**
* Component providing a way to toggle font ligatures.
*/
export const DisplayLigaturesShim = observer(() => { export const DisplayLigaturesShim = observer(() => {
const settings = useApplicationState().settings; const settings = useApplicationState().settings;
if (settings.theme.getFont() !== "Inter") return null; if (settings.theme.getFont() !== "Inter") return null;
@ -173,86 +205,15 @@ export const DisplayLigaturesShim = observer(() => {
); );
}); });
/**
* Component providing a way to change emoji pack.
*/
export const DisplayEmojiShim = observer(() => { export const DisplayEmojiShim = observer(() => {
const settings = useApplicationState().settings; const settings = useApplicationState().settings;
const emojiPack = settings.get("appearance:emoji");
const setPack = (v: EmojiPack) => () => settings.set("appearance:emoji", v);
return ( return (
<> <EmojiSelector
<h3> value={settings.get("appearance:emoji")}
<Text id="app.settings.pages.appearance.emoji_pack" /> setValue={(v) => settings.set("appearance:emoji", v)}
</h3>
<div /* className={styles.emojiPack} */>
<div /* className={styles.row} */>
<div>
<div
/* className={styles.button} */
onClick={setPack("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={setPack("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={setPack("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={setPack("noto")}
data-active={emojiPack === "noto"}>
<img
loading="eager"
src={notoSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Noto Emoji</h4>
</div>
</div>
</div>
</>
); );
}); });

View file

@ -0,0 +1,162 @@
import styled from "styled-components";
import { Text } from "preact-i18n";
import { EmojiPack } from "../../../mobx/stores/Settings";
import mutantSVG from "./mutant_emoji.svg";
import notoSVG from "./noto_emoji.svg";
import openmojiSVG from "./openmoji_emoji.svg";
import twemojiSVG from "./twemoji_emoji.svg";
const Container = styled.div`
gap: 12px;
display: flex;
flex-direction: column;
.row {
gap: 12px;
display: flex;
> div {
flex: 1;
display: flex;
flex-direction: column;
}
}
.button {
padding: 2rem 1.2rem;
display: grid;
place-items: center;
cursor: pointer;
transition: border 0.3s;
background: var(--hover);
border: 3px solid transparent;
border-radius: var(--border-radius);
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;
}
}
@media only screen and (max-width: 800px) {
a {
display: block;
}
}
}
`;
interface Props {
value?: EmojiPack;
setValue: (pack: EmojiPack) => void;
}
export function EmojiSelector({ value, setValue }: Props) {
return (
<>
<h3>
<Text id="app.settings.pages.appearance.emoji_pack" />
</h3>
<Container>
<div class="row">
<div>
<div
class="button"
onClick={() => setValue("mutant")}
data-active={value === "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
class="button"
onClick={() => setValue("twemoji")}
data-active={value === "twemoji"}>
<img
loading="eager"
src={twemojiSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Twemoji</h4>
</div>
</div>
<div class="row">
<div>
<div
class="button"
onClick={() => setValue("openmoji")}
data-active={value === "openmoji"}>
<img
loading="eager"
src={openmojiSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Openmoji</h4>
</div>
<div>
<div
class="button"
onClick={() => setValue("noto")}
data-active={value === "noto"}>
<img
loading="eager"
src={notoSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Noto Emoji</h4>
</div>
</div>
</Container>
</>
);
}

View file

@ -58,7 +58,7 @@ export function ThemeBaseSelector({ value, setValue }: Props) {
src={lightSVG} src={lightSVG}
draggable={false} draggable={false}
data-active={value === "light"} data-active={value === "light"}
onClick={() => value !== "light" && setValue("light")} onClick={() => setValue("light")}
onContextMenu={(e) => e.preventDefault()} onContextMenu={(e) => e.preventDefault()}
/> />
<h4> <h4>
@ -71,7 +71,7 @@ export function ThemeBaseSelector({ value, setValue }: Props) {
src={darkSVG} src={darkSVG}
draggable={false} draggable={false}
data-active={value === "dark"} data-active={value === "dark"}
onClick={() => value !== "dark" && setValue("dark")} onClick={() => setValue("dark")}
onContextMenu={(e) => e.preventDefault()} onContextMenu={(e) => e.preventDefault()}
/> />
<h4> <h4>

View file

@ -0,0 +1,181 @@
import { Pencil } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import styled from "styled-components";
import { useDebounceCallback } from "../../../lib/debounce";
import { useApplicationState } from "../../../mobx/State";
import { Variables } from "../../../context/Theme";
import InputBox from "../../ui/InputBox";
const Container = styled.div`
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;
}
}
}
`;
export default observer(() => {
const theme = useApplicationState().settings.theme;
const setVariable = useDebounceCallback(
(data) => {
const { key, value } = data as { key: Variables; value: string };
theme.setVariable(key, value);
},
[theme],
100,
);
return (
<Container>
{(
[
"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((key) => (
<div
class="entry"
key={key}
style={{ backgroundColor: theme.getVariable(key) }}>
<div class="input">
<input
type="color"
value={theme.getVariable(key)}
onChange={(el) =>
setVariable({
key,
value: el.currentTarget.value,
})
}
/>
</div>
<span
style={{
color: getContrastingColour(
theme.getVariable(key),
theme.getVariable("primary-background"),
),
}}>
{key}
</span>
<div class="override">
<div
class="picker"
onClick={(e) =>
e.currentTarget.parentElement?.parentElement
?.querySelector("input")
?.click()
}>
<Pencil size={24} />
</div>
<InputBox
type="text"
class="text"
value={theme.getVariable(key)}
onChange={(el) =>
setVariable({
key,
value: el.currentTarget.value,
})
}
/>
</div>
</div>
))}
</Container>
);
});
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";
}

View file

@ -0,0 +1,89 @@
import { Import, Reset } from "@styled-icons/boxicons-regular";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { useApplicationState } from "../../../mobx/State";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import Tooltip from "../../common/Tooltip";
import Button from "../../ui/Button";
const Actions = styled.div`
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;
}
}
`;
export default function ThemeTools() {
const { writeClipboard, openScreen } = useIntermediate();
const theme = useApplicationState().settings.theme;
return (
<Actions>
<Tooltip
content={
<Text id="app.settings.pages.appearance.reset_overrides" />
}>
<Button contrast iconbutton onClick={theme.reset}>
<Reset size={22} />
</Button>
</Tooltip>
<div
class="code"
onClick={() => writeClipboard(JSON.stringify(theme))}>
<Tooltip content={<Text id="app.special.copy" />}>
{" "}
{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();
theme.hydrate(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 (text) =>
theme.hydrate(JSON.parse(text)),
});
}
}}>
<Import size={22} />
</Button>
</Tooltip>
</Actions>
);
}

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -5,6 +5,8 @@ import styled, { css } from "styled-components";
import { RefObject } from "preact"; import { RefObject } from "preact";
import { useRef } from "preact/hooks"; import { useRef } from "preact/hooks";
import { useDebounceCallback } from "../../lib/debounce";
interface Props { interface Props {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
@ -115,6 +117,11 @@ const Rows = styled.div`
export default function ColourSwatches({ value, onChange }: Props) { export default function ColourSwatches({ value, onChange }: Props) {
const ref = useRef<HTMLInputElement>() as RefObject<HTMLInputElement>; const ref = useRef<HTMLInputElement>() as RefObject<HTMLInputElement>;
const setValue = useDebounceCallback(
(value) => onChange(value as string),
[onChange],
100,
);
return ( return (
<SwatchesBase> <SwatchesBase>
@ -122,7 +129,7 @@ export default function ColourSwatches({ value, onChange }: Props) {
type="color" type="color"
value={value} value={value}
ref={ref} ref={ref}
onChange={(ev) => onChange(ev.currentTarget.value)} onChange={(ev) => setValue(ev.currentTarget.value)}
/> />
<Swatch <Swatch
colour={value} colour={value}

View file

@ -9,6 +9,8 @@ import { useApplicationState } from "../../mobx/State";
import { connectState } from "../../redux/connector"; import { connectState } from "../../redux/connector";
import { QueuedMessage } from "../../redux/reducers/queue"; import { QueuedMessage } from "../../redux/reducers/queue";
import { setGlobalEmojiPack } from "../../components/common/Emoji";
import { AppContext } from "./RevoltClient"; import { AppContext } from "./RevoltClient";
type Props = { type Props = {
@ -30,6 +32,12 @@ function StateMonitor(props: Props) {
return () => client.removeListener("message", add); return () => client.removeListener("message", add);
}, [client, props.messages]); }, [client, props.messages]);
// Set global emoji pack.
useEffect(() => {
const v = state.settings.get("appearance:emoji");
v && setGlobalEmojiPack(v);
}, [state.settings.get("appearance:emoji")]);
return null; return null;
} }

View file

@ -2,7 +2,7 @@ import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
import { mapToRecord } from "../../lib/conversion"; import { mapToRecord } from "../../lib/conversion";
import { Fonts, MonospaceFonts, Overrides, Theme } from "../../context/Theme"; import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme";
import { Sounds } from "../../assets/sounds/Audio"; import { Sounds } from "../../assets/sounds/Audio";
import Persistent from "../interfaces/Persistent"; import Persistent from "../interfaces/Persistent";

View file

@ -25,7 +25,42 @@ export default class STheme {
constructor(settings: Settings) { constructor(settings: Settings) {
this.settings = settings; this.settings = settings;
makeAutoObservable(this); makeAutoObservable(this);
this.setBase = this.setBase.bind(this); this.setBase = this.setBase.bind(this);
this.reset = this.reset.bind(this);
}
@computed toJSON() {
return JSON.parse(
JSON.stringify({
...this.getVariables(),
css: this.getCSS(),
font: this.getFont(),
monospaceFont: this.getMonospaceFont(),
}),
);
}
@action hydrate(data: Partial<Theme>) {
for (const key of Object.keys(data)) {
const value = data[key as keyof Theme] as string;
switch (key) {
case "css": {
this.setCSS(value);
break;
}
case "font": {
this.setFont(value as Fonts);
break;
}
case "monospaceFont": {
this.setMonospaceFont(value as MonospaceFonts);
break;
}
default:
this.setVariable(key as Variables, value);
}
}
} }
/** /**
@ -72,8 +107,8 @@ export default class STheme {
* @returns Value of variable * @returns Value of variable
*/ */
@computed getVariable(key: Variables) { @computed getVariable(key: Variables) {
return (this.settings.get("appearance:theme:overrides") ?? return (this.settings.get("appearance:theme:overrides")?.[key] ??
PRESETS[this.getBase()])[key]!; PRESETS[this.getBase()]?.[key])!;
} }
@action setFont(font: Fonts) { @action setFont(font: Fonts) {
@ -125,4 +160,9 @@ export default class STheme {
this.settings.remove("appearance:theme:base"); this.settings.remove("appearance:theme:base");
} }
} }
@action reset() {
this.settings.remove("appearance:theme:overrides");
this.settings.remove("appearance:theme:css");
}
} }

View file

@ -1,231 +1,43 @@
import { Reset, Import } from "@styled-icons/boxicons-regular";
import { Pencil } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// @ts-expect-error shade-blend-color does not have typings.
import pSBC from "shade-blend-color";
import styles from "./Panes.module.scss"; import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useCallback, useEffect, useState } from "preact/hooks";
import { debounce } from "../../../lib/debounce";
import { useApplicationState } from "../../../mobx/State";
import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector";
import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
import { Theme, ThemeOptions } from "../../../context/Theme";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import CollapsibleSection from "../../../components/common/CollapsibleSection"; import CollapsibleSection from "../../../components/common/CollapsibleSection";
import Tooltip from "../../../components/common/Tooltip";
import Button from "../../../components/ui/Button";
import InputBox from "../../../components/ui/InputBox";
import { import {
ThemeBaseSelectorShim, ThemeBaseSelectorShim,
ThemeShopShim, ThemeShopShim,
ThemeAccentShim, ThemeAccentShim,
DisplayCompactShim,
DisplayFontShim, DisplayFontShim,
DisplayMonospaceFontShim, DisplayMonospaceFontShim,
DisplayLigaturesShim, DisplayLigaturesShim,
DisplayEmojiShim, DisplayEmojiShim,
ThemeCustomCSSShim, ThemeCustomCSSShim,
} from "../../../components/settings/AppearanceShims"; } from "../../../components/settings/AppearanceShims";
import ThemeOverrides from "../../../components/settings/appearance/ThemeOverrides";
import ThemeTools from "../../../components/settings/appearance/ThemeTools";
interface Props { export const Appearance = observer(() => {
settings: Settings;
}
// ! FIXME: code needs to be rewritten to fix jittering
export const Component = observer((props: Props) => {
//const theme = useApplicationState().settings.theme;
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 = theme.getBase();*/
return ( return (
<div className={styles.appearance}> <div className={styles.appearance}>
<ThemeBaseSelectorShim /> <ThemeBaseSelectorShim />
<ThemeShopShim /> <ThemeShopShim />
<ThemeAccentShim /> <ThemeAccentShim />
{/*<DisplayCompactShim />
<DisplayFontShim /> <DisplayFontShim />
<DisplayLigaturesShim /> <DisplayLigaturesShim />
<DisplayEmojiShim />*/} <DisplayEmojiShim />
{/*<CollapsibleSection <CollapsibleSection
defaultValue={false} defaultValue={false}
id="settings_overrides" id="settings_overrides"
summary={<Text id="app.settings.pages.appearance.overrides" />}> summary={<Text id="app.settings.pages.appearance.overrides" />}>
<div className={styles.actions}> <ThemeTools />
<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" />}>
{" "}
{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>
<h3>App</h3> <h3>App</h3>
<div className={styles.overrides}> <ThemeOverrides />
{( </CollapsibleSection>
[
"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.getVariable(x) }}>
<div className={styles.input}>
<input
type="color"
value={theme.getVariable(x)}
onChange={(v) =>
setOverride({
[x]: v.currentTarget.value,
})
}
/>
</div>
<span
style={`color: ${getContrastingColour(
theme.getVariable(x),
theme.getVariable("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.getVariable(x)}
onChange={(y) =>
setOverride({
[x]: y.currentTarget.value,
})
}
/>
</div>
</div>
))}
</div>
</CollapsibleSection>*/}
<CollapsibleSection <CollapsibleSection
id="settings_advanced_appearance" id="settings_advanced_appearance"
@ -238,19 +50,4 @@ export const Component = observer((props: Props) => {
); );
}); });
export const Appearance = connectState(Component, (state) => { // <DisplayCompactShim />
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";
}

View file

@ -15,6 +15,8 @@ import Tip from "../../../components/ui/Tip";
const constraints = { audio: true }; const constraints = { audio: true };
// TODO: do not rewrite this code until voice is rewritten!
export function Component() { export function Component() {
const [mediaStream, setMediaStream] = useState<MediaStream | undefined>( const [mediaStream, setMediaStream] = useState<MediaStream | undefined>(
undefined, undefined,
@ -57,11 +59,11 @@ export function Component() {
return () => { return () => {
if (mediaStream) { if (mediaStream) {
// close microphone access on unmount // close microphone access on unmount
mediaStream.getTracks().forEach(track => { mediaStream.getTracks().forEach((track) => {
track.stop() track.stop();
}) });
}
} }
};
}, [mediaStream]); }, [mediaStream]);
useEffect(() => { useEffect(() => {

View file

@ -232,97 +232,6 @@
display: flex; display: flex;
flex-direction: column; 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 { .sessions {

View file

@ -1,6 +1,6 @@
import type { Theme, ThemeOptions } from "../../context/Theme"; import type { Theme, ThemeOptions } from "../../context/Theme";
import { setEmojiPack } from "../../components/common/Emoji"; import { setGlobalEmojiPack } from "../../components/common/Emoji";
import type { Sounds } from "../../assets/sounds/Audio"; import type { Sounds } from "../../assets/sounds/Audio";
import type { SyncUpdateAction } from "./sync"; import type { SyncUpdateAction } from "./sync";
@ -59,7 +59,7 @@ export function settings(
state = {} as Settings, state = {} as Settings,
action: SettingsAction, action: SettingsAction,
): Settings { ): Settings {
setEmojiPack(state.appearance?.emojiPack ?? "mutant"); // setGlobalEmojiPack(state.appearance?.emojiPack ?? "mutant");
switch (action.type) { switch (action.type) {
case "SETTINGS_SET_THEME": case "SETTINGS_SET_THEME":