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 npm prune --production
FROM node:15-buster
FROM node:16-buster
WORKDIR /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;
}>`
height: 100%;
width: 240px;
width: 236px;
display: flex;
flex-shrink: 0;
flex-direction: column;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -58,7 +58,7 @@ export default function App() {
leftPanel={
inSpecial
? undefined
: { width: 292, component: <LeftSidebar /> }
: { width: 288, component: <LeftSidebar /> }
}
rightPanel={
!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 { observer } from "mobx-react-lite";
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;
color: var(--secondary-foreground);
margin-right: 5px;
${!isTouchscreenDevice && css`
> svg {
margin-right: -5px;
}
${!isTouchscreenDevice &&
css`
&:hover {
color: var(--foreground);
}
`}
`
`;
export default observer(({ channel, toggleSidebar, toggleChannelSidebar }: ChannelHeaderProps) => {
const { openScreen } = useIntermediate();
export default observer(
({ channel, toggleSidebar, toggleChannelSidebar }: ChannelHeaderProps) => {
const { openScreen } = useIntermediate();
const name = getChannelName(channel);
let icon, recipient: User | undefined;
switch (channel.channel_type) {
case "SavedMessages":
icon = <Notepad size={24} />;
break;
case "DirectMessage":
icon = <At size={24} />;
recipient = channel.recipient;
break;
case "Group":
icon = <Group size={24} />;
break;
case "TextChannel":
icon = <Hash size={24} />;
break;
}
const name = getChannelName(channel);
let icon, recipient: User | undefined;
switch (channel.channel_type) {
case "SavedMessages":
icon = <Notepad size={24} />;
break;
case "DirectMessage":
icon = <At size={24} />;
recipient = channel.recipient;
break;
case "Group":
icon = <Group size={24} />;
break;
case "TextChannel":
icon = <Hash size={24} />;
break;
}
return (
<Header placement="primary">
<HamburgerAction />
<IconConainer onClick={toggleChannelSidebar}>{icon}</IconConainer>
<Info>
<span className="name">{name}</span>
{isTouchscreenDevice &&
channel.channel_type === "DirectMessage" && (
<>
<div className="divider" />
<span className="desc">
<div
className="status"
style={{
backgroundColor:
useStatusColour(recipient),
}}
/>
<UserStatus user={recipient} />
</span>
</>
)}
{!isTouchscreenDevice &&
(channel.channel_type === "Group" ||
channel.channel_type === "TextChannel") &&
channel.description && (
<>
<div className="divider" />
<span
className="desc"
onClick={() =>
openScreen({
id: "channel_info",
channel,
})
}>
<Markdown
content={
channel.description.split("\n")[0] ?? ""
}
disallowBigEmoji
/>
</span>
</>
)}
</Info>
<HeaderActions channel={channel} toggleSidebar={toggleSidebar} />
</Header>
);
});
return (
<Header placement="primary">
<HamburgerAction />
<IconContainer onClick={toggleChannelSidebar}>
{/*isTouchscreenDevice && <ChevronLeft size={18} /> FIXME: requires mobx merge */}
{icon}
</IconContainer>
<Info>
<span className="name">{name}</span>
{isTouchscreenDevice &&
channel.channel_type === "DirectMessage" && (
<>
<div className="divider" />
<span className="desc">
<div
className="status"
style={{
backgroundColor:
useStatusColour(recipient),
}}
/>
<UserStatus user={recipient} />
</span>
</>
)}
{!isTouchscreenDevice &&
(channel.channel_type === "Group" ||
channel.channel_type === "TextChannel") &&
channel.description && (
<>
<div className="divider" />
<span
className="desc"
onClick={() =>
openScreen({
id: "channel_info",
channel,
})
}>
<Markdown
content={
channel.description.split(
"\n",
)[0] ?? ""
}
disallowBigEmoji
/>
</span>
</>
)}
</Info>
<HeaderActions
channel={channel}
toggleSidebar={toggleSidebar}
/>
</Header>
);
},
);

View file

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

View file

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

View file

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

View file

@ -68,7 +68,17 @@ export const Account = observer(() => {
onClick={() => switchPage("profile")}
/>
<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}>
<Tooltip
content={
@ -113,6 +123,7 @@ export const Account = observer(() => {
<>
{value}{" "}
<a
style={{ fontSize: "13px" }}
onClick={(ev) =>
stopPropagation(
ev,
@ -126,6 +137,7 @@ export const Account = observer(() => {
<>
@.{" "}
<a
style={{ fontSize: "13px" }}
onClick={(ev) =>
stopPropagation(
ev,

View file

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

View file

@ -57,11 +57,11 @@ export function Component() {
return () => {
if (mediaStream) {
// close microphone access on unmount
mediaStream.getTracks().forEach(track => {
track.stop()
})
mediaStream.getTracks().forEach((track) => {
track.stop();
});
}
}
};
}, [mediaStream]);
useEffect(() => {
@ -90,61 +90,109 @@ export function Component() {
return (
<>
<div class={styles.audio}>
<h3>
<Text id="app.settings.pages.audio.input_device" />
</h3>
{!permission && (
<div className={styles.grant_permission}>
<span className={styles.description}>
<Text id="app.settings.pages.audio.tip_grant_permission" />
</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 />
<Tip error hideSeparator>
<Text id="app.settings.pages.audio.tip_grant_permission" />
</Tip>
)}
{error && permission === "prompt" && (
<Tip>
<TextReact
id="app.settings.pages.audio.tip_retry"
fields={{
retryBtn: (
<a onClick={handleAskForPermission}>
<Text id="app.settings.pages.audio.button_retry" />
</a>
),
}}
/>
<Tip error hideSeparator>
<Text id="app.settings.pages.audio.tip_retry" />
<a onClick={handleAskForPermission}>
<Text id="app.settings.pages.audio.button_retry" />
</a>
.
</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>
</>
);

View file

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

View file

@ -19,6 +19,21 @@
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 {
display: flex;
flex-grow: 1;
@ -48,13 +63,35 @@
gap: 4px;
color: var(--tertiary-foreground);
> :nth-child(2) {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
a {
color: inherit;
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 {
display: flex;
padding: 1em 0;
@ -87,11 +124,15 @@
}
.preview {
background: var(--background);
border-radius: var(--border-radius);
width: 100%;
display: flex;
display: grid;
place-items: center;
grid-template-columns: minmax(auto, 100%);
padding-bottom: 30px;
padding: 20px;
margin-bottom: 30px;
> div {
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 {
gap: 20px;
display: flex;
.pfp {
display: flex;
flex-shrink: 0;
align-items: center;
flex-direction: column;
}
.background {
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 {
.grant_permission {
margin-bottom: 18px;
.description {
font-weight: 400;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
font-size: 12px;
margin-bottom: 8px;
.audioRow {
margin-top: 20px;
display: flex;
gap: 20px;
.select {
flex-direction: column;
width: 50%;
}
}
.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;
display: flex;
width: 100%;
margin-bottom: 15px;
img {
cursor: pointer;
@ -541,6 +654,9 @@
.myBots {
.botCard {
display: flex;
flex-direction: column;
gap: 10px;
background: var(--secondary-background);
margin: 8px 0;
padding: 12px;
@ -566,6 +682,13 @@
}
}
.infocontainer {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.infoheader {
gap: 8px;
width: 100%;
@ -581,6 +704,10 @@
align-items: center;
flex-direction: row;
width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.userDetail {
@ -593,9 +720,13 @@
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.userName {
.username {
display: flex;
flex-direction: row;
align-items: center;
@ -619,9 +750,23 @@
gap: 4px;
color: var(--tertiary-foreground);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
> :nth-child(2) {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
a {
color: inherit;
cursor: pointer;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
}
@ -631,6 +776,20 @@
flex-direction: row;
gap: 10px;
}
@media only screen and (max-width: 800px) {
.infocontainer {
flex-direction: column;
> button {
width: 100%;
}
}
.buttonRow {
flex-direction: column;
}
}
}
section {

View file

@ -1,3 +1,4 @@
import { Markdown } from "@styled-icons/boxicons-logos";
import { observer } from "mobx-react-lite";
import { Profile as ProfileI } from "revolt-api/types/Users";
@ -62,7 +63,7 @@ export const Profile = observer(() => {
return (
<div className={styles.user}>
<h3>
<Text id="app.settings.pages.profile.preview" />
<Text id="app.special.modals.actions.preview" />
</h3>
<div className={styles.preview}>
<UserProfile
@ -71,6 +72,12 @@ export const Profile = observer(() => {
dummyProfile={profile}
/>
</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.pfp}>
<h3>
@ -155,6 +162,19 @@ export const Profile = observer(() => {
onFocus={onFocus}
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>
<Button
contrast

View file

@ -14,6 +14,10 @@ interface Props {
export function Component(props: Props) {
return (
<div className={styles.notifications}>
{/*<h3>
<Text id="app.settings.pages.sync.options" />
</h3>
<h5>Sync items automatically</h5>*/}
<h3>
<Text id="app.settings.pages.sync.categories" />
</h3>
@ -46,6 +50,9 @@ export function Component(props: Props) {
<Text id={`app.settings.pages.${title}`} />
</Checkbox>
))}
{/*<h5 style={{ marginTop: "20px", color: "grey" }}>
Last sync at 12:00
</h5>*/}
</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 { useEffect, useState } from "preact/hooks";
@ -6,6 +13,7 @@ import { dispatch } from "../../../redux";
import { Theme, generateVariables, ThemeOptions } from "../../../context/Theme";
import InputBox from "../../../components/ui/InputBox";
import Tip from "../../../components/ui/Tip";
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
const ThemeInfo = styled.article`
display: grid;
grid:
"preview name creator" min-content
"preview desc desc" 1fr
/ 200px 1fr 1fr;
gap: 0.5rem 1rem;
display: flex;
flex-direction: column;
gap: 10px;
padding: 1rem;
border-radius: var(--border-radius);
background: var(--secondary-background);
@ -93,6 +97,7 @@ const ThemeInfo = styled.article`
}
.name {
margin-top: 5px !important;
grid-area: name;
margin: 0;
}
@ -104,15 +109,115 @@ const ThemeInfo = styled.article`
}
.description {
margin-bottom: 5px;
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`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
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 }>`
${(props) => props.theme && generateVariables(props.theme)}
`;
@ -140,6 +245,10 @@ const ThemePreview = ({ theme, ...props }: ThemePreviewProps) => {
const ThemeShopRoot = styled.div`
display: grid;
gap: 1rem;
h5 {
margin-bottom: 0;
}
`;
export function ThemeShop() {
@ -175,18 +284,71 @@ export function ThemeShop() {
return (
<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>
This section is under construction.
The Theme Shop is currently under construction.
</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?.map(([slug, theme]) => (
<ThemeInfo
key={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
class="preview"
onClick={() => {
@ -195,17 +357,27 @@ export function ThemeShop() {
theme: {
slug,
meta: theme,
theme: themeData[slug]
}
})
theme: themeData[slug],
},
});
dispatch({
type: "SETTINGS_SET_THEME",
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>
<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>
))}
</ThemeList>

View file

@ -17,9 +17,9 @@ self.addEventListener("push", (event) => {
icon: data.icon,
image: data.image,
body: data.body,
timestamp: data.timestamp,
timestamp: data.timestamp * 1000,
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,
});
}