feat(settings): UI improvements (#448)

* Fixed CSS for Settings.tsx + new Theme Shop design

* reformat

* More changes to UI CSS

* Small CSS fixes for Settings.tsx, Account, Bots

* Updated theme shop, settings pages, cleanup

* chore: force sync language submodule

* fix(sidebar): prevent items from shrinking

* fix(push): fix timestamp and icon for push notifications

* fix(voice): hide grant permission button after grant

* chore: hide new shop / chevron before merge

* chore(ci): bump node to v16 in dockerfile

* fix(sidebar): change width of channel sidebar

Co-authored-by: trashtemp <96388163+trashtemp@users.noreply.github.com>
This commit is contained in:
Paul Makles 2021-12-20 13:37:21 +00:00 committed by GitHub
parent 535a40df0c
commit 9298f205fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 744 additions and 272 deletions

View file

@ -12,7 +12,7 @@ RUN yarn typecheck
RUN yarn build RUN yarn build
RUN npm prune --production RUN npm prune --production
FROM node:15-buster FROM node:16-buster
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --from=builder /usr/src/app . COPY --from=builder /usr/src/app .

2
external/lang vendored

@ -1 +1 @@
Subproject commit 40afd0527defb106142c50b979bbfc6283e9e48d Subproject commit 9b9858c64503783364dffba18b2a5356d11cdc30

View file

@ -14,7 +14,7 @@ export const GenericSidebarBase = styled.div<{
mobilePadding?: boolean; mobilePadding?: boolean;
}>` }>`
height: 100%; height: 100%;
width: 240px; width: 236px;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
flex-direction: column; flex-direction: column;

View file

@ -79,7 +79,9 @@ const ServersBase = styled.div`
width: 56px; width: 56px;
height: 100%; height: 100%;
padding-left: 2px; padding-left: 2px;
display: flex; display: flex;
flex-shrink: 0;
flex-direction: column; flex-direction: column;
${isTouchscreenDevice && ${isTouchscreenDevice &&

View file

@ -32,7 +32,7 @@ interface Props {
const ServerBase = styled.div` const ServerBase = styled.div`
height: 100%; height: 100%;
width: 240px; width: 236px;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
flex-direction: column; flex-direction: column;

View file

@ -24,6 +24,7 @@ export default styled.button<Props>`
font-size: 0.875rem; font-size: 0.875rem;
font-family: inherit; font-family: inherit;
font-weight: 500; font-weight: 500;
flex-shrink: 0;
transition: 0.2s ease opacity; transition: 0.2s ease opacity;
transition: 0.2s ease background-color; transition: 0.2s ease background-color;

View file

@ -10,7 +10,7 @@ interface Props {
} }
export default styled.div<Props>` export default styled.div<Props>`
gap: 6px; gap: 10px;
height: 48px; height: 48px;
flex: 0 auto; flex: 0 auto;
display: flex; display: flex;

View file

@ -49,9 +49,9 @@ export const TipBase = styled.div<Props>`
${(props) => ${(props) =>
props.error && props.error &&
css` css`
color: var(--error); color: white;
border: 2px solid var(--error); border: 2px solid var(--error);
background: var(--secondary-header); background: var(--error);
`} `}
`; `;

View file

@ -29,6 +29,10 @@
align-items: center; align-items: center;
flex-direction: row; flex-direction: row;
> svg {
cursor: pointer;
}
.details { .details {
flex-grow: 1; flex-grow: 1;
min-width: 0; min-width: 0;

View file

@ -58,7 +58,7 @@ export default function App() {
leftPanel={ leftPanel={
inSpecial inSpecial
? undefined ? undefined
: { width: 292, component: <LeftSidebar /> } : { width: 288, component: <LeftSidebar /> }
} }
rightPanel={ rightPanel={
!inSpecial && inChannel !inSpecial && inChannel

View file

@ -1,4 +1,4 @@
import { At, Hash, Menu } from "@styled-icons/boxicons-regular"; import { At, Hash, Menu, ChevronLeft } from "@styled-icons/boxicons-regular";
import { Notepad, Group } from "@styled-icons/boxicons-solid"; import { Notepad, Group } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
@ -65,85 +65,103 @@ const Info = styled.div`
} }
`; `;
const IconConainer = styled.div` const IconContainer = styled.div`
display: flex;
align-items: center;
cursor: pointer; cursor: pointer;
color: var(--secondary-foreground); color: var(--secondary-foreground);
margin-right: 5px;
${!isTouchscreenDevice && css` > svg {
margin-right: -5px;
}
${!isTouchscreenDevice &&
css`
&:hover { &:hover {
color: var(--foreground); color: var(--foreground);
} }
`} `}
` `;
export default observer(({ channel, toggleSidebar, toggleChannelSidebar }: ChannelHeaderProps) => { export default observer(
const { openScreen } = useIntermediate(); ({ channel, toggleSidebar, toggleChannelSidebar }: ChannelHeaderProps) => {
const { openScreen } = useIntermediate();
const name = getChannelName(channel); const name = getChannelName(channel);
let icon, recipient: User | undefined; let icon, recipient: User | undefined;
switch (channel.channel_type) { switch (channel.channel_type) {
case "SavedMessages": case "SavedMessages":
icon = <Notepad size={24} />; icon = <Notepad size={24} />;
break; break;
case "DirectMessage": case "DirectMessage":
icon = <At size={24} />; icon = <At size={24} />;
recipient = channel.recipient; recipient = channel.recipient;
break; break;
case "Group": case "Group":
icon = <Group size={24} />; icon = <Group size={24} />;
break; break;
case "TextChannel": case "TextChannel":
icon = <Hash size={24} />; icon = <Hash size={24} />;
break; break;
} }
return ( return (
<Header placement="primary"> <Header placement="primary">
<HamburgerAction /> <HamburgerAction />
<IconConainer onClick={toggleChannelSidebar}>{icon}</IconConainer> <IconContainer onClick={toggleChannelSidebar}>
<Info> {/*isTouchscreenDevice && <ChevronLeft size={18} /> FIXME: requires mobx merge */}
<span className="name">{name}</span> {icon}
{isTouchscreenDevice && </IconContainer>
channel.channel_type === "DirectMessage" && ( <Info>
<> <span className="name">{name}</span>
<div className="divider" /> {isTouchscreenDevice &&
<span className="desc"> channel.channel_type === "DirectMessage" && (
<div <>
className="status" <div className="divider" />
style={{ <span className="desc">
backgroundColor: <div
useStatusColour(recipient), className="status"
}} style={{
/> backgroundColor:
<UserStatus user={recipient} /> useStatusColour(recipient),
</span> }}
</> />
)} <UserStatus user={recipient} />
{!isTouchscreenDevice && </span>
(channel.channel_type === "Group" || </>
channel.channel_type === "TextChannel") && )}
channel.description && ( {!isTouchscreenDevice &&
<> (channel.channel_type === "Group" ||
<div className="divider" /> channel.channel_type === "TextChannel") &&
<span channel.description && (
className="desc" <>
onClick={() => <div className="divider" />
openScreen({ <span
id: "channel_info", className="desc"
channel, onClick={() =>
}) openScreen({
}> id: "channel_info",
<Markdown channel,
content={ })
channel.description.split("\n")[0] ?? "" }>
} <Markdown
disallowBigEmoji content={
/> channel.description.split(
</span> "\n",
</> )[0] ?? ""
)} }
</Info> disallowBigEmoji
<HeaderActions channel={channel} toggleSidebar={toggleSidebar} /> />
</Header> </span>
); </>
}); )}
</Info>
<HeaderActions
channel={channel}
toggleSidebar={toggleSidebar}
/>
</Header>
);
},
);

View file

@ -13,7 +13,7 @@
.actions { .actions {
gap: 8px; gap: 8px;
width: 240px; width: 236px;
margin: auto; margin: auto;
display: flex; display: flex;

View file

@ -130,13 +130,18 @@
.container { .container {
min-width: 218px; min-width: 218px;
max-width: 300px;
padding: 80px 8px; padding: 80px 8px;
display: flex; display: flex;
gap: 2px; gap: 2px;
flex-direction: column; flex-direction: column;
} }
@media only screen and (min-width: 800px) {
.container {
max-width: 300px;
}
}
.divider { .divider {
height: 30px; height: 30px;
} }
@ -224,6 +229,11 @@
font-weight: 400; font-weight: 400;
} }
h6 {
font-size: 1rem;
font-weight: 600;
}
.footer { .footer {
border-top: 1px solid; border-top: 1px solid;
margin: 0; margin: 0;

View file

@ -4,7 +4,6 @@ import {
Globe, Globe,
LogOut, LogOut,
Desktop, Desktop,
Bot,
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { import {
Bell, Bell,
@ -17,6 +16,7 @@ import {
Megaphone, Megaphone,
Speaker, Speaker,
Store, Store,
Bot,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { Route, Switch, useHistory } from "react-router-dom"; import { Route, Switch, useHistory } from "react-router-dom";
import { LIBRARY_VERSION } from "revolt.js"; import { LIBRARY_VERSION } from "revolt.js";

View file

@ -68,7 +68,17 @@ export const Account = observer(() => {
onClick={() => switchPage("profile")} onClick={() => switchPage("profile")}
/> />
<div className={styles.userDetail}> <div className={styles.userDetail}>
@{client.user!.username} <div className={styles.userContainer}>
<UserIcon
className={styles.tinyavatar}
target={client.user!}
size={25}
onClick={() => switchPage("profile")}
/>
<div className={styles.username}>
@{client.user!.username}
</div>
</div>
<div className={styles.userid}> <div className={styles.userid}>
<Tooltip <Tooltip
content={ content={
@ -113,6 +123,7 @@ export const Account = observer(() => {
<> <>
{value}{" "} {value}{" "}
<a <a
style={{ fontSize: "13px" }}
onClick={(ev) => onClick={(ev) =>
stopPropagation( stopPropagation(
ev, ev,
@ -126,6 +137,7 @@ export const Account = observer(() => {
<> <>
@.{" "} @.{" "}
<a <a
style={{ fontSize: "13px" }}
onClick={(ev) => onClick={(ev) =>
stopPropagation( stopPropagation(
ev, ev,

View file

@ -1,5 +1,6 @@
import { Reset, Import } from "@styled-icons/boxicons-regular"; import { Reset, Import } from "@styled-icons/boxicons-regular";
import { Pencil, Store } from "@styled-icons/boxicons-solid"; import { Pencil, Store } from "@styled-icons/boxicons-solid";
import { Link } from "react-router-dom";
// @ts-expect-error shade-blend-color does not have typings. // @ts-expect-error shade-blend-color does not have typings.
import pSBC from "shade-blend-color"; import pSBC from "shade-blend-color";
@ -8,16 +9,13 @@ import { Text } from "preact-i18n";
import { useCallback, useContext, useEffect, useState } from "preact/hooks"; import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import CategoryButton from "../../../components/ui/fluent/CategoryButton";
import { debounce } from "../../../lib/debounce"; import { debounce } from "../../../lib/debounce";
import { dispatch } from "../../../redux"; import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector"; import { connectState } from "../../../redux/connector";
import { isExperimentEnabled } from "../../../redux/reducers/experiments";
import { EmojiPacks, Settings } from "../../../redux/reducers/settings"; import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
import { import {
DEFAULT_FONT, DEFAULT_FONT,
DEFAULT_MONO_FONT, DEFAULT_MONO_FONT,
@ -40,14 +38,13 @@ import Checkbox from "../../../components/ui/Checkbox";
import ColourSwatches from "../../../components/ui/ColourSwatches"; import ColourSwatches from "../../../components/ui/ColourSwatches";
import ComboBox from "../../../components/ui/ComboBox"; import ComboBox from "../../../components/ui/ComboBox";
import InputBox from "../../../components/ui/InputBox"; import InputBox from "../../../components/ui/InputBox";
import CategoryButton from "../../../components/ui/fluent/CategoryButton";
import darkSVG from "../assets/dark.svg"; import darkSVG from "../assets/dark.svg";
import lightSVG from "../assets/light.svg"; import lightSVG from "../assets/light.svg";
import mutantSVG from "../assets/mutant_emoji.svg"; import mutantSVG from "../assets/mutant_emoji.svg";
import notoSVG from "../assets/noto_emoji.svg"; import notoSVG from "../assets/noto_emoji.svg";
import openmojiSVG from "../assets/openmoji_emoji.svg"; import openmojiSVG from "../assets/openmoji_emoji.svg";
import twemojiSVG from "../assets/twemoji_emoji.svg"; import twemojiSVG from "../assets/twemoji_emoji.svg";
import { Link } from "react-router-dom";
import { isExperimentEnabled } from "../../../redux/reducers/experiments";
interface Props { interface Props {
settings: Settings; settings: Settings;
@ -112,8 +109,7 @@ export function Component(props: Props) {
draggable={false} draggable={false}
data-active={selected === "light"} data-active={selected === "light"}
onClick={() => onClick={() =>
selected !== "light" && selected !== "light" && setTheme({ base: "light" })
setTheme({ base: "light" })
} }
onContextMenu={(e) => e.preventDefault()} onContextMenu={(e) => e.preventDefault()}
/> />
@ -138,11 +134,20 @@ export function Component(props: Props) {
</div> </div>
</div> </div>
{isExperimentEnabled('theme_shop') && <Link to="/settings/theme_shop" replace> {isExperimentEnabled("theme_shop") && (
<CategoryButton icon={<Store size={24} />} action="chevron" hover> <Link
<Text id="app.settings.pages.theme_shop.title" /> to="/settings/theme_shop"
</CategoryButton> replace
</Link>} className={styles.focus}>
<CategoryButton
icon={<Store size={24} />}
action="chevron"
description={"Browse themes made by the community"}
hover>
<Text id="app.settings.pages.theme_shop.title" />
</CategoryButton>
</Link>
)}
<h3> <h3>
<Text id="app.settings.pages.appearance.accent_selector" /> <Text id="app.settings.pages.appearance.accent_selector" />

View file

@ -57,11 +57,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(() => {
@ -90,61 +90,109 @@ export function Component() {
return ( return (
<> <>
<div class={styles.audio}> <div class={styles.audio}>
<h3>
<Text id="app.settings.pages.audio.input_device" />
</h3>
{!permission && ( {!permission && (
<div className={styles.grant_permission}> <Tip error hideSeparator>
<span className={styles.description}> <Text id="app.settings.pages.audio.tip_grant_permission" />
<Text id="app.settings.pages.audio.tip_grant_permission" /> </Tip>
</span>
<Button
compact
onClick={(e) => handleAskForPermission(e)}
error>
<Text id="app.settings.pages.audio.button_grant" />
</Button>
</div>
)}
<ComboBox
value={window.localStorage.getItem("audioInputDevice") ?? 0}
onChange={(e) =>
changeAudioDevice(e.currentTarget.value, "input")
}>
{mediaDevices
?.filter((device) => device.kind === "audioinput")
.map((device) => {
return (
<option
value={device.deviceId}
key={device.deviceId}>
{device.label || (
<Text id="app.settings.pages.audio.device_label_NA" />
)}
</option>
);
})}
</ComboBox>
{error && error.name === "NotAllowedError" && (
<Overline error="AudioPermissionBlock" type="error" block />
)} )}
{error && permission === "prompt" && ( {error && permission === "prompt" && (
<Tip> <Tip error hideSeparator>
<TextReact <Text id="app.settings.pages.audio.tip_retry" />
id="app.settings.pages.audio.tip_retry" <a onClick={handleAskForPermission}>
fields={{ <Text id="app.settings.pages.audio.button_retry" />
retryBtn: ( </a>
<a onClick={handleAskForPermission}> .
<Text id="app.settings.pages.audio.button_retry" />
</a>
),
}}
/>
</Tip> </Tip>
)} )}
<div className={styles.audioRow}>
<div className={styles.select}>
<h3>
<Text id="app.settings.pages.audio.input_device" />
</h3>
<div class={styles.audioBox}>
<ComboBox
value={
window.localStorage.getItem(
"audioInputDevice",
) ?? 0
}
onChange={(e) =>
changeAudioDevice(
e.currentTarget.value,
"input",
)
}>
{mediaDevices
?.filter(
(device) =>
device.kind === "audioinput",
)
.map((device) => {
return (
<option
value={device.deviceId}
key={device.deviceId}>
{device.label || (
<Text id="app.settings.pages.audio.device_label_NA" />
)}
</option>
);
})}
</ComboBox>
{!permission && (
<Button
compact
onClick={(e) => handleAskForPermission(e)}
error>
<Text id="app.settings.pages.audio.button_grant" />
</Button>
)}
{error && error.name === "NotAllowedError" && (
<Overline
error="AudioPermissionBlock"
type="error"
block
/>
)}
</div>
</div>
<div className={styles.select}>
<h3>
<Text id="app.settings.pages.audio.output_device" />
</h3>
{/* TOFIX: create audio output combobox*/}
<ComboBox
value={
window.localStorage.getItem(
"audioOutputDevice",
) ?? 0
}
onChange={(e) =>
changeAudioDevice(
e.currentTarget.value,
"output",
)
}>
{mediaDevices
?.filter(
(device) => device.kind === "audiooutput",
)
.map((device) => {
return (
<option
value={device.deviceId}
key={device.deviceId}>
{device.label || (
<Text id="app.settings.pages.audio.device_label_NA" />
)}
</option>
);
})}
</ComboBox>
</div>
</div>
</div> </div>
</> </>
); );

View file

@ -48,7 +48,7 @@ interface Changes {
const BotBadge = styled.div` const BotBadge = styled.div`
display: inline-block; display: inline-block;
flex-shrink: 0;
height: 1.3em; height: 1.3em;
padding: 0px 4px; padding: 0px 4px;
font-size: 0.7em; font-size: 0.7em;
@ -228,99 +228,103 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
return ( return (
<div key={bot._id} className={styles.botCard}> <div key={bot._id} className={styles.botCard}>
<div className={styles.infoheader}> <div className={styles.infocontainer}>
<div className={styles.container}> <div className={styles.infoheader}>
{!editMode ? ( <div className={styles.container}>
<UserIcon {!editMode ? (
className={styles.avatar} <UserIcon
target={user} className={styles.avatar}
size={48} target={user}
onClick={() => size={42}
openScreen({ onClick={() =>
id: "profile", openScreen({
user_id: user._id, id: "profile",
}) user_id: user._id,
} })
/> }
) : ( />
<FileUploader ) : (
width={64} <FileUploader
height={64} width={42}
style="icon" height={42}
fileType="avatars" style="icon"
behaviour="upload" fileType="avatars"
maxFileSize={4_000_000} behaviour="upload"
onUpload={(avatar) => editBotAvatar(avatar)} maxFileSize={4_000_000}
remove={() => editBotAvatar()} onUpload={(avatar) => editBotAvatar(avatar)}
defaultPreview={user.generateAvatarURL( remove={() => editBotAvatar()}
{ max_side: 256 }, defaultPreview={user.generateAvatarURL(
true, { max_side: 256 },
)} true,
previewURL={user.generateAvatarURL( )}
{ max_side: 256 }, previewURL={user.generateAvatarURL(
true, { max_side: 256 },
)} true,
/> )}
)} />
)}
{!editMode ? ( {!editMode ? (
<div className={styles.userDetail}> <div className={styles.userDetail}>
<div className={styles.userName}> <div className={styles.userName}>
{user!.username}{" "} {user!.username}{" "}
<BotBadge> <BotBadge>
<Text id="app.main.channel.bot" /> <Text id="app.main.channel.bot" />
</BotBadge> </BotBadge>
</div> </div>
<div className={styles.userid}> <div className={styles.userid}>
<Tooltip <Tooltip
content={ content={
<Text id="app.settings.pages.bots.unique_id" /> <Text id="app.settings.pages.bots.unique_id" />
}>
<HelpCircle size={16} />
</Tooltip>
<Tooltip
content={<Text id="app.special.copy" />}>
<a
onClick={() =>
writeClipboard(user!._id)
}> }>
{user!._id} <HelpCircle size={16} />
</a> </Tooltip>
</Tooltip> <Tooltip
content={
<Text id="app.special.copy" />
}>
<a
onClick={() =>
writeClipboard(user!._id)
}>
{user!._id}
</a>
</Tooltip>
</div>
</div> </div>
</div> ) : (
) : ( <InputBox
<InputBox ref={setUsernameRef}
ref={setUsernameRef} value={data.username}
value={data.username} disabled={saving}
disabled={saving} onChange={(e) =>
onChange={(e) => setData({
setData({ ...data,
...data, username: e.currentTarget.value,
username: e.currentTarget.value, })
}) }
} />
/> )}
</div>
{!editMode && (
<Tooltip
content={
<Text
id={`app.settings.pages.bots.${
bot.public ? "public" : "private"
}_bot_tip`}
/>
}>
{bot.public ? (
<Globe size={24} />
) : (
<LockAlt size={24} />
)}
</Tooltip>
)} )}
</div> </div>
{!editMode && (
<Tooltip
content={
<Text
id={`app.settings.pages.bots.${
bot.public ? "public" : "private"
}_bot_tip`}
/>
}>
{bot.public ? (
<Globe size={24} />
) : (
<LockAlt size={24} />
)}
</Tooltip>
)}
<Button <Button
disabled={saving} disabled={saving}
onClick={() => { onClick={() => {
@ -377,7 +381,7 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
<CollapsibleSection <CollapsibleSection
defaultValue={false} defaultValue={false}
id={`bot_profile_${bot._id}`} id={`bot_profile_${bot._id}`}
summary="Profile"> summary="Bot Profile">
<h3> <h3>
<Text id="app.settings.pages.profile.custom_background" /> <Text id="app.settings.pages.profile.custom_background" />
</h3> </h3>
@ -473,7 +477,7 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
<Text id="app.special.modals.actions.save" /> <Text id="app.special.modals.actions.save" />
</Button> </Button>
<Button <Button
accent error
onClick={async () => { onClick={async () => {
setSaving(true); setSaving(true);
openScreen({ openScreen({
@ -540,6 +544,16 @@ export const MyBots = observer(() => {
action="chevron"> action="chevron">
<Text id="app.settings.pages.bots.create_bot" /> <Text id="app.settings.pages.bots.create_bot" />
</CategoryButton> </CategoryButton>
<h5>
By creating a bot, you are agreeing to the {` `}
<a
href="https://revolt.chat/aup"
target="_blank"
rel="noreferrer">
Acceptable Usage Policy
</a>
.
</h5>
{bots?.map((bot) => { {bots?.map((bot) => {
return ( return (
<BotCard <BotCard

View file

@ -19,6 +19,21 @@
width: 100%; width: 100%;
} }
.userContainer {
display: flex;
align-items: center;
gap: 8px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.username {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.userDetail { .userDetail {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
@ -48,13 +63,35 @@
gap: 4px; gap: 4px;
color: var(--tertiary-foreground); color: var(--tertiary-foreground);
> :nth-child(2) {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
a { a {
color: inherit; color: inherit;
cursor: pointer; cursor: pointer;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }
} }
} }
@media only screen and (max-width: 300px) {
.avatar {
display: none;
}
}
@media only screen and (min-width: 300px) {
.tinyavatar {
display: none;
}
}
.details { .details {
display: flex; display: flex;
padding: 1em 0; padding: 1em 0;
@ -87,11 +124,15 @@
} }
.preview { .preview {
background: var(--background);
border-radius: var(--border-radius);
width: 100%; width: 100%;
display: flex;
display: grid; display: grid;
place-items: center; place-items: center;
grid-template-columns: minmax(auto, 100%); grid-template-columns: minmax(auto, 100%);
padding-bottom: 30px; padding: 20px;
margin-bottom: 30px;
> div { > div {
width: 100%; width: 100%;
@ -99,18 +140,71 @@
} }
} }
@media only screen and (max-width: 600px) {
.preview {
background: none;
padding: 0;
}
}
.badgePicker {
display: flex;
gap: 10px;
margin-bottom: 20px;
.check {
cursor: pointer;
height: 50px;
width: 50px;
background: var(--secondary-background);
border-radius: var(--border-radius);
display: flex;
align-items: center;
justify-content: center;
&:nth-child(2) {
border: 3px solid var(--accent);
}
&:nth-child(1) {
opacity: 0.5;
cursor: not-allowed;
}
}
}
.row { .row {
gap: 20px; gap: 20px;
display: flex; display: flex;
.pfp { .pfp {
display: flex; display: flex;
flex-shrink: 0;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
} }
.background { .background {
flex-grow: 1; flex-grow: 1;
width: 100%;
}
}
.markdown {
display: flex;
align-items: center;
margin-top: 8px;
gap: 4px;
h5 {
margin: 0;
}
}
@media only screen and (max-width: 600px) {
.row {
align-items: flex-start;
flex-direction: column;
} }
} }
@ -135,16 +229,34 @@
} }
.audio { .audio {
.grant_permission { .audioRow {
margin-bottom: 18px; margin-top: 20px;
.description { display: flex;
font-weight: 400; gap: 20px;
display: -webkit-box;
-webkit-box-orient: vertical; .select {
-webkit-line-clamp: 3; flex-direction: column;
overflow: hidden; width: 50%;
font-size: 12px; }
margin-bottom: 8px; }
.audioBox {
display: flex;
flex-direction: column;
gap: 15px;
align-items: center;
> button {
width: 100%;
}
}
@media only screen and (max-width: 800px) {
.audioRow {
flex-direction: column;
.select {
width: 100%;
}
} }
} }
} }
@ -160,6 +272,7 @@
gap: 8px; gap: 8px;
display: flex; display: flex;
width: 100%; width: 100%;
margin-bottom: 15px;
img { img {
cursor: pointer; cursor: pointer;
@ -541,6 +654,9 @@
.myBots { .myBots {
.botCard { .botCard {
display: flex;
flex-direction: column;
gap: 10px;
background: var(--secondary-background); background: var(--secondary-background);
margin: 8px 0; margin: 8px 0;
padding: 12px; padding: 12px;
@ -566,6 +682,13 @@
} }
} }
.infocontainer {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.infoheader { .infoheader {
gap: 8px; gap: 8px;
width: 100%; width: 100%;
@ -581,6 +704,10 @@
align-items: center; align-items: center;
flex-direction: row; flex-direction: row;
width: 100%; width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }
.userDetail { .userDetail {
@ -593,9 +720,13 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }
.userName { .username {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@ -619,9 +750,23 @@
gap: 4px; gap: 4px;
color: var(--tertiary-foreground); color: var(--tertiary-foreground);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
> :nth-child(2) {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
a { a {
color: inherit; color: inherit;
cursor: pointer; cursor: pointer;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }
} }
} }
@ -631,6 +776,20 @@
flex-direction: row; flex-direction: row;
gap: 10px; gap: 10px;
} }
@media only screen and (max-width: 800px) {
.infocontainer {
flex-direction: column;
> button {
width: 100%;
}
}
.buttonRow {
flex-direction: column;
}
}
} }
section { section {

View file

@ -1,3 +1,4 @@
import { Markdown } from "@styled-icons/boxicons-logos";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Profile as ProfileI } from "revolt-api/types/Users"; import { Profile as ProfileI } from "revolt-api/types/Users";
@ -62,7 +63,7 @@ export const Profile = observer(() => {
return ( return (
<div className={styles.user}> <div className={styles.user}>
<h3> <h3>
<Text id="app.settings.pages.profile.preview" /> <Text id="app.special.modals.actions.preview" />
</h3> </h3>
<div className={styles.preview}> <div className={styles.preview}>
<UserProfile <UserProfile
@ -71,6 +72,12 @@ export const Profile = observer(() => {
dummyProfile={profile} dummyProfile={profile}
/> />
</div> </div>
<h3>Badges</h3>
<div className={styles.badgePicker}>
<div className={styles.check}>a</div>
<div className={styles.check}>b</div>
<div className={styles.check}>c</div>
</div>
<div className={styles.row}> <div className={styles.row}>
<div className={styles.pfp}> <div className={styles.pfp}>
<h3> <h3>
@ -155,6 +162,19 @@ export const Profile = observer(() => {
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
/> />
<div className={styles.markdown}>
<Markdown size="24" />
<h5>
Descriptions support Markdown formatting,{" "}
<a
href="https://developers.revolt.chat/markdown"
target="_blank"
rel="noreferrer">
learn more here
</a>
.
</h5>
</div>
<p> <p>
<Button <Button
contrast contrast

View file

@ -14,6 +14,10 @@ interface Props {
export function Component(props: Props) { export function Component(props: Props) {
return ( return (
<div className={styles.notifications}> <div className={styles.notifications}>
{/*<h3>
<Text id="app.settings.pages.sync.options" />
</h3>
<h5>Sync items automatically</h5>*/}
<h3> <h3>
<Text id="app.settings.pages.sync.categories" /> <Text id="app.settings.pages.sync.categories" />
</h3> </h3>
@ -46,6 +50,9 @@ export function Component(props: Props) {
<Text id={`app.settings.pages.${title}`} /> <Text id={`app.settings.pages.${title}`} />
</Checkbox> </Checkbox>
))} ))}
{/*<h5 style={{ marginTop: "20px", color: "grey" }}>
Last sync at 12:00
</h5>*/}
</div> </div>
); );
} }

View file

@ -1,3 +1,10 @@
import { Plus, Check } from "@styled-icons/boxicons-regular";
import {
Star,
BarChartAlt2,
Brush,
Bookmark,
} from "@styled-icons/boxicons-solid";
import styled from "styled-components"; import styled from "styled-components";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
@ -6,6 +13,7 @@ import { dispatch } from "../../../redux";
import { Theme, generateVariables, ThemeOptions } from "../../../context/Theme"; import { Theme, generateVariables, ThemeOptions } from "../../../context/Theme";
import InputBox from "../../../components/ui/InputBox";
import Tip from "../../../components/ui/Tip"; import Tip from "../../../components/ui/Tip";
import previewPath from "../assets/preview.svg"; import previewPath from "../assets/preview.svg";
@ -35,13 +43,9 @@ export type Manifest = {
// TODO: ability to preview / display the settings set like in the appearance pane // TODO: ability to preview / display the settings set like in the appearance pane
const ThemeInfo = styled.article` const ThemeInfo = styled.article`
display: grid; display: flex;
grid: flex-direction: column;
"preview name creator" min-content gap: 10px;
"preview desc desc" 1fr
/ 200px 1fr 1fr;
gap: 0.5rem 1rem;
padding: 1rem; padding: 1rem;
border-radius: var(--border-radius); border-radius: var(--border-radius);
background: var(--secondary-background); background: var(--secondary-background);
@ -93,6 +97,7 @@ const ThemeInfo = styled.article`
} }
.name { .name {
margin-top: 5px !important;
grid-area: name; grid-area: name;
margin: 0; margin: 0;
} }
@ -104,15 +109,115 @@ const ThemeInfo = styled.article`
} }
.description { .description {
margin-bottom: 5px;
grid-area: desc; grid-area: desc;
} }
.previewBox {
position: relative;
height: 100%;
width: 100%;
.hover {
opacity: 0;
font-family: var(--font), sans-serif;
font-variant-ligatures: var(--ligatures);
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
color: white;
height: 100%;
width: 100%;
z-index: 10;
position: absolute;
background: rgba(0, 0, 0, 0.5);
cursor: pointer;
transition: opacity 0.2s ease-in-out;
&:hover {
opacity: 1;
}
}
> svg {
height: 100%;
}
}
`; `;
const ThemeList = styled.div` const ThemeList = styled.div`
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem; gap: 1rem;
`; `;
const Banner = styled.div`
display: flex;
flex-direction: column;
`;
const Category = styled.div`
display: flex;
gap: 8px;
align-items: center;
.title {
display: flex;
align-items: center;
gap: 8px;
flex-grow: 1;
}
.view {
font-size: 12px;
}
`;
const ActiveTheme = styled.div`
display: flex;
flex-direction: column;
background: var(--secondary-background);
padding: 0;
border-radius: var(--border-radius);
gap: 8px;
overflow: hidden;
.active-indicator {
display: flex;
gap: 6px;
align-items: center;
background: var(--accent);
width: 100%;
padding: 5px 10px;
font-size: 13px;
font-weight: 400;
color: white;
}
.title {
font-size: 1.2rem;
font-weight: 600;
}
.author {
font-size: 12px;
margin-bottom: 5px;
}
.theme {
width: 124px;
height: 80px;
background: var(--tertiary-background);
border-radius: 4px;
}
.container {
display: flex;
gap: 16px;
padding: 10px 16px 16px;
}
`;
const ThemedSVG = styled.svg<{ theme: Theme }>` const ThemedSVG = styled.svg<{ theme: Theme }>`
${(props) => props.theme && generateVariables(props.theme)} ${(props) => props.theme && generateVariables(props.theme)}
`; `;
@ -140,6 +245,10 @@ const ThemePreview = ({ theme, ...props }: ThemePreviewProps) => {
const ThemeShopRoot = styled.div` const ThemeShopRoot = styled.div`
display: grid; display: grid;
gap: 1rem; gap: 1rem;
h5 {
margin-bottom: 0;
}
`; `;
export function ThemeShop() { export function ThemeShop() {
@ -175,18 +284,71 @@ export function ThemeShop() {
return ( return (
<ThemeShopRoot> <ThemeShopRoot>
<h5>
Browse hundreds of themes, created and curated by the community.
</h5>
{/*<LoadFail>
<h5>
Oops! Couldn't load the theme shop. Make sure you're
connected to the internet and try again.
</h5>
</LoadFail>*/}
<Tip warning hideSeparator> <Tip warning hideSeparator>
This section is under construction. The Theme Shop is currently under construction.
</Tip> </Tip>
{/* FIXME INTEGRATE WITH MOBX */}
{/*<ActiveTheme>
<div class="active-indicator">
<Check size="16" />
Currently active
</div>
<div class="container">
<div class="theme">theme svg goes here</div>
<div class="info">
<div class="title">Theme Title</div>
<div class="author">by Author</div>
<h5>This is a theme description.</h5>
</div>
</div>
</ActiveTheme>
<InputBox placeholder="Search themes..." contrast />
<Category>
<div class="title">
<Bookmark size={16} />
Saved
</div>
<a class="view">Manage installed</a>
</Category>
<Category>
<div class="title">
<Star size={16} />
New this week
</div>
<a class="view">View all</a>
</Category>
<Category>
<div class="title">
<Brush size={16} />
Default themes
</div>
<a class="view">View all</a>
</Category>
<Category>
<div class="title">
<BarChartAlt2 size={16} />
Highest rated
</div>
<a class="view">View all</a>
</Category>*/}
<ThemeList> <ThemeList>
{themeList?.map(([slug, theme]) => ( {themeList?.map(([slug, theme]) => (
<ThemeInfo <ThemeInfo
key={slug} key={slug}
data-loaded={Reflect.has(themeData, slug)}> data-loaded={Reflect.has(themeData, slug)}>
<h2 class="name">{theme.name}</h2>
{/* Maybe id's of the users should be included as well / instead? */}
<div class="creator">by {theme.creator}</div>
<div class="description">{theme.description}</div>
<button <button
class="preview" class="preview"
onClick={() => { onClick={() => {
@ -195,17 +357,27 @@ export function ThemeShop() {
theme: { theme: {
slug, slug,
meta: theme, meta: theme,
theme: themeData[slug] theme: themeData[slug],
} },
}) });
dispatch({ dispatch({
type: "SETTINGS_SET_THEME", type: "SETTINGS_SET_THEME",
theme: { base: slug }, theme: { base: slug },
}); });
}}> }}>
<ThemePreview slug={slug} theme={themeData[slug]} /> <div class="previewBox">
<div class="hover">Use theme</div>
<ThemePreview
slug={slug}
theme={themeData[slug]}
/>
</div>
</button> </button>
<h1 class="name">{theme.name}</h1>
{/* Maybe id's of the users should be included as well / instead? */}
<div class="creator">by {theme.creator}</div>
<h5 class="description">{theme.description}</h5>
</ThemeInfo> </ThemeInfo>
))} ))}
</ThemeList> </ThemeList>

View file

@ -17,9 +17,9 @@ self.addEventListener("push", (event) => {
icon: data.icon, icon: data.icon,
image: data.image, image: data.image,
body: data.body, body: data.body,
timestamp: data.timestamp, timestamp: data.timestamp * 1000,
tag: data.tag, tag: data.tag,
badge: "https://app.revolt.chat/assets/icons/android-chrome-512x512.png", badge: "https://app.revolt.chat/assets/icons/monochrome.svg",
data: data.url, data: data.url,
}); });
} }