chore: merge branch 'ui/glass-header'

This commit is contained in:
Paul Makles 2022-01-02 23:48:19 +00:00
commit c26ef4c2b2
45 changed files with 863 additions and 396 deletions

2
external/lang vendored

@ -1 +1 @@
Subproject commit 35894f6b1ecadc9df4ab77a5de9c9d5256a36582 Subproject commit 1d3e85e7f6d0ad7a590854e240d7c47291f3e2cf

View file

@ -59,6 +59,7 @@
} }
}, },
"dependencies": { "dependencies": {
"color-rgba": "^2.3.0",
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"klaw": "^3.0.0", "klaw": "^3.0.0",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
@ -95,6 +96,7 @@
"@traptitech/markdown-it-katex": "^3.4.3", "@traptitech/markdown-it-katex": "^3.4.3",
"@traptitech/markdown-it-spoiler": "^1.1.6", "@traptitech/markdown-it-spoiler": "^1.1.6",
"@trivago/prettier-plugin-sort-imports": "^2.0.2", "@trivago/prettier-plugin-sort-imports": "^2.0.2",
"@types/color-rgba": "^2.1.0",
"@types/lodash.defaultsdeep": "^4.6.6", "@types/lodash.defaultsdeep": "^4.6.6",
"@types/lodash.isequal": "^4.5.5", "@types/lodash.isequal": "^4.5.5",
"@types/markdown-it": "^12.0.2", "@types/markdown-it": "^12.0.2",

View file

@ -673,6 +673,7 @@ export const emojiDictionary = {
mandarin: "🍊", mandarin: "🍊",
lemon: "🍋", lemon: "🍋",
banana: "🍌", banana: "🍌",
nanner: "🍌",
pineapple: "🍍", pineapple: "🍍",
mango: "🥭", mango: "🥭",
apple: "🍎", apple: "🍎",
@ -876,6 +877,7 @@ export const emojiDictionary = {
train: "🚋", train: "🚋",
bus: "🚌", bus: "🚌",
oncoming_bus: "🚍", oncoming_bus: "🚍",
trolley: "🚎",
trolleybus: "🚎", trolleybus: "🚎",
minibus: "🚐", minibus: "🚐",
ambulance: "🚑", ambulance: "🚑",

View file

@ -4,84 +4,135 @@ import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { ServerPermission } from "revolt.js/dist/api/permissions"; import { ServerPermission } from "revolt.js/dist/api/permissions";
import { Server } from "revolt.js/dist/maps/Servers"; import { Server } from "revolt.js/dist/maps/Servers";
import styled from "styled-components"; import styled, { css } from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import Header from "../ui/Header"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import IconButton from "../ui/IconButton"; import IconButton from "../ui/IconButton";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
interface Props { interface Props {
server: Server; server: Server;
background?: boolean;
} }
const ServerName = styled.div` const ServerBanner = styled.div<Omit<Props, "server">>`
flex-grow: 1; flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: end;
background-size: cover;
background-repeat: norepeat;
background-position: center center;
${(props) =>
props.background
? css`
height: 120px;
.container {
background: linear-gradient(
0deg,
var(--secondary-background),
transparent
);
}
`
: css`
background-color: var(--secondary-header);
`}
.container {
height: var(--header-height);
display: flex;
align-items: center;
padding: 0 14px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
gap: 8px;
.title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
}
}
`; `;
export default observer(({ server }: Props) => { export default observer(({ server }: Props) => {
const bannerURL = server.generateBannerURL({ width: 480 }); const bannerURL = server.generateBannerURL({ width: 480 });
return ( return (
<Header <ServerBanner
borders
placement="secondary"
background={typeof bannerURL !== "undefined"} background={typeof bannerURL !== "undefined"}
style={{ style={{
background: bannerURL ? `url('${bannerURL}')` : undefined, backgroundImage: bannerURL ? `url('${bannerURL}')` : undefined,
}}> }}>
{server.flags && server.flags & 1 ? ( <div className="container">
<Tooltip {server.flags && server.flags & 1 ? (
content={<Text id="app.special.server-badges.official" />} <Tooltip
placement={"bottom-start"}> content={
<svg width="20" height="20"> <Text id="app.special.server-badges.official" />
<image }
xlinkHref="/assets/badges/verified.svg" placement={"bottom-start"}>
height="20" <svg width="20" height="20">
width="20" <image
/> xlinkHref="/assets/badges/verified.svg"
<image height="20"
xlinkHref="/assets/badges/revolt_r.svg" width="20"
height="15" />
width="15" <image
x="2" xlinkHref="/assets/badges/revolt_r.svg"
y="3" height="15"
style={ width="15"
"justify-content: center; align-items: center; filter: brightness(0);" x="2"
} y="3"
/> style={
</svg> "justify-content: center; align-items: center; filter: brightness(0);"
</Tooltip> }
) : undefined} />
{server.flags && server.flags & 2 ? ( </svg>
<Tooltip </Tooltip>
content={<Text id="app.special.server-badges.verified" />} ) : undefined}
placement={"bottom-start"}> {server.flags && server.flags & 2 ? (
<svg width="20" height="20"> <Tooltip
<image content={
xlinkHref="/assets/badges/verified.svg" <Text id="app.special.server-badges.verified" />
height="20" }
width="20" placement={"bottom-start"}>
/> <svg width="20" height="20">
<foreignObject x="2" y="2" width="15" height="15"> <image
<Check size={15} color="black" strokeWidth={8} /> xlinkHref="/assets/badges/verified.svg"
</foreignObject> height="20"
</svg> width="20"
</Tooltip> />
) : undefined} <foreignObject x="2" y="2" width="15" height="15">
<Check
<ServerName>{server.name}</ServerName> size={15}
{(server.permission & ServerPermission.ManageServer) > 0 && ( color="black"
<div className="actions"> strokeWidth={8}
/>
</foreignObject>
</svg>
</Tooltip>
) : undefined}
<div className="title">{server.name}</div>
{(server.permission & ServerPermission.ManageServer) > 0 && (
<Link to={`/server/${server._id}/settings`}> <Link to={`/server/${server._id}/settings`}>
<IconButton> <IconButton>
<Cog size={24} /> <Cog size={20} />
</IconButton> </IconButton>
</Link> </Link>
</div> )}
)} </div>
</Header> </ServerBanner>
); );
}); });

View file

@ -80,7 +80,7 @@ const Blocked = styled.div`
color: var(--tertiary-foreground); color: var(--tertiary-foreground);
.text { .text {
padding: 14px 14px 14px 0; padding: 14px;
} }
svg { svg {
@ -89,13 +89,17 @@ const Blocked = styled.div`
`; `;
const Action = styled.div` const Action = styled.div`
display: flex;
place-items: center;
> div { > div {
height: 48px; height: 48px;
width: 48px; width: 34px;
padding: 12px; display: flex;
align-items: center;
justify-content: end;
/*padding: 14px 0 14px 14px;*/
}
.mobile {
justify-content: start;
} }
${() => ${() =>

View file

@ -13,19 +13,25 @@ export const Bar = styled.div<{ position: "top" | "bottom"; accent?: boolean }>`
z-index: 10; z-index: 10;
position: relative; position: relative;
${(props) =>
props.position === "top" &&
css`
top: 0;
`}
${(props) =>
props.position === "bottom" &&
css`
top: 65px;
${() =>
isTouchscreenDevice &&
css`
top: -90px;
`}
`}
> div { > div {
${(props) =>
props.position === "bottom" &&
css`
top: -26px;
${() =>
isTouchscreenDevice &&
css`
top: -32px;
`}
`}
height: 28px; height: 28px;
width: 100%; width: 100%;
position: absolute; position: absolute;
@ -52,6 +58,7 @@ export const Bar = styled.div<{ position: "top" | "bottom"; accent?: boolean }>`
${(props) => ${(props) =>
props.position === "top" props.position === "top"
? css` ? css`
top: 48px;
border-radius: 0 0 var(--border-radius) border-radius: 0 0 var(--border-radius)
var(--border-radius); var(--border-radius);
` `
@ -60,6 +67,12 @@ export const Bar = styled.div<{ position: "top" | "bottom"; accent?: boolean }>`
0; 0;
`} `}
${() =>
isTouchscreenDevice &&
css`
top: 56px;
`}
> div { > div {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -49,8 +49,12 @@ export default observer(
} }
}}> }}>
<div> <div>
New messages since{" "} <Text
{dayjs(decodeTime(last_id)).fromNow()} id="app.main.channel.misc.new_messages"
fields={{
time_ago: dayjs(decodeTime(last_id)).fromNow(),
}}
/>
</div> </div>
<div> <div>
<Text id="app.main.channel.misc.jump_beginning" /> <Text id="app.main.channel.misc.jump_beginning" />

View file

@ -25,7 +25,11 @@ const Base = styled.div`
flex-direction: row; flex-direction: row;
width: calc(100% - var(--scrollbar-thickness)); width: calc(100% - var(--scrollbar-thickness));
color: var(--secondary-foreground); color: var(--secondary-foreground);
background: var(--secondary-background); background-color: rgba(
var(--secondary-background-rgb),
max(var(--min-opacity), 0.75)
);
backdrop-filter: blur(10px);
} }
.avatars { .avatars {

View file

@ -1,3 +1,4 @@
import { Group } from "@styled-icons/boxicons-solid";
import { autorun } from "mobx"; import { autorun } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
@ -47,7 +48,7 @@ const EmbedInviteBase = styled.div`
const EmbedInviteDetails = styled.div` const EmbedInviteDetails = styled.div`
flex-grow: 1; flex-grow: 1;
padding-left: 12px; padding-inline-start: 12px;
${() => ${() =>
isTouchscreenDevice && isTouchscreenDevice &&
css` css`
@ -63,7 +64,14 @@ const EmbedInviteName = styled.div`
`; `;
const EmbedInviteMemberCount = styled.div` const EmbedInviteMemberCount = styled.div`
display: flex;
align-items: center;
gap: 2px;
font-size: 0.8em; font-size: 0.8em;
> svg {
color: var(--secondary-foreground);
}
`; `;
type Props = { type Props = {
@ -119,6 +127,7 @@ export function EmbedInvite({ code }: Props) {
<EmbedInviteDetails> <EmbedInviteDetails>
<EmbedInviteName>{invite.server_name}</EmbedInviteName> <EmbedInviteName>{invite.server_name}</EmbedInviteName>
<EmbedInviteMemberCount> <EmbedInviteMemberCount>
<Group size={12} />
{invite.member_count.toLocaleString()}{" "} {invite.member_count.toLocaleString()}{" "}
{invite.member_count === 1 ? "member" : "members"} {invite.member_count === 1 ? "member" : "members"}
</EmbedInviteMemberCount> </EmbedInviteMemberCount>

View file

@ -17,10 +17,10 @@ const Base = styled.div`
`; `;
const Navbar = styled.div` const Navbar = styled.div`
z-index: 100; z-index: 500;
max-width: 500px;
margin: 0 auto;
display: flex; display: flex;
margin: 0 auto;
max-width: 500px;
height: var(--bottom-navigation-height); height: var(--bottom-navigation-height);
`; `;
@ -71,7 +71,12 @@ export default observer(() => {
} }
} }
history.push(layout.getLastHomePath()); const path = layout.getLastHomePath();
if (path === "/friends") {
history.push("/");
} else {
history.push(path);
}
}}> }}>
<Message size={24} /> <Message size={24} />
</IconButton> </IconButton>

View file

@ -8,6 +8,13 @@ export default styled.div`
user-select: none; user-select: none;
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
/*background: var(--background);*/
background-color: rgba(
var(--background-rgb),
max(var(--min-opacity), 0.75)
);
backdrop-filter: blur(20px);
`; `;
export const GenericSidebarBase = styled.div<{ export const GenericSidebarBase = styled.div<{
@ -21,10 +28,15 @@ export const GenericSidebarBase = styled.div<{
/*border-end-start-radius: 8px;*/ /*border-end-start-radius: 8px;*/
background: var(--secondary-background); background: var(--secondary-background);
> :nth-child(1) { /*> :nth-child(1) {
border-end-start-radius: 8px; //border-end-start-radius: 8px;
} }
> :nth-child(2) {
margin-top: 48px;
background: red;
}*/
${(props) => ${(props) =>
props.mobilePadding && props.mobilePadding &&
isTouchscreenDevice && isTouchscreenDevice &&

View file

@ -1,4 +1,5 @@
import { X, Crown } from "@styled-icons/boxicons-regular"; import { X } from "@styled-icons/boxicons-regular";
import { Crown } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Presence } from "revolt-api/types/Users"; import { Presence } from "revolt-api/types/Users";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";

View file

@ -7,6 +7,7 @@ import {
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link, useLocation, useParams } from "react-router-dom"; import { Link, useLocation, useParams } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users"; import { RelationshipStatus } from "revolt-api/types/Users";
import styled, { css } from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect } from "preact/hooks"; import { useContext, useEffect } from "preact/hooks";
@ -27,6 +28,21 @@ import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
import ButtonItem, { ChannelButton } from "../items/ButtonItem"; import ButtonItem, { ChannelButton } from "../items/ButtonItem";
import ConnectionStatus from "../items/ConnectionStatus"; import ConnectionStatus from "../items/ConnectionStatus";
const Navbar = styled.div`
display: flex;
align-items: center;
padding: 0 14px;
font-weight: 600;
flex-shrink: 0;
height: 48px;
${() =>
isTouchscreenDevice &&
css`
height: 56px;
`}
`;
export default observer(() => { export default observer(() => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const client = useContext(AppContext); const client = useContext(AppContext);
@ -55,6 +71,9 @@ export default observer(() => {
return ( return (
<GenericSidebarBase mobilePadding> <GenericSidebarBase mobilePadding>
<Navbar>
<Text id="app.home.directs" />
</Navbar>
<ConnectionStatus /> <ConnectionStatus />
<GenericSidebarList> <GenericSidebarList>
<ConditionalLink active={pathname === "/"} to="/"> <ConditionalLink active={pathname === "/"} to="/">

View file

@ -95,6 +95,7 @@ const ServerList = styled.div`
overflow-y: scroll; overflow-y: scroll;
padding-bottom: 20px; padding-bottom: 20px;
flex-direction: column; flex-direction: column;
margin-top: -2px;
scrollbar-width: none; scrollbar-width: none;
@ -168,6 +169,7 @@ const ServerCircle = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0;
.circle { .circle {
display: flex; display: flex;
@ -384,7 +386,7 @@ export default observer(() => {
</div> </div>
</Tooltip> </Tooltip>
</ServerCircle> </ServerCircle>
<ServerCircle> {/*<ServerCircle>
<Tooltip <Tooltip
content={ content={
<div <div
@ -394,14 +396,13 @@ export default observer(() => {
gap: "4px", gap: "4px",
}}> }}>
<div>Discover Public Servers</div> <div>Discover Public Servers</div>
<LinkExternal size={12} />
</div> </div>
} }
placement="right"> placement="right">
<div className="circle"> <div className="circle">
<IconButton> <IconButton>
<a <a
href="https://revolt.social" href="#"
target="_blank" target="_blank"
rel="noreferrer"> rel="noreferrer">
<Compass size={32} /> <Compass size={32} />
@ -409,7 +410,7 @@ export default observer(() => {
</IconButton> </IconButton>
</div> </div>
</Tooltip> </Tooltip>
</ServerCircle> </ServerCircle>*/}
</ServerList> </ServerList>
<PaintCounter small /> <PaintCounter small />
{!isTouchscreenDevice && ( {!isTouchscreenDevice && (

View file

@ -1,5 +1,6 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Redirect, useParams } from "react-router"; import { Redirect, useParams } from "react-router";
import { Server } from "revolt.js/dist/maps/Servers";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { attachContextMenu } from "preact-context-menu"; import { attachContextMenu } from "preact-context-menu";
@ -48,6 +49,10 @@ const ServerList = styled.div`
} }
`; `;
interface Props {
server: Server;
}
export default observer(() => { export default observer(() => {
const client = useClient(); const client = useClient();
const state = useApplicationState(); const state = useApplicationState();

View file

@ -6,9 +6,12 @@ import { Presence } from "revolt-api/types/Users";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Server } from "revolt.js/dist/maps/Servers"; import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users"; import { User } from "revolt.js/dist/maps/Users";
import styled, { css } from "styled-components";
import { useContext, useEffect, useMemo } from "preact/hooks"; import { useContext, useEffect, useMemo } from "preact/hooks";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { import {
ClientStatus, ClientStatus,
StatusContext, StatusContext,
@ -18,6 +21,15 @@ import {
import { GenericSidebarBase } from "../SidebarBase"; import { GenericSidebarBase } from "../SidebarBase";
import MemberList, { MemberListGroup } from "./MemberList"; import MemberList, { MemberListGroup } from "./MemberList";
export const Container = styled.div`
padding-top: 48px;
${isTouchscreenDevice &&
css`
padding-top: 0;
`}
`;
export default function MemberSidebar() { export default function MemberSidebar() {
const channel = useClient().channels.get( const channel = useClient().channels.get(
useParams<{ channel: string }>().channel, useParams<{ channel: string }>().channel,
@ -157,6 +169,10 @@ export const GroupMemberSidebar = observer(
return ( return (
<GenericSidebarBase> <GenericSidebarBase>
<Container>
{/*{isTouchscreenDevice && <div>Group settings go here</div>}*/}
</Container>
<MemberList entries={entries} context={channel} /> <MemberList entries={entries} context={channel} />
</GenericSidebarBase> </GenericSidebarBase>
); );
@ -180,6 +196,9 @@ export const ServerMemberSidebar = observer(
return ( return (
<GenericSidebarBase> <GenericSidebarBase>
<Container>
{/*{isTouchscreenDevice && <div>Server settings go here</div>}*/}
</Container>
<MemberList entries={entries} context={channel} /> <MemberList entries={entries} context={channel} />
</GenericSidebarBase> </GenericSidebarBase>
); );

View file

@ -218,28 +218,32 @@ export const DisplaySeasonalShim = observer(() => {
const settings = useApplicationState().settings; const settings = useApplicationState().settings;
return ( return (
<> <Checkbox
<h3> checked={settings.get("appearance:seasonal") ?? true}
<Text id="app.settings.pages.appearance.theme_options.title" /> onChange={(v) => settings.set("appearance:seasonal", v)}
</h3> description={
{/* TOFIX: WIP feature - follows system theme */} <Text id="app.settings.pages.appearance.theme_options.seasonal_desc" />
{/*<Checkbox }>
checked={settings.get("appearance:seasonal") ?? true} <Text id="app.settings.pages.appearance.theme_options.seasonal" />
onChange={(v) => settings.set("appearance:seasonal", v)} </Checkbox>
description={ );
<Text id="app.settings.pages.appearance.theme_options.follow_desc" /> });
}>
<Text id="app.settings.pages.appearance.theme_options.follow" /> /**
</Checkbox>*/} * Component providing a way to toggle transparency effects.
<Checkbox */
checked={settings.get("appearance:seasonal") ?? true} export const DisplayTransparencyShim = observer(() => {
onChange={(v) => settings.set("appearance:seasonal", v)} const settings = useApplicationState().settings;
description={
<Text id="app.settings.pages.appearance.theme_options.seasonal_desc" /> return (
}> <Checkbox
<Text id="app.settings.pages.appearance.theme_options.seasonal" /> checked={settings.get("appearance:transparency") ?? true}
</Checkbox> onChange={(v) => settings.set("appearance:transparency", v)}
</> description={
<Text id="app.settings.pages.appearance.theme_options.transparency_desc" />
}>
<Text id="app.settings.pages.appearance.theme_options.transparency" />
</Checkbox>
); );
}); });

View file

@ -19,4 +19,8 @@ export default styled.select`
&:focus { &:focus {
box-shadow: 0 0 0 1.5pt var(--accent); box-shadow: 0 0 0 1.5pt var(--accent);
} }
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
`; `;

View file

@ -1,13 +1,20 @@
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
export default styled.details<{ sticky?: boolean; large?: boolean }>` export default styled.details<{ sticky?: boolean; large?: boolean }>`
summary { summary {
${(props) => ${(props) =>
props.sticky && props.sticky &&
css` css`
top: -1px; top: 48px;
z-index: 10; z-index: 10;
position: sticky; position: sticky;
${() =>
isTouchscreenDevice &&
css`
top: 56px;
`}
`} `}
${(props) => ${(props) =>

View file

@ -4,6 +4,7 @@ import {
Menu, Menu,
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useLocation } from "react-router-dom";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
@ -14,14 +15,16 @@ import { SIDEBAR_CHANNELS } from "../../mobx/stores/Layout";
import { Children } from "../../types/Preact"; import { Children } from "../../types/Preact";
interface Props { interface Props {
borders?: boolean; topBorder?: boolean;
bottomBorder?: boolean;
background?: boolean; background?: boolean;
transparent?: boolean;
placement: "primary" | "secondary"; placement: "primary" | "secondary";
} }
const Header = styled.div<Props>` const Header = styled.div<Props>`
gap: 10px; gap: 10px;
height: 48px;
flex: 0 auto; flex: 0 auto;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
@ -29,15 +32,11 @@ const Header = styled.div<Props>`
font-weight: 600; font-weight: 600;
user-select: none; user-select: none;
align-items: center; align-items: center;
height: var(--header-height);
background-size: cover !important; background-size: cover !important;
background-position: center !important; background-position: center !important;
background-color: var(--primary-header);
/*> div {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}*/
svg { svg {
flex-shrink: 0; flex-shrink: 0;
@ -48,11 +47,21 @@ const Header = styled.div<Props>`
color: var(--secondary-foreground); color: var(--secondary-foreground);
} }
${() => ${(props) =>
isTouchscreenDevice && props.transparent
css` ? css`
height: 56px; background-color: rgba(
`} var(--primary-header-rgb),
max(var(--min-opacity), 0.75)
);
backdrop-filter: blur(10px);
z-index: 20;
position: absolute;
width: 100%;
`
: css`
background-color: var(--primary-header);
`}
${(props) => ${(props) =>
props.background && props.background &&
@ -71,10 +80,16 @@ const Header = styled.div<Props>`
`} `}
${(props) => ${(props) =>
props.borders && props.topBorder &&
css` css`
border-start-start-radius: 8px; border-start-start-radius: 8px;
`} `}
${(props) =>
props.bottomBorder &&
css`
border-end-start-radius: 8px;
`}
`; `;
export default Header; export default Header;
@ -98,19 +113,24 @@ const IconContainer = styled.div`
`} `}
`; `;
interface PageHeaderProps { type PageHeaderProps = Omit<Props, "placement" | "borders"> & {
noBurger?: boolean; noBurger?: boolean;
children: Children; children: Children;
icon: Children; icon: Children;
} };
export const PageHeader = observer( export const PageHeader = observer(
({ children, icon, noBurger }: PageHeaderProps) => { ({ children, icon, noBurger, ...props }: PageHeaderProps) => {
const layout = useApplicationState().layout; const layout = useApplicationState().layout;
const visible = layout.getSectionState(SIDEBAR_CHANNELS, true); const visible = layout.getSectionState(SIDEBAR_CHANNELS, true);
const { pathname } = useLocation();
return ( return (
<Header placement="primary" borders={!visible}> <Header
{...props}
placement="primary"
topBorder={!visible}
bottomBorder={!pathname.includes("/server")}>
{!noBurger && <HamburgerAction />} {!noBurger && <HamburgerAction />}
<IconContainer <IconContainer
onClick={() => onClick={() =>
@ -135,7 +155,7 @@ export function HamburgerAction() {
function openSidebar() { function openSidebar() {
document document
.querySelector("#app > div > div") .querySelector("#app > div > div > div")
?.scrollTo({ behavior: "smooth", left: 0 }); ?.scrollTo({ behavior: "smooth", left: 0 });
} }

View file

@ -1,9 +1,9 @@
import styled from "styled-components"; import styled from "styled-components";
export default styled.div` export default styled.div`
height: 0px; height: 0;
opacity: 0.6; opacity: 0.6;
flex-shrink: 0; flex-shrink: 0;
margin: 8px 10px; margin: 8px 15px;
border-top: 1px solid var(--tertiary-foreground); border-top: 1px solid var(--tertiary-foreground);
`; `;

View file

@ -1,3 +1,4 @@
import rgba from "color-rgba";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { createGlobalStyle } from "styled-components"; import { createGlobalStyle } from "styled-components";
@ -71,6 +72,12 @@ export type Theme = Overrides & {
font?: Fonts; font?: Fonts;
css?: string; css?: string;
monospaceFont?: MonospaceFonts; monospaceFont?: MonospaceFonts;
"min-opacity"?: number;
};
export type ComputedVariables = Theme & {
"header-height"?: string;
"effective-bottom-offset"?: string;
}; };
export interface ThemeOptions { export interface ThemeOptions {
@ -287,7 +294,13 @@ 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) => {
return `--${key}: ${theme[key]};`; const colour = rgba(theme[key]);
if (colour) {
const [r, g, b] = colour;
return `--${key}: ${theme[key]}; --${key}-rgb: ${r}, ${g}, ${b};`;
} else {
return `--${key}: ${theme[key]};`;
}
}); });
}; };

View file

@ -1 +1,11 @@
/**
* Schedule a task at the end of the Event Loop
* @param cb Callback
*/
export const defer = (cb: () => void) => setTimeout(cb, 0); export const defer = (cb: () => void) => setTimeout(cb, 0);
/**
* Schedule a task at the end of the second Event Loop
* @param cb Callback
*/
export const chainedDefer = (cb: () => void) => defer(() => defer(cb));

View file

@ -138,6 +138,16 @@ export default class Layout implements Store, Persistent<Data> {
return this.lastHomePath; return this.lastHomePath;
} }
/**
* Get the last path the user had open.
* @returns Last path
*/
@computed getLastPath() {
return this.lastSection === "home"
? this.lastHomePath
: this.getLastOpened(this.lastSection);
}
/** /**
* Set the current path open in the home tab. * Set the current path open in the home tab.
* @param path Pathname * @param path Pathname

View file

@ -29,6 +29,7 @@ export interface ISettings {
"appearance:emoji": EmojiPack; "appearance:emoji": EmojiPack;
"appearance:ligatures": boolean; "appearance:ligatures": boolean;
"appearance:seasonal": boolean; "appearance:seasonal": boolean;
"appearance:transparency": boolean;
"appearance:theme:base": "dark" | "light"; "appearance:theme:base": "dark" | "light";
"appearance:theme:overrides": Partial<Overrides>; "appearance:theme:overrides": Partial<Overrides>;
@ -140,6 +141,7 @@ export default class Settings
if (key === "appearance") { if (key === "appearance") {
this.remove("appearance:emoji"); this.remove("appearance:emoji");
this.remove("appearance:seasonal"); this.remove("appearance:seasonal");
this.remove("appearance:transparency");
} else { } else {
this.remove("appearance:ligatures"); this.remove("appearance:ligatures");
this.remove("appearance:theme:base"); this.remove("appearance:theme:base");
@ -169,6 +171,7 @@ export default class Settings
appearance: this.pullKeys([ appearance: this.pullKeys([
"appearance:emoji", "appearance:emoji",
"appearance:seasonal", "appearance:seasonal",
"appearance:transparency",
]), ]),
theme: this.pullKeys([ theme: this.pullKeys([
"appearance:ligatures", "appearance:ligatures",

View file

@ -1,5 +1,8 @@
import rgba from "color-rgba";
import { makeAutoObservable, computed, action } from "mobx"; import { makeAutoObservable, computed, action } from "mobx";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { import {
Theme, Theme,
PRESETS, PRESETS,
@ -8,6 +11,7 @@ import {
DEFAULT_MONO_FONT, DEFAULT_MONO_FONT,
Fonts, Fonts,
MonospaceFonts, MonospaceFonts,
ComputedVariables,
} from "../../../context/Theme"; } from "../../../context/Theme";
import Settings from "../Settings"; import Settings from "../Settings";
@ -96,10 +100,10 @@ export default class STheme {
}; };
} }
@computed computeVariables(): Theme { @computed computeVariables(): ComputedVariables {
const variables = this.getVariables() as Record< const variables = this.getVariables() as Record<
string, string,
string | boolean string | boolean | number
>; >;
for (const key of Object.keys(variables)) { for (const key of Object.keys(variables)) {
@ -109,7 +113,16 @@ export default class STheme {
} }
} }
return variables as unknown as Theme; return {
...(variables as unknown as Theme),
"min-opacity": this.settings.get("appearance:transparency", true)
? 0
: 1,
"header-height": isTouchscreenDevice ? "56px" : "48px",
"effective-bottom-offset": isTouchscreenDevice
? "var(--bottom-navigation-height)"
: "0px",
};
} }
@action setVariable(key: Variables, value: string) { @action setVariable(key: Variables, value: string) {
@ -204,15 +217,11 @@ export default class STheme {
function getContrastingColour(hex: string, fallback?: string): string { function getContrastingColour(hex: string, fallback?: string): string {
if (typeof hex !== "string") return "black"; if (typeof hex !== "string") return "black";
// TODO: Switch to color-parse const colour = rgba(hex);
// Try parse hex value. if (!colour) return fallback ? getContrastingColour(fallback) : "black";
hex = hex.replace(/#/g, "");
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)) const [r, g, b] = colour;
return fallback ? getContrastingColour(fallback) : "black"; return (r / 255) * 0.299 + (g / 255) * 0.587 + (b / 255) * 0.114 >= 0.186
? "black"
return r * 0.299 + g * 0.587 + b * 0.114 >= 0.186 ? "black" : "white"; : "white";
} }

View file

@ -1,14 +1,16 @@
import { Docked, OverlappingPanels, ShowIf } from "react-overlapping-panels"; import { Docked, OverlappingPanels, ShowIf } from "react-overlapping-panels";
import { Switch, Route, useLocation } from "react-router-dom"; import { Switch, Route, useLocation } from "react-router-dom";
import styled from "styled-components"; import styled, { css } from "styled-components";
import ContextMenus from "../lib/ContextMenus"; import ContextMenus from "../lib/ContextMenus";
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
import { useApplicationState } from "../mobx/State";
import { SIDEBAR_CHANNELS } from "../mobx/stores/Layout";
import Popovers from "../context/intermediate/Popovers"; import Popovers from "../context/intermediate/Popovers";
import Notifications from "../context/revoltjs/Notifications"; import Notifications from "../context/revoltjs/Notifications";
import StateMonitor from "../context/revoltjs/StateMonitor"; import StateMonitor from "../context/revoltjs/StateMonitor";
import SyncManager from "../context/revoltjs/SyncManager";
import { Titlebar } from "../components/native/Titlebar"; import { Titlebar } from "../components/native/Titlebar";
import BottomNavigation from "../components/navigation/BottomNavigation"; import BottomNavigation from "../components/navigation/BottomNavigation";
@ -24,12 +26,54 @@ import ChannelSettings from "./settings/ChannelSettings";
import ServerSettings from "./settings/ServerSettings"; import ServerSettings from "./settings/ServerSettings";
import Settings from "./settings/Settings"; import Settings from "./settings/Settings";
const Routes = styled.div` const AppContainer = styled.div`
background-size: cover !important;
background-position: center center !important;
`;
const StatusBar = styled.div`
height: 40px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
gap: 14px;
.button {
padding: 5px;
border: 1px solid white;
border-radius: var(--border-radius);
}
`;
const Routes = styled.div.attrs({ "data-component": "routes" })<{
borders: boolean;
}>`
min-width: 0; min-width: 0;
display: flex; display: flex;
overflow: hidden; position: relative;
flex-direction: column; flex-direction: column;
background: var(--primary-background); background: var(--primary-background);
/*background-color: rgba(
var(--primary-background-rgb),
max(var(--min-opacity), 0.75)
);*/
//backdrop-filter: blur(10px);
${() =>
isTouchscreenDevice &&
css`
overflow: hidden;
`}
${(props) =>
props.borders &&
css`
border-start-start-radius: 8px;
`}
`; `;
export default function App() { export default function App() {
@ -37,6 +81,7 @@ export default function App() {
const fixedBottomNav = const fixedBottomNav =
path === "/" || path === "/settings" || path.startsWith("/friends"); path === "/" || path === "/settings" || path.startsWith("/friends");
const inChannel = path.includes("/channel"); const inChannel = path.includes("/channel");
const inServer = path.includes("/server");
const inSpecial = const inSpecial =
(path.startsWith("/friends") && isTouchscreenDevice) || (path.startsWith("/friends") && isTouchscreenDevice) ||
path.startsWith("/invite") || path.startsWith("/invite") ||
@ -44,90 +89,102 @@ export default function App() {
return ( return (
<> <>
{window.isNative && !window.native.getConfig().frame && ( {/*<StatusBar>
<Titlebar /> <div className="title">Planned outage: CDN (~2 hours)</div>
)} <div className="button">View status</div>
<OverlappingPanels </StatusBar>*/}
width="100vw" <AppContainer>
height={ {window.isNative && !window.native.getConfig().frame && (
window.isNative && !window.native.getConfig().frame <Titlebar />
? "calc(var(--app-height) - var(--titlebar-height))" )}
: "var(--app-height)" <OverlappingPanels
} width="100vw"
leftPanel={ height={
inSpecial window.isNative && !window.native.getConfig().frame
? undefined ? "calc(var(--app-height) - var(--titlebar-height))"
: { width: 288, component: <LeftSidebar /> } : "var(--app-height)"
} }
rightPanel={ leftPanel={
!inSpecial && inChannel inSpecial
? { width: 236, component: <RightSidebar /> } ? undefined
: undefined : { width: 288, component: <LeftSidebar /> }
} }
bottomNav={{ rightPanel={
component: <BottomNavigation />, !inSpecial && inChannel
showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left, ? { width: 236, component: <RightSidebar /> }
height: 50, : undefined
}} }
docked={isTouchscreenDevice ? Docked.None : Docked.Left}> bottomNav={{
<Routes> component: <BottomNavigation />,
<Switch> showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left,
<Route height: 50,
path="/server/:server/channel/:channel/settings/:page" }}
component={ChannelSettings} docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
/> <Routes borders={inServer}>
<Route <Switch>
path="/server/:server/channel/:channel/settings" <Route
component={ChannelSettings} path="/server/:server/channel/:channel/settings/:page"
/> component={ChannelSettings}
<Route />
path="/server/:server/settings/:page" <Route
component={ServerSettings} path="/server/:server/channel/:channel/settings"
/> component={ChannelSettings}
<Route />
path="/server/:server/settings" <Route
component={ServerSettings} path="/server/:server/settings/:page"
/> component={ServerSettings}
<Route />
path="/channel/:channel/settings/:page" <Route
component={ChannelSettings} path="/server/:server/settings"
/> component={ServerSettings}
<Route />
path="/channel/:channel/settings" <Route
component={ChannelSettings} path="/channel/:channel/settings/:page"
/> component={ChannelSettings}
/>
<Route
path="/channel/:channel/settings"
component={ChannelSettings}
/>
<Route <Route
path="/channel/:channel/:message" path="/channel/:channel/:message"
component={Channel} component={Channel}
/> />
<Route <Route
path="/server/:server/channel/:channel/:message" path="/server/:server/channel/:channel/:message"
component={Channel} component={Channel}
/> />
<Route <Route
path="/server/:server/channel/:channel" path="/server/:server/channel/:channel"
component={Channel} component={Channel}
/> />
<Route path="/server/:server" component={Channel} /> <Route path="/server/:server" component={Channel} />
<Route path="/channel/:channel" component={Channel} /> <Route
path="/channel/:channel"
component={Channel}
/>
<Route path="/settings/:page" component={Settings} /> <Route
<Route path="/settings" component={Settings} /> path="/settings/:page"
component={Settings}
/>
<Route path="/settings" component={Settings} />
<Route path="/dev" component={Developer} /> <Route path="/dev" component={Developer} />
<Route path="/friends" component={Friends} /> <Route path="/friends" component={Friends} />
<Route path="/open/:id" component={Open} /> <Route path="/open/:id" component={Open} />
<Route path="/bot/:id" component={InviteBot} /> <Route path="/bot/:id" component={InviteBot} />
<Route path="/" component={Home} /> <Route path="/" component={Home} />
</Switch> </Switch>
</Routes> </Routes>
<ContextMenus /> <ContextMenus />
<Popovers /> <Popovers />
<Notifications /> <Notifications />
<StateMonitor /> <StateMonitor />
</OverlappingPanels> </OverlappingPanels>
</AppContainer>
</> </>
); );
} }

View file

@ -29,7 +29,7 @@ import ChannelHeader from "./ChannelHeader";
import { MessageArea } from "./messaging/MessageArea"; import { MessageArea } from "./messaging/MessageArea";
import VoiceHeader from "./voice/VoiceHeader"; import VoiceHeader from "./voice/VoiceHeader";
const ChannelMain = styled.div` const ChannelMain = styled.div.attrs({ "data-component": "channel" })`
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
min-height: 0; min-height: 0;
@ -37,7 +37,9 @@ const ChannelMain = styled.div`
flex-direction: row; flex-direction: row;
`; `;
const ChannelContent = styled.div` const ChannelContent = styled.div.attrs({
"data-component": "content",
})`
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
overflow: hidden; overflow: hidden;

View file

@ -78,7 +78,6 @@ const Info = styled.div`
export default observer(({ channel }: ChannelHeaderProps) => { export default observer(({ channel }: ChannelHeaderProps) => {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const layout = useApplicationState().layout;
const name = getChannelName(channel); const name = getChannelName(channel);
let icon, recipient: User | undefined; let icon, recipient: User | undefined;
@ -99,7 +98,7 @@ export default observer(({ channel }: ChannelHeaderProps) => {
} }
return ( return (
<PageHeader icon={icon}> <PageHeader icon={icon} transparent>
<Info> <Info>
<span className="name">{name}</span> <span className="name">{name}</span>
{isTouchscreenDevice && {isTouchscreenDevice &&

View file

@ -10,6 +10,7 @@ import {
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { chainedDefer, defer } from "../../../lib/defer";
import { internalEmit } from "../../../lib/eventEmitter"; import { internalEmit } from "../../../lib/eventEmitter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState"; import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState";
@ -29,20 +30,34 @@ export default function HeaderActions({ channel }: ChannelHeaderProps) {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const history = useHistory(); const history = useHistory();
function openRightSidebar() { function slideOpen() {
const panels = document.querySelector("#app > div > div"); if (!isTouchscreenDevice) return;
const panels = document.querySelector("#app > div > div > div");
panels?.scrollTo({ panels?.scrollTo({
behavior: "smooth", behavior: "smooth",
left: panels.clientWidth * 3, left: panels.clientWidth * 3,
}); });
} }
function openSidebar() { function openSearch() {
if (isTouchscreenDevice) { if (
openRightSidebar(); !isTouchscreenDevice &&
} else { !layout.getSectionState(SIDEBAR_MEMBERS, true)
) {
layout.toggleSectionState(SIDEBAR_MEMBERS, true); layout.toggleSectionState(SIDEBAR_MEMBERS, true);
} }
slideOpen();
chainedDefer(() => internalEmit("RightSidebar", "open", "search"));
}
function openMembers() {
if (!isTouchscreenDevice) {
layout.toggleSectionState(SIDEBAR_MEMBERS, true);
}
slideOpen();
chainedDefer(() => internalEmit("RightSidebar", "open", undefined));
} }
return ( return (
@ -74,17 +89,13 @@ export default function HeaderActions({ channel }: ChannelHeaderProps) {
)} )}
<VoiceActions channel={channel} /> <VoiceActions channel={channel} />
{channel.channel_type !== "VoiceChannel" && ( {channel.channel_type !== "VoiceChannel" && (
<IconButton <IconButton onClick={openSearch}>
onClick={() => {
internalEmit("RightSidebar", "open", "search");
openRightSidebar();
}}>
<Search size={25} /> <Search size={25} />
</IconButton> </IconButton>
)} )}
{(channel.channel_type === "Group" || {(channel.channel_type === "Group" ||
channel.channel_type === "TextChannel") && ( channel.channel_type === "TextChannel") && (
<IconButton onClick={openSidebar}> <IconButton onClick={openMembers}>
<Group size={25} /> <Group size={25} />
</IconButton> </IconButton>
)} )}

View file

@ -33,13 +33,18 @@ import Preloader from "../../../components/ui/Preloader";
import ConversationStart from "./ConversationStart"; import ConversationStart from "./ConversationStart";
import MessageRenderer from "./MessageRenderer"; import MessageRenderer from "./MessageRenderer";
const Area = styled.div` const Area = styled.div.attrs({ "data-scroll-offset": "with-padding" })`
height: 100%; height: 100%;
flex-grow: 1; flex-grow: 1;
min-height: 0; min-height: 0;
word-break: break-word;
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
word-break: break-word;
&::-webkit-scrollbar-thumb {
min-height: 150px;
}
> div { > div {
display: flex; display: flex;

View file

@ -30,6 +30,7 @@ interface Props {
} }
const VoiceBase = styled.div` const VoiceBase = styled.div`
margin-top: 48px;
padding: 20px; padding: 20px;
background: var(--secondary-background); background: var(--secondary-background);

View file

@ -13,7 +13,6 @@
.list { .list {
padding: 0 10px 10px 10px; padding: 0 10px 10px 10px;
user-select: none; user-select: none;
overflow-y: scroll;
&[data-empty="true"] { &[data-empty="true"] {
img { img {
@ -185,12 +184,9 @@
} }
} }
// Hide the remove friend button on smaller screens.
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
.list { .list .remove {
padding: 0 8px 8px 8px;
}
.remove {
display: none; display: none;
} }
} }

View file

@ -3,8 +3,10 @@ import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { RelationshipStatus, Presence } from "revolt-api/types/Users"; import { RelationshipStatus, Presence } from "revolt-api/types/Users";
import { User } from "revolt.js/dist/maps/Users"; import { User } from "revolt.js/dist/maps/Users";
import styled, { css } from "styled-components";
import styles from "./Friend.module.scss"; import styles from "./Friend.module.scss";
import classNames from "classnames";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { TextReact } from "../../lib/i18n"; import { TextReact } from "../../lib/i18n";
@ -16,7 +18,7 @@ import { useClient } from "../../context/revoltjs/RevoltClient";
import CollapsibleSection from "../../components/common/CollapsibleSection"; import CollapsibleSection from "../../components/common/CollapsibleSection";
import Tooltip from "../../components/common/Tooltip"; import Tooltip from "../../components/common/Tooltip";
import UserIcon from "../../components/common/user/UserIcon"; import UserIcon from "../../components/common/user/UserIcon";
import Header, { PageHeader } from "../../components/ui/Header"; import { PageHeader } from "../../components/ui/Header";
import IconButton from "../../components/ui/IconButton"; import IconButton from "../../components/ui/IconButton";
import { Children } from "../../types/Preact"; import { Children } from "../../types/Preact";
@ -72,7 +74,7 @@ export default observer(() => {
const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0; const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0;
return ( return (
<> <>
<PageHeader icon={<UserDetail size={24} />} noBurger> <PageHeader icon={<UserDetail size={24} />} transparent noBurger>
<div className={styles.title}> <div className={styles.title}>
<Text id="app.navigation.tabs.friends" /> <Text id="app.navigation.tabs.friends" />
</div> </div>
@ -115,99 +117,104 @@ export default observer(() => {
*/} */}
</div> </div>
</PageHeader> </PageHeader>
<div <div data-scroll-offset="true" data-avoids-navigation="true">
className={styles.list} <div
data-empty={isEmpty} className={classNames(styles.list, "with-padding")}
data-mobile={isTouchscreenDevice}> data-empty={isEmpty}
{isEmpty && ( data-mobile={isTouchscreenDevice}>
<> {isEmpty && (
<img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" /> <>
<Text id="app.special.friends.nobody" /> <img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" />
</> <Text id="app.special.friends.nobody" />
)} </>
)}
{incoming.length > 0 && ( {incoming.length > 0 && (
<div <div
className={styles.pending} className={styles.pending}
onClick={() => onClick={() =>
openScreen({ openScreen({
id: "pending_requests", id: "pending_requests",
users: incoming, users: incoming,
}) })
}>
<div className={styles.avatars}>
{incoming.map(
(x, i) =>
i < 3 && (
<UserIcon
target={x}
size={64}
mask={
i <
Math.min(incoming.length - 1, 2)
? "url(#overlap)"
: undefined
}
/>
),
)}
</div>
<div className={styles.details}>
<div>
<Text id="app.special.friends.pending" />{" "}
<span>{incoming.length}</span>
</div>
<span>
{incoming.length > 3 ? (
<TextReact
id="app.special.friends.from.several"
fields={{
userlist: userlist.slice(0, 6),
count: incoming.length - 3,
}}
/>
) : incoming.length > 1 ? (
<TextReact
id="app.special.friends.from.multiple"
fields={{
user: userlist.shift()!,
userlist: userlist.slice(1),
}}
/>
) : (
<TextReact
id="app.special.friends.from.single"
fields={{ user: userlist[0] }}
/>
)}
</span>
</div>
<ChevronRight size={28} />
</div>
)}
{lists.map(([i18n, list, section_id], index) => {
if (index === 0) return;
if (list.length === 0) return;
return (
<CollapsibleSection
key={section_id}
id={`friends_${section_id}`}
defaultValue={true}
sticky
large
summary={
<div class="title">
<Text id={i18n} /> {list.length}
</div>
}> }>
{list.map((x) => ( <div className={styles.avatars}>
<Friend key={x._id} user={x} /> {incoming.map(
))} (x, i) =>
</CollapsibleSection> i < 3 && (
); <UserIcon
})} target={x}
size={64}
mask={
i <
Math.min(
incoming.length - 1,
2,
)
? "url(#overlap)"
: undefined
}
/>
),
)}
</div>
<div className={styles.details}>
<div>
<Text id="app.special.friends.pending" />{" "}
<span>{incoming.length}</span>
</div>
<span>
{incoming.length > 3 ? (
<TextReact
id="app.special.friends.from.several"
fields={{
userlist: userlist.slice(0, 6),
count: incoming.length - 3,
}}
/>
) : incoming.length > 1 ? (
<TextReact
id="app.special.friends.from.multiple"
fields={{
user: userlist.shift()!,
userlist: userlist.slice(1),
}}
/>
) : (
<TextReact
id="app.special.friends.from.single"
fields={{ user: userlist[0] }}
/>
)}
</span>
</div>
<ChevronRight size={28} />
</div>
)}
{lists.map(([i18n, list, section_id], index) => {
if (index === 0) return;
if (list.length === 0) return;
return (
<CollapsibleSection
key={section_id}
id={`friends_${section_id}`}
defaultValue={true}
sticky
large
summary={
<div class="title">
<Text id={i18n} /> {list.length}
</div>
}>
{list.map((x) => (
<Friend key={x._id} user={x} />
))}
</CollapsibleSection>
);
})}
</div>
</div> </div>
</> </>
); );

View file

@ -8,7 +8,7 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 95%; height: 100%;
padding: 12px; padding: 12px;
h3 { h3 {
@ -30,15 +30,11 @@
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 16px; gap: 16px;
max-width: 650px; max-width: 650px;
margin-bottom: 20px; margin-bottom: 30px;
a { a {
width: 100%; width: 100%;
&:nth-child(4) {
margin-bottom: 20px;
}
div { div {
margin: 0; margin: 0;
} }

View file

@ -83,7 +83,7 @@ export default observer(() => {
</div> </div>
)} )}
<div className="content"> <div className="content">
<PageHeader icon={<HomeIcon size={24} />}> <PageHeader icon={<HomeIcon size={24} />} transparent>
<Text id="app.navigation.tabs.home" /> <Text id="app.navigation.tabs.home" />
</PageHeader> </PageHeader>
<div className={styles.homeScreen}> <div className={styles.homeScreen}>
@ -103,8 +103,8 @@ export default observer(() => {
Create a group Create a group
</CategoryButton> </CategoryButton>
</Link> </Link>
<a {/*<a
href="https://revolt.social" href="#"
target="_blank" target="_blank"
rel="noreferrer"> rel="noreferrer">
<CategoryButton <CategoryButton
@ -115,7 +115,7 @@ export default observer(() => {
}> }>
Join a community Join a community
</CategoryButton> </CategoryButton>
</a> </a>*/}
{client.servers.get( {client.servers.get(
"01F7ZSBSFHQ8TA81725KQCSDDP", "01F7ZSBSFHQ8TA81725KQCSDDP",
@ -159,24 +159,24 @@ export default observer(() => {
rel="noreferrer"> rel="noreferrer">
<CategoryButton <CategoryButton
action="external" action="external"
description={
"Support the project by donating - thank you!"
}
icon={<Money size={32} />}> icon={<Money size={32} />}>
<Text id="app.home.donate" /> <Text id="app.home.donate" />
</CategoryButton> </CategoryButton>
</a> </a>
<Tooltip
content={
<Text id="app.home.settings-tooltip" />
}>
<Link to="/settings">
<CategoryButton
action="chevron"
icon={<Cog size={32} />}>
<Text id="app.home.settings" />
</CategoryButton>
</Link>
</Tooltip>
</div> </div>
<Tooltip
content={<Text id="app.home.settings-tooltip" />}>
<Link to="/settings">
<CategoryButton
action="chevron"
icon={<Cog size={32} />}>
<Text id="app.home.settings" />
</CategoryButton>
</Link>
</Tooltip>
{isDecember && ( {isDecember && (
<a href="#" onClick={toggleSeasonalTheme}> <a href="#" onClick={toggleSeasonalTheme}>
Turn {seasonalTheme ? "off" : "on"} homescreen Turn {seasonalTheme ? "off" : "on"} homescreen

View file

@ -102,7 +102,7 @@ export function GenericSettings({
/> />
</Helmet> </Helmet>
{isTouchscreenDevice && ( {isTouchscreenDevice && (
<Header placement="primary"> <Header placement="primary" transparent>
{typeof page === "undefined" ? ( {typeof page === "undefined" ? (
<> <>
{showExitButton && ( {showExitButton && (
@ -168,6 +168,9 @@ export function GenericSettings({
<div className={styles.content}> <div className={styles.content}>
<div <div
className={styles.scrollbox} className={styles.scrollbox}
data-scroll-offset={
isTouchscreenDevice ? "with-padding" : undefined
}
ref={(ref) => { ref={(ref) => {
// Force scroll to top if page changes. // Force scroll to top if page changes.
if (ref) { if (ref) {

View file

@ -43,18 +43,13 @@
background: var(--primary-background); background: var(--primary-background);
} }
.scrollbox {
&::-webkit-scrollbar-thumb {
border-top: none;
}
}
/* Sidebar */ /* Sidebar */
.sidebar { .sidebar {
overflow-y: auto; overflow-y: auto;
.container { .container {
padding: 20px 8px calc(var(--bottom-navigation-height) + 30px); padding: calc(var(--header-height) + 4px) 8px
calc(var(--bottom-navigation-height) + 30px);
min-width: 218px; min-width: 218px;
} }
@ -76,7 +71,7 @@
.contentcontainer { .contentcontainer {
max-width: unset !important; max-width: unset !important;
padding: 16px 12px var(--bottom-navigation-height) !important; padding: 72px 12px var(--bottom-navigation-height) !important;
} }
} }
} }
@ -117,6 +112,7 @@
// All children receive custom scrollbar. // All children receive custom scrollbar.
> * > ::-webkit-scrollbar-thumb { > * > ::-webkit-scrollbar-thumb {
min-height: 100px;
width: 4px; width: 4px;
background-clip: content-box; background-clip: content-box;
border-top: 80px solid transparent; border-top: 80px solid transparent;

View file

@ -17,6 +17,7 @@ import {
Speaker, Speaker,
Store, Store,
Bot, Bot,
Trash,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Route, Switch, useHistory } from "react-router-dom"; import { Route, Switch, useHistory } from "react-router-dom";
@ -24,16 +25,19 @@ import { LIBRARY_VERSION } from "revolt.js";
import styled from "styled-components"; import styled from "styled-components";
import styles from "./Settings.module.scss"; import styles from "./Settings.module.scss";
import { openContextMenu } from "preact-context-menu";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { useApplicationState } from "../../mobx/State"; import { useApplicationState } from "../../mobx/State";
import { useIntermediate } from "../../context/intermediate/Intermediate";
import RequiresOnline from "../../context/revoltjs/RequiresOnline"; import RequiresOnline from "../../context/revoltjs/RequiresOnline";
import { AppContext, LogOutContext } from "../../context/revoltjs/RevoltClient"; import { AppContext, LogOutContext } from "../../context/revoltjs/RevoltClient";
import UserIcon from "../../components/common/user/UserIcon"; import UserIcon from "../../components/common/user/UserIcon";
import { Username } from "../../components/common/user/UserShort"; import { Username } from "../../components/common/user/UserShort";
import UserStatus from "../../components/common/user/UserStatus";
import LineDivider from "../../components/ui/LineDivider"; import LineDivider from "../../components/ui/LineDivider";
import ButtonItem from "../../components/navigation/items/ButtonItem"; import ButtonItem from "../../components/navigation/items/ButtonItem";
@ -54,19 +58,67 @@ import { Sessions } from "./panes/Sessions";
import { Sync } from "./panes/Sync"; import { Sync } from "./panes/Sync";
import { ThemeShop } from "./panes/ThemeShop"; import { ThemeShop } from "./panes/ThemeShop";
const IndexHeader = styled.div` const AccountHeader = styled.div`
display: flex; display: flex;
background: var(--secondary-background); flex-direction: column;
border-radius: var(--border-radius); border-radius: var(--border-radius);
padding: 20px; overflow: hidden;
align-items: center; margin-bottom: 10px;
gap: 10px;
.account {
padding: 20px;
gap: 10px;
align-items: center;
display: flex;
background: var(--secondary-background);
.details {
display: flex;
flex-direction: column;
font-size: 12px;
gap: 2px;
> span {
font-size: 20px;
font-weight: 600;
}
}
}
.statusChanger {
display: flex;
align-items: center;
background: var(--tertiary-background);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.status {
padding-inline-start: 12px;
height: 48px;
display: flex;
align-items: center;
color: var(--secondary-foreground);
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
svg {
width: 48px;
flex-shrink: 0;
}
}
`; `;
export default observer(() => { export default observer(() => {
const history = useHistory(); const history = useHistory();
const client = useContext(AppContext); const client = useContext(AppContext);
const logout = useContext(LogOutContext); const logout = useContext(LogOutContext);
const { openScreen } = useIntermediate();
const experiments = useApplicationState().experiments; const experiments = useApplicationState().experiments;
function switchPage(to?: string) { function switchPage(to?: string) {
@ -270,10 +322,40 @@ export default observer(() => {
</> </>
} }
indexHeader={ indexHeader={
<IndexHeader> <AccountHeader>
<UserIcon size={64} target={client.user!} /> <div className="account">
<Username user={client.user!} prefixAt /> <UserIcon
</IndexHeader> size={64}
target={client.user!}
status
onClick={() => openContextMenu("Status")}
/>
<div className="details">
<Username user={client.user!} prefixAt />
<UserStatus user={client.user!} />
</div>
</div>
<div className="statusChanger">
<a
className="status"
onClick={() =>
openScreen({
id: "special_input",
type: "set_custom_status",
})
}>
Change your status...
</a>
{client.user!.status?.text && (
<Trash
size={24}
onClick={() =>
client.users.edit({ remove: "StatusText" })
}
/>
)}
</div>
</AccountHeader>
} }
/> />
); );

View file

@ -15,6 +15,7 @@ import {
DisplayEmojiShim, DisplayEmojiShim,
ThemeCustomCSSShim, ThemeCustomCSSShim,
DisplaySeasonalShim, DisplaySeasonalShim,
DisplayTransparencyShim,
} from "../../../components/settings/AppearanceShims"; } from "../../../components/settings/AppearanceShims";
import ThemeOverrides from "../../../components/settings/appearance/ThemeOverrides"; import ThemeOverrides from "../../../components/settings/appearance/ThemeOverrides";
import ThemeTools from "../../../components/settings/appearance/ThemeTools"; import ThemeTools from "../../../components/settings/appearance/ThemeTools";
@ -27,6 +28,10 @@ export const Appearance = observer(() => {
<hr /> <hr />
<ThemeAccentShim /> <ThemeAccentShim />
<hr /> <hr />
<h3>
<Text id="app.settings.pages.appearance.theme_options.title" />
</h3>
<DisplayTransparencyShim />
<DisplaySeasonalShim /> <DisplaySeasonalShim />
<hr /> <hr />
<DisplayFontShim /> <DisplayFontShim />

View file

@ -126,7 +126,6 @@ export const Overview = observer(({ server }: Props) => {
alignItems: "center", alignItems: "center",
}}> }}>
<span style={{ flexShrink: "0", flex: `25%` }}>{i18n}</span> <span style={{ flexShrink: "0", flex: `25%` }}>{i18n}</span>
<span>Sends a message when someone joins your server</span>
<ComboBox <ComboBox
value={ value={
systemMessages?.[ systemMessages?.[

View file

@ -1,12 +1,17 @@
.preact-context-menu .context-menu { .preact-context-menu .context-menu {
z-index: 5000; z-index: 10000;
min-width: 190px; min-width: 190px;
padding: 6px 8px; padding: 6px 8px;
user-select: none; user-select: none;
font-size: 0.875rem; font-size: 0.875rem;
color: var(--secondary-foreground); color: var(--secondary-foreground);
border-radius: var(--border-radius); border-radius: var(--border-radius);
background: var(--primary-background) !important; //background: var(--primary-background) !important;
background-color: rgba(
var(--primary-background-rgb),
max(var(--min-opacity), 0.8)
);
backdrop-filter: blur(10px);
box-shadow: 0px 0px 8px 8px rgba(0, 0, 0, 0.05); box-shadow: 0px 0px 8px 8px rgba(0, 0, 0, 0.05);
> span, > span,
@ -23,7 +28,11 @@
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: var(--secondary-background); background-color: rgba(
var(--secondary-background-rgb),
max(var(--min-opacity), 0.75)
);
backdrop-filter: blur(10px);
} }
} }

View file

@ -13,9 +13,32 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
min-height: 30px;
min-width: 30px;
background-clip: content-box;
background: var(--scrollbar-thumb); background: var(--scrollbar-thumb);
} }
[data-scroll-offset] {
overflow-y: scroll;
}
[data-scroll-offset="with-padding"],
[data-scroll-offset] .with-padding {
padding-top: var(--header-height);
}
[data-scroll-offset]::-webkit-scrollbar-thumb {
background-clip: content-box;
border-top: var(--header-height) solid transparent;
}
[data-avoids-navigation]::-webkit-scrollbar-thumb {
background-clip: content-box;
border-bottom: var(--effective-bottom-offset) solid transparent;
}
::-webkit-scrollbar-corner { ::-webkit-scrollbar-corner {
background: transparent; background: transparent;
} }

View file

@ -30,7 +30,7 @@
--input-border-width: 2px; --input-border-width: 2px;
--textarea-padding: 16px; --textarea-padding: 16px;
--textarea-line-height: 20px; --textarea-line-height: 20px;
--message-box-padding: 14px 14px 14px 0; --message-box-padding: 14px;
--attachment-max-width: 480px; --attachment-max-width: 480px;
--attachment-max-height: 640px; --attachment-max-height: 640px;

View file

@ -1303,6 +1303,11 @@
javascript-natural-sort "0.7.1" javascript-natural-sort "0.7.1"
lodash "4.17.21" lodash "4.17.21"
"@types/color-rgba@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/color-rgba/-/color-rgba-2.1.0.tgz#0182795370deae5c2c62f71ea6e91c6bab87394d"
integrity sha512-tWcJLEiKdZ3ihJdThfLCe6Kw5vo0lgGcuucGkbtzcp1zifDA1E2Z96wxeSS/r+ytpHD15NCAWabX8GV911ywCA==
"@types/debug@^4.1.6": "@types/debug@^4.1.6":
version "4.1.7" version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
@ -1642,6 +1647,11 @@ ajv@^8.0.1, ajv@^8.6.0:
require-from-string "^2.0.2" require-from-string "^2.0.2"
uri-js "^4.2.2" uri-js "^4.2.2"
almost-equal@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/almost-equal/-/almost-equal-1.1.0.tgz#f851c631138757994276aa2efbe8dfa3066cccdd"
integrity sha1-+FHGMROHV5lCdqou++jfowZszN0=
ansi-colors@^4.1.1: ansi-colors@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
@ -1934,11 +1944,34 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
color-name@~1.1.4: color-name@^1.0.0, color-name@~1.1.4:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-parse@^1.4.1:
version "1.4.2"
resolved "https://registry.yarnpkg.com/color-parse/-/color-parse-1.4.2.tgz#78651f5d34df1a57f997643d86f7f87268ad4eb5"
integrity sha512-RI7s49/8yqDj3fECFZjUI1Yi0z/Gq1py43oNJivAIIDSyJiOZLfYCRQEgn8HEVAj++PcRe8AnL2XF0fRJ3BTnA==
dependencies:
color-name "^1.0.0"
color-rgba@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/color-rgba/-/color-rgba-2.3.0.tgz#d5eb481d7933d2542d1f222ea10ad40d159e9d35"
integrity sha512-z/5fMOY8/IzrBHPBk+n3ATNSM/1atXcHCRPTGPLlzYJ4fn7CRD46zzt3lkLtQ44cL8UIUU4JBXDVrhWj1khiwg==
dependencies:
color-parse "^1.4.1"
color-space "^1.14.6"
color-space@^1.14.6:
version "1.16.0"
resolved "https://registry.yarnpkg.com/color-space/-/color-space-1.16.0.tgz#611781bca41cd8582a1466fd9e28a7d3d89772a2"
integrity sha512-A6WMiFzunQ8KEPFmj02OnnoUnqhmSaHaZ/0LVFcPTdlvm8+3aMJ5x1HRHy3bDHPkovkf4sS0f4wsVvwk71fKkg==
dependencies:
hsluv "^0.0.3"
mumath "^3.3.4"
colorette@^1.3.0: colorette@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af"
@ -2742,6 +2775,11 @@ hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-
dependencies: dependencies:
react-is "^16.7.0" react-is "^16.7.0"
hsluv@^0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/hsluv/-/hsluv-0.0.3.tgz#829107dafb4a9f8b52a1809ed02e091eade6754c"
integrity sha1-gpEH2vtKn4tSoYCe0C4JHq3mdUw=
idb@^6.0.0: idb@^6.0.0:
version "6.1.2" version "6.1.2"
resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.2.tgz#82ef5c951b8e1f47875d36ccafa4bedafc62f2f1" resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.2.tgz#82ef5c951b8e1f47875d36ccafa4bedafc62f2f1"
@ -3276,6 +3314,13 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
mumath@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/mumath/-/mumath-3.3.4.tgz#48d4a0f0fd8cad4e7b32096ee89b161a63d30bbf"
integrity sha1-SNSg8P2MrU57Mglu6JsWGmPTC78=
dependencies:
almost-equal "^1.1.0"
nanoid@^3.1.30: nanoid@^3.1.30:
version "3.1.30" version "3.1.30"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"