chore: clean up contrasting colours code

This commit is contained in:
Paul 2021-12-24 14:13:10 +00:00
parent a46fbcf409
commit fee56d8f54
9 changed files with 141 additions and 34 deletions

View file

@ -8,12 +8,17 @@ import { Text } from "preact-i18n";
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
import { getRenderer } from "../../../../lib/renderer/Singleton"; import { getRenderer } from "../../../../lib/renderer/Singleton";
const Bar = styled.div` export const Bar = styled.div<{ position: "top" | "bottom"; accent?: boolean }>`
z-index: 10; z-index: 10;
position: relative; position: relative;
> div { > div {
top: -26px; ${(props) =>
props.position === "bottom" &&
css`
top: -26px;
`}
height: 28px; height: 28px;
width: 100%; width: 100%;
position: absolute; position: absolute;
@ -24,10 +29,29 @@ const Bar = styled.div`
padding: 0 8px; padding: 0 8px;
user-select: none; user-select: none;
justify-content: space-between; justify-content: space-between;
color: var(--secondary-foreground);
transition: color ease-in-out 0.08s; transition: color ease-in-out 0.08s;
background: var(--secondary-background);
border-radius: var(--border-radius) var(--border-radius) 0 0; ${(props) =>
props.accent
? css`
color: var(--accent-contrast);
background: var(--accent);
`
: css`
color: var(--secondary-foreground);
background: var(--secondary-background);
`}
${(props) =>
props.position === "top"
? css`
border-radius: 0 0 var(--border-radius)
var(--border-radius);
`
: css`
border-radius: var(--border-radius) var(--border-radius) 0
0;
`}
> div { > div {
display: flex; display: flex;
@ -58,7 +82,7 @@ export default observer(({ channel }: { channel: Channel }) => {
if (renderer.state !== "RENDER" || renderer.atBottom) return null; if (renderer.state !== "RENDER" || renderer.atBottom) return null;
return ( return (
<Bar> <Bar position="bottom">
<div onClick={() => renderer.jumpToBottom(true)}> <div onClick={() => renderer.jumpToBottom(true)}>
<div> <div>
<Text id="app.main.channel.misc.viewing_old" /> <Text id="app.main.channel.misc.viewing_old" />

View file

@ -0,0 +1,47 @@
import { UpArrowAlt } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { Channel } from "revolt.js/dist/maps/Channels";
import { decodeTime } from "ulid";
import { getRenderer } from "../../../../lib/renderer/Singleton";
import { dayjs } from "../../../../context/Locale";
import { Bar } from "./JumpToBottom";
export default observer(
({ channel, last_id }: { channel: Channel; last_id?: string }) => {
const renderer = getRenderer(channel);
const history = useHistory();
if (renderer.state !== "RENDER") return null;
if (!last_id) return null;
return (
<>
<Bar position="top" accent>
<div
onClick={() => {
if (channel.channel_type === "TextChannel") {
history.push(
`/server/${channel.server_id}/channel/${channel._id}/${last_id}`,
);
} else {
history.push(
`/channel/${channel._id}/${last_id}`,
);
}
}}>
<div>
New messages since{" "}
{dayjs(decodeTime(last_id)).fromNow()}
</div>
<div>
Click to jump to start. <UpArrowAlt size={20} />
</div>
</div>
</Bar>
</>
);
},
);

View file

@ -24,7 +24,7 @@ const BotBadge = styled.div`
margin-inline-start: 2px; margin-inline-start: 2px;
text-transform: uppercase; text-transform: uppercase;
color: var(--foreground); color: var(--accent-contrast);
background: var(--accent); background: var(--accent);
border-radius: calc(var(--border-radius) / 2); border-radius: calc(var(--border-radius) / 2);
`; `;

View file

@ -134,8 +134,8 @@ export default observer(() => {
</div> </div>
<span <span
style={{ style={{
color: getContrastingColour( color: theme.getContrastingVariable(
theme.getVariable(key), key,
theme.getVariable("primary-background"), theme.getVariable("primary-background"),
), ),
}}> }}>
@ -168,14 +168,3 @@ export default observer(() => {
</Container> </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

@ -102,23 +102,22 @@ interface PageHeaderProps {
export const PageHeader = observer( export const PageHeader = observer(
({ children, icon, noBurger }: PageHeaderProps) => { ({ children, icon, noBurger }: PageHeaderProps) => {
const layout = useApplicationState().layout; const layout = useApplicationState().layout;
const visible = layout.getSectionState(SIDEBAR_CHANNELS, true);
return ( return (
<Header placement="primary"> <Header placement="primary" borders={!visible}>
{!noBurger && <HamburgerAction />} {!noBurger && <HamburgerAction />}
<IconContainer <IconContainer
onClick={() => onClick={() =>
layout.toggleSectionState(SIDEBAR_CHANNELS, true) layout.toggleSectionState(SIDEBAR_CHANNELS, true)
}> }>
{!isTouchscreenDevice && {!isTouchscreenDevice && visible && (
layout.getSectionState(SIDEBAR_CHANNELS, true) && ( <ChevronLeft size={18} />
<ChevronLeft size={18} /> )}
)}
{icon} {icon}
{!isTouchscreenDevice && {!isTouchscreenDevice && !visible && (
!layout.getSectionState(SIDEBAR_CHANNELS, true) && ( <ChevronRight size={18} />
<ChevronRight size={18} /> )}
)}
</IconContainer> </IconContainer>
{children} {children}
</Header> </Header>

View file

@ -279,7 +279,6 @@ export const PRESETS: Record<string, Theme> = {
}, },
}; };
const keys = Object.keys(PRESETS.dark);
const GlobalTheme = createGlobalStyle<{ theme: Theme }>` const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
:root { :root {
${(props) => generateVariables(props.theme)} ${(props) => generateVariables(props.theme)}
@ -288,7 +287,6 @@ const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
export const generateVariables = (theme: Theme) => { export const generateVariables = (theme: Theme) => {
return (Object.keys(theme) as Variables[]).map((key) => { return (Object.keys(theme) as Variables[]).map((key) => {
if (!keys.includes(key)) return;
return `--${key}: ${theme[key]};`; return `--${key}: ${theme[key]};`;
}); });
}; };
@ -326,7 +324,7 @@ export default observer(() => {
return () => window.removeEventListener("resize", resize); return () => window.removeEventListener("resize", resize);
}, [root]); }, [root]);
const variables = theme.getVariables(); const variables = theme.computeVariables();
return ( return (
<> <>
<Helmet> <Helmet>

View file

@ -96,6 +96,22 @@ export default class STheme {
}; };
} }
@computed computeVariables(): Theme {
const variables = this.getVariables() as Record<
string,
string | boolean
>;
for (const key of Object.keys(variables)) {
const value = variables[key];
if (typeof value === "string") {
variables[key + "-contrast"] = getContrastingColour(value);
}
}
return variables as unknown as Theme;
}
@action setVariable(key: Variables, value: string) { @action setVariable(key: Variables, value: string) {
this.settings.set("appearance:theme:overrides", { this.settings.set("appearance:theme:overrides", {
...this.settings.get("appearance:theme:overrides"), ...this.settings.get("appearance:theme:overrides"),
@ -113,6 +129,15 @@ export default class STheme {
PRESETS[this.getBase()]?.[key])!; PRESETS[this.getBase()]?.[key])!;
} }
/**
* Get the contrasting colour of a variable by its key.
* @param key Variable
* @returns Contrasting value
*/
@computed getContrastingVariable(key: Variables, fallback?: string) {
return getContrastingColour(this.getVariable(key), fallback);
}
@action setFont(font: Fonts) { @action setFont(font: Fonts) {
this.settings.set("appearance:theme:font", font); this.settings.set("appearance:theme:font", font);
} }
@ -175,3 +200,17 @@ export default class STheme {
this.settings.remove("appearance:theme:css"); this.settings.remove("appearance:theme:css");
} }
} }
function getContrastingColour(hex: string, fallback = "black"): string {
// TODO: Switch to color-parse
// Try parse hex value.
hex = hex.replace("#", "");
const r = parseInt(hex.substr(0, 2), 16) / 255;
const g = parseInt(hex.substr(2, 2), 16) / 255;
const b = parseInt(hex.substr(4, 2), 16) / 255;
if (isNaN(r) || isNaN(g) || isNaN(b))
return fallback ? getContrastingColour(fallback) : "black";
return r * 0.299 + g * 0.587 + b * 0.114 >= 0.186 ? "black" : "white";
}

View file

@ -7,7 +7,7 @@ import { Channel as ChannelI } from "revolt.js/dist/maps/Channels";
import styled from "styled-components"; import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useEffect } from "preact/hooks"; import { useEffect, useMemo } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
@ -19,6 +19,7 @@ import { useClient } from "../../context/revoltjs/RevoltClient";
import AgeGate from "../../components/common/AgeGate"; import AgeGate from "../../components/common/AgeGate";
import MessageBox from "../../components/common/messaging/MessageBox"; import MessageBox from "../../components/common/messaging/MessageBox";
import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom"; import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom";
import NewMessages from "../../components/common/messaging/bars/NewMessages";
import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator"; import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator";
import Header, { PageHeader } from "../../components/ui/Header"; import Header, { PageHeader } from "../../components/ui/Header";
@ -87,6 +88,15 @@ export function Channel({ id }: { id: string }) {
const TextChannel = observer(({ channel }: { channel: ChannelI }) => { const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
const layout = useApplicationState().layout; const layout = useApplicationState().layout;
// Cache the unread location.
const last_id = useMemo(
() =>
(channel.unread
? channel.client.unreads?.getUnread(channel._id)?.last_id
: undefined) ?? undefined,
[channel],
);
// Mark channel as read. // Mark channel as read.
useEffect(() => { useEffect(() => {
const checkUnread = () => const checkUnread = () =>
@ -119,6 +129,7 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
<ChannelMain> <ChannelMain>
<ChannelContent> <ChannelContent>
<VoiceHeader id={channel._id} /> <VoiceHeader id={channel._id} />
<NewMessages channel={channel} last_id={last_id} />
<MessageArea channel={channel} /> <MessageArea channel={channel} />
<TypingIndicator channel={channel} /> <TypingIndicator channel={channel} />
<JumpToBottom channel={channel} /> <JumpToBottom channel={channel} />

View file

@ -56,7 +56,7 @@ const BotBadge = styled.div`
margin-inline-start: 2px; margin-inline-start: 2px;
text-transform: uppercase; text-transform: uppercase;
color: var(--foreground); color: var(--accent-contrast);
background: var(--accent); background: var(--accent);
border-radius: calc(var(--border-radius) / 2); border-radius: calc(var(--border-radius) / 2);
`; `;