diff --git a/src/components/common/messaging/bars/JumpToBottom.tsx b/src/components/common/messaging/bars/JumpToBottom.tsx index bde2f086..a3074748 100644 --- a/src/components/common/messaging/bars/JumpToBottom.tsx +++ b/src/components/common/messaging/bars/JumpToBottom.tsx @@ -8,12 +8,17 @@ import { Text } from "preact-i18n"; import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice"; import { getRenderer } from "../../../../lib/renderer/Singleton"; -const Bar = styled.div` +export const Bar = styled.div<{ position: "top" | "bottom"; accent?: boolean }>` z-index: 10; position: relative; > div { - top: -26px; + ${(props) => + props.position === "bottom" && + css` + top: -26px; + `} + height: 28px; width: 100%; position: absolute; @@ -24,10 +29,29 @@ const Bar = styled.div` padding: 0 8px; user-select: none; justify-content: space-between; - color: var(--secondary-foreground); 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 { display: flex; @@ -58,7 +82,7 @@ export default observer(({ channel }: { channel: Channel }) => { if (renderer.state !== "RENDER" || renderer.atBottom) return null; return ( - +
renderer.jumpToBottom(true)}>
diff --git a/src/components/common/messaging/bars/NewMessages.tsx b/src/components/common/messaging/bars/NewMessages.tsx new file mode 100644 index 00000000..483a1577 --- /dev/null +++ b/src/components/common/messaging/bars/NewMessages.tsx @@ -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 ( + <> + +
{ + if (channel.channel_type === "TextChannel") { + history.push( + `/server/${channel.server_id}/channel/${channel._id}/${last_id}`, + ); + } else { + history.push( + `/channel/${channel._id}/${last_id}`, + ); + } + }}> +
+ New messages since{" "} + {dayjs(decodeTime(last_id)).fromNow()} +
+
+ Click to jump to start. +
+
+
+ + ); + }, +); diff --git a/src/components/common/user/UserShort.tsx b/src/components/common/user/UserShort.tsx index 7a7155f0..473ef660 100644 --- a/src/components/common/user/UserShort.tsx +++ b/src/components/common/user/UserShort.tsx @@ -24,7 +24,7 @@ const BotBadge = styled.div` margin-inline-start: 2px; text-transform: uppercase; - color: var(--foreground); + color: var(--accent-contrast); background: var(--accent); border-radius: calc(var(--border-radius) / 2); `; diff --git a/src/components/settings/appearance/ThemeOverrides.tsx b/src/components/settings/appearance/ThemeOverrides.tsx index aaee68b8..cf573a35 100644 --- a/src/components/settings/appearance/ThemeOverrides.tsx +++ b/src/components/settings/appearance/ThemeOverrides.tsx @@ -134,8 +134,8 @@ export default observer(() => {
@@ -168,14 +168,3 @@ export default observer(() => { ); }); - -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"; -} diff --git a/src/components/ui/Header.tsx b/src/components/ui/Header.tsx index 18ee8d03..48b1062b 100644 --- a/src/components/ui/Header.tsx +++ b/src/components/ui/Header.tsx @@ -102,23 +102,22 @@ interface PageHeaderProps { export const PageHeader = observer( ({ children, icon, noBurger }: PageHeaderProps) => { const layout = useApplicationState().layout; + const visible = layout.getSectionState(SIDEBAR_CHANNELS, true); return ( -
+
{!noBurger && } layout.toggleSectionState(SIDEBAR_CHANNELS, true) }> - {!isTouchscreenDevice && - layout.getSectionState(SIDEBAR_CHANNELS, true) && ( - - )} + {!isTouchscreenDevice && visible && ( + + )} {icon} - {!isTouchscreenDevice && - !layout.getSectionState(SIDEBAR_CHANNELS, true) && ( - - )} + {!isTouchscreenDevice && !visible && ( + + )} {children}
diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx index 2c69ac9f..e5697881 100644 --- a/src/context/Theme.tsx +++ b/src/context/Theme.tsx @@ -279,7 +279,6 @@ export const PRESETS: Record = { }, }; -const keys = Object.keys(PRESETS.dark); const GlobalTheme = createGlobalStyle<{ theme: Theme }>` :root { ${(props) => generateVariables(props.theme)} @@ -288,7 +287,6 @@ const GlobalTheme = createGlobalStyle<{ theme: Theme }>` export const generateVariables = (theme: Theme) => { return (Object.keys(theme) as Variables[]).map((key) => { - if (!keys.includes(key)) return; return `--${key}: ${theme[key]};`; }); }; @@ -326,7 +324,7 @@ export default observer(() => { return () => window.removeEventListener("resize", resize); }, [root]); - const variables = theme.getVariables(); + const variables = theme.computeVariables(); return ( <> diff --git a/src/mobx/stores/helpers/STheme.ts b/src/mobx/stores/helpers/STheme.ts index 93257b7e..4316a91e 100644 --- a/src/mobx/stores/helpers/STheme.ts +++ b/src/mobx/stores/helpers/STheme.ts @@ -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) { this.settings.set("appearance:theme:overrides", { ...this.settings.get("appearance:theme:overrides"), @@ -113,6 +129,15 @@ export default class STheme { 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) { this.settings.set("appearance:theme:font", font); } @@ -175,3 +200,17 @@ export default class STheme { 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"; +} diff --git a/src/pages/channels/Channel.tsx b/src/pages/channels/Channel.tsx index 4364c206..17efb460 100644 --- a/src/pages/channels/Channel.tsx +++ b/src/pages/channels/Channel.tsx @@ -7,7 +7,7 @@ import { Channel as ChannelI } from "revolt.js/dist/maps/Channels"; import styled from "styled-components"; import { Text } from "preact-i18n"; -import { useEffect } from "preact/hooks"; +import { useEffect, useMemo } from "preact/hooks"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; @@ -19,6 +19,7 @@ import { useClient } from "../../context/revoltjs/RevoltClient"; import AgeGate from "../../components/common/AgeGate"; import MessageBox from "../../components/common/messaging/MessageBox"; 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 Header, { PageHeader } from "../../components/ui/Header"; @@ -87,6 +88,15 @@ export function Channel({ id }: { id: string }) { const TextChannel = observer(({ channel }: { channel: ChannelI }) => { 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. useEffect(() => { const checkUnread = () => @@ -119,6 +129,7 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => { + diff --git a/src/pages/settings/panes/MyBots.tsx b/src/pages/settings/panes/MyBots.tsx index 99ac70b5..3e68463f 100644 --- a/src/pages/settings/panes/MyBots.tsx +++ b/src/pages/settings/panes/MyBots.tsx @@ -56,7 +56,7 @@ const BotBadge = styled.div` margin-inline-start: 2px; text-transform: uppercase; - color: var(--foreground); + color: var(--accent-contrast); background: var(--accent); border-radius: calc(var(--border-radius) / 2); `;