Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 841 additions and 548 deletions
import { InfoCircle } from "@styled-icons/boxicons-regular"; import { User } from "revolt.js/dist/maps/Users";
import styled from "styled-components";
import { Children } from "../../../types/Preact"; import { Children } from "../../../types/Preact";
import Tooltip from "../Tooltip";
import { Username } from "./UserShort"; import { Username } from "./UserShort";
import styled from "styled-components";
import UserStatus from "./UserStatus"; import UserStatus from "./UserStatus";
import Tooltip from "../Tooltip";
import { User } from "revolt.js";
interface Props { interface Props {
user?: User, user?: User;
children: Children children: Children;
} }
const Base = styled.div` const Base = styled.div`
...@@ -38,16 +38,18 @@ const Base = styled.div` ...@@ -38,16 +38,18 @@ const Base = styled.div`
export default function UserHover({ user, children }: Props) { export default function UserHover({ user, children }: Props) {
return ( return (
<Tooltip placement="right-end" content={ <Tooltip
<Base> placement="right-end"
<Username className="username" user={user} /> content={
<span className="status"> <Base>
<UserStatus user={user} /> <Username className="username" user={user} />
</span> <span className="status">
{/*<div className="tip"><InfoCircle size={13}/>Right-click on the avatar to access the quick menu</div>*/} <UserStatus user={user} />
</Base> </span>
}> {/*<div className="tip"><InfoCircle size={13}/>Right-click on the avatar to access the quick menu</div>*/}
{ children } </Base>
}>
{children}
</Tooltip> </Tooltip>
) );
} }
import { MicrophoneOff } from "@styled-icons/boxicons-regular"; import { MicrophoneOff } from "@styled-icons/boxicons-regular";
import { User } from "revolt.js"; import { observer } from "mobx-react-lite";
import { Users } from "revolt.js/dist/api/objects"; import { Presence } from "revolt-api/types/Users";
import { User } from "revolt.js/dist/maps/Users";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
...@@ -21,10 +22,10 @@ interface Props extends IconBaseProps<User> { ...@@ -21,10 +22,10 @@ interface Props extends IconBaseProps<User> {
export function useStatusColour(user?: User) { export function useStatusColour(user?: User) {
const theme = useContext(ThemeContext); const theme = useContext(ThemeContext);
return user?.online && user?.status?.presence !== Users.Presence.Invisible return user?.online && user?.status?.presence !== Presence.Invisible
? user?.status?.presence === Users.Presence.Idle ? user?.status?.presence === Presence.Idle
? theme["status-away"] ? theme["status-away"]
: user?.status?.presence === Users.Presence.Busy : user?.status?.presence === Presence.Busy
? theme["status-busy"] ? theme["status-busy"]
: theme["status-online"] : theme["status-online"]
: theme["status-invisible"]; : theme["status-invisible"];
...@@ -50,55 +51,68 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>` ...@@ -50,55 +51,68 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
`} `}
`; `;
export default function UserIcon( export default observer(
props: Props & Omit<JSX.SVGAttributes<SVGSVGElement>, keyof Props>, (
) { props: Props &
const client = useContext(AppContext); Omit<
JSX.SVGAttributes<SVGSVGElement>,
keyof Props | "children" | "as"
>,
) => {
const client = useContext(AppContext);
const { const {
target, target,
attachment, attachment,
size, size,
voice, status,
status,
animate,
mask,
children,
as,
...svgProps
} = props;
const iconURL =
client.generateFileURL(
target?.avatar ?? attachment,
{ max_side: 256 },
animate, animate,
) ?? (target ? client.users.getDefaultAvatarURL(target._id) : fallback); mask,
hover,
...svgProps
} = props;
const iconURL =
client.generateFileURL(
target?.avatar ?? attachment,
{ max_side: 256 },
animate,
) ?? (target ? target.defaultAvatarURL : fallback);
return ( return (
<IconBase <IconBase
{...svgProps} {...svgProps}
width={size} width={size}
height={size} height={size}
aria-hidden="true" hover={hover}
viewBox="0 0 32 32"> aria-hidden="true"
<foreignObject viewBox="0 0 32 32">
x="0" <foreignObject
y="0" x="0"
width="32" y="0"
height="32" width="32"
mask={mask ?? (status ? "url(#user)" : undefined)}> height="32"
{<img src={iconURL} draggable={false} loading="lazy" />} class="icon"
</foreignObject> mask={mask ?? (status ? "url(#user)" : undefined)}>
{props.status && ( {<img src={iconURL} draggable={false} loading="lazy" />}
<circle cx="27" cy="27" r="5" fill={useStatusColour(target)} />
)}
{props.voice && (
<foreignObject x="22" y="22" width="10" height="10">
<VoiceIndicator status={props.voice}>
{props.voice === "muted" && <MicrophoneOff size={6} />}
</VoiceIndicator>
</foreignObject> </foreignObject>
)} {props.status && (
</IconBase> <circle
); cx="27"
} cy="27"
r="5"
fill={useStatusColour(target)}
/>
)}
{props.voice && (
<foreignObject x="22" y="22" width="10" height="10">
<VoiceIndicator status={props.voice}>
{props.voice === "muted" && (
<MicrophoneOff size={6} />
)}
</VoiceIndicator>
</foreignObject>
)}
</IconBase>
);
},
);
import { User } from "revolt.js"; import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom";
import { User } from "revolt.js/dist/maps/Users";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import UserIcon from "./UserIcon"; import UserIcon from "./UserIcon";
export function Username({ export const Username = observer(
user, ({
...otherProps user,
}: { user?: User } & JSX.HTMLAttributes<HTMLElement>) { ...otherProps
return ( }: { user?: User } & JSX.HTMLAttributes<HTMLElement>) => {
<span {...otherProps}> let username = user?.username;
{user?.username ?? <Text id="app.main.channel.unknown_user" />} let color;
</span>
); if (user) {
} const { server } = useParams<{ server?: string }>();
if (server) {
const client = useClient();
const member = client.members.getKey({
server,
user: user._id,
});
if (member) {
if (member.nickname) {
username = member.nickname;
}
if (member.roles && member.roles.length > 0) {
const srv = client.servers.get(member._id.server);
if (srv?.roles) {
for (const role of member.roles) {
const c = srv.roles[role].colour;
if (c) {
color = c;
continue;
}
}
}
}
}
}
}
return (
<span {...otherProps} style={{ color }}>
{username ?? <Text id="app.main.channel.unknown_user" />}
</span>
);
},
);
export default function UserShort({ export default function UserShort({
user, user,
......
import { User } from "revolt.js"; import { observer } from "mobx-react-lite";
import { Users } from "revolt.js/dist/api/objects"; import { Presence } from "revolt-api/types/Users";
import { User } from "revolt.js/dist/maps/Users";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import Tooltip from "../Tooltip"; import Tooltip from "../Tooltip";
interface Props { interface Props {
...@@ -9,29 +11,29 @@ interface Props { ...@@ -9,29 +11,29 @@ interface Props {
tooltip?: boolean; tooltip?: boolean;
} }
export default function UserStatus({ user, tooltip }: Props) { export default observer(({ user, tooltip }: Props) => {
if (user?.online) { if (user?.online) {
if (user.status?.text) { if (user.status?.text) {
if (tooltip) { if (tooltip) {
return ( return (
<Tooltip arrow={undefined} content={ user.status.text }> <Tooltip arrow={undefined} content={user.status.text}>
{ user.status.text } {user.status.text}
</Tooltip> </Tooltip>
) );
} }
return <>{user.status.text}</>; return <>{user.status.text}</>;
} }
if (user.status?.presence === Users.Presence.Busy) { if (user.status?.presence === Presence.Busy) {
return <Text id="app.status.busy" />; return <Text id="app.status.busy" />;
} }
if (user.status?.presence === Users.Presence.Idle) { if (user.status?.presence === Presence.Idle) {
return <Text id="app.status.idle" />; return <Text id="app.status.idle" />;
} }
if (user.status?.presence === Users.Presence.Invisible) { if (user.status?.presence === Presence.Invisible) {
return <Text id="app.status.offline" />; return <Text id="app.status.offline" />;
} }
...@@ -39,4 +41,4 @@ export default function UserStatus({ user, tooltip }: Props) { ...@@ -39,4 +41,4 @@ export default function UserStatus({ user, tooltip }: Props) {
} }
return <Text id="app.status.offline" />; return <Text id="app.status.offline" />;
} });
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
margin-bottom: 0; margin-bottom: 0;
margin-top: 1px; margin-top: 1px;
margin-right: 2px; margin-right: 2px;
vertical-align: -.3em; vertical-align: -0.3em;
} }
p, p,
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
font-size: 90%; font-size: 90%;
background: var(--block); background: var(--block);
border-radius: var(--border-radius); border-radius: var(--border-radius);
font-family: var(--monoscape-font), monospace; font-family: var(--monospace-font), monospace;
} }
input[type="checkbox"] { input[type="checkbox"] {
...@@ -118,6 +118,7 @@ ...@@ -118,6 +118,7 @@
> * { > * {
opacity: 0; opacity: 0;
pointer-events: none;
} }
&:global(.shown) { &:global(.shown) {
...@@ -128,17 +129,18 @@ ...@@ -128,17 +129,18 @@
> * { > * {
opacity: 1; opacity: 1;
pointer-events: unset;
} }
} }
} }
:global(.code) { :global(.code) {
font-family: var(--monoscape-font), monospace; font-family: var(--monospace-font), monospace;
:global(.lang) { :global(.lang) {
width: fit-content; width: fit-content;
padding-bottom: 8px; padding-bottom: 8px;
div { div {
color: #111; color: #111;
cursor: pointer; cursor: pointer;
...@@ -174,7 +176,7 @@ ...@@ -174,7 +176,7 @@
input[type="checkbox"] + label:before { input[type="checkbox"] + label:before {
width: 12px; width: 12px;
height: 12px; height: 12px;
content: 'a'; content: "a";
font-size: 10px; font-size: 10px;
margin-right: 6px; margin-right: 6px;
line-height: 12px; line-height: 12px;
...@@ -185,7 +187,7 @@ ...@@ -185,7 +187,7 @@
} }
input[type="checkbox"][checked="true"] + label:before { input[type="checkbox"][checked="true"] + label:before {
content: '✓'; content: "✓";
align-items: center; align-items: center;
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
......
...@@ -9,7 +9,7 @@ export interface MarkdownProps { ...@@ -9,7 +9,7 @@ export interface MarkdownProps {
export default function Markdown(props: MarkdownProps) { export default function Markdown(props: MarkdownProps) {
return ( return (
// @ts-expect-error // @ts-expect-error Typings mis-match.
<Suspense fallback={props.content}> <Suspense fallback={props.content}>
<Renderer {...props} /> <Renderer {...props} />
</Suspense> </Suspense>
......
/* eslint-disable react-hooks/rules-of-hooks */
import MarkdownKatex from "@traptitech/markdown-it-katex"; import MarkdownKatex from "@traptitech/markdown-it-katex";
import MarkdownSpoilers from "@traptitech/markdown-it-spoiler"; import MarkdownSpoilers from "@traptitech/markdown-it-spoiler";
import "katex/dist/katex.min.css"; import "katex/dist/katex.min.css";
import MarkdownIt from "markdown-it"; import MarkdownIt from "markdown-it";
// @ts-ignore // @ts-expect-error No typings.
import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare"; import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
// @ts-ignore // @ts-expect-error No typings.
import MarkdownSub from "markdown-it-sub"; import MarkdownSub from "markdown-it-sub";
// @ts-ignore // @ts-expect-error No typings.
import MarkdownSup from "markdown-it-sup"; import MarkdownSup from "markdown-it-sup";
import Prism from "prismjs"; import Prism from "prismjs";
import "prismjs/themes/prism-tomorrow.css"; import "prismjs/themes/prism-tomorrow.css";
import { RE_MENTIONS } from "revolt.js"; import { RE_MENTIONS } from "revolt.js";
import styles from "./Markdown.module.scss"; import styles from "./Markdown.module.scss";
import { useCallback, useContext, useRef } from "preact/hooks"; import { useCallback, useContext } from "preact/hooks";
import { internalEmit } from "../../lib/eventEmitter"; import { internalEmit } from "../../lib/eventEmitter";
...@@ -35,7 +36,7 @@ declare global { ...@@ -35,7 +36,7 @@ declare global {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
window.copycode = function (element: HTMLDivElement) { window.copycode = function (element: HTMLDivElement) {
try { try {
let code = element.parentElement?.parentElement?.children[1]; const code = element.parentElement?.parentElement?.children[1];
if (code) { if (code) {
navigator.clipboard.writeText(code.textContent?.trim() ?? ""); navigator.clipboard.writeText(code.textContent?.trim() ?? "");
} }
...@@ -47,9 +48,9 @@ export const md: MarkdownIt = MarkdownIt({ ...@@ -47,9 +48,9 @@ export const md: MarkdownIt = MarkdownIt({
breaks: true, breaks: true,
linkify: true, linkify: true,
highlight: (str, lang) => { highlight: (str, lang) => {
let v = Prism.languages[lang]; const v = Prism.languages[lang];
if (v) { if (v) {
let out = Prism.highlight(str, v, lang); const out = Prism.highlight(str, v, lang);
return `<pre class="code"><div class="lang"><div onclick="copycode(this)">${lang}</div></div><code class="language-${lang}">${out}</code></pre>`; return `<pre class="code"><div class="lang"><div onclick="copycode(this)">${lang}</div></div><code class="language-${lang}">${out}</code></pre>`;
} }
...@@ -66,7 +67,9 @@ export const md: MarkdownIt = MarkdownIt({ ...@@ -66,7 +67,9 @@ export const md: MarkdownIt = MarkdownIt({
.use(MarkdownKatex, { .use(MarkdownKatex, {
throwOnError: false, throwOnError: false,
maxExpand: 0, maxExpand: 0,
maxSize: 10 maxSize: 10,
strict: false,
errorColor: "var(--error)",
}); });
// TODO: global.d.ts file for defining globals // TODO: global.d.ts file for defining globals
...@@ -82,6 +85,9 @@ md.renderer.rules.emoji = function (token, idx) { ...@@ -82,6 +85,9 @@ md.renderer.rules.emoji = function (token, idx) {
const RE_TWEMOJI = /:(\w+):/g; const RE_TWEMOJI = /:(\w+):/g;
// ! FIXME: Move to library
const RE_CHANNELS = /<#([A-z0-9]{26})>/g;
export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
const client = useContext(AppContext); const client = useContext(AppContext);
if (typeof content === "undefined") return null; if (typeof content === "undefined") return null;
...@@ -89,10 +95,9 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { ...@@ -89,10 +95,9 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
// We replace the message with the mention at the time of render. // We replace the message with the mention at the time of render.
// We don't care if the mention changes. // We don't care if the mention changes.
let newContent = content.replace( const newContent = content
RE_MENTIONS, .replace(RE_MENTIONS, (sub: string, ...args: unknown[]) => {
(sub: string, ...args: any[]) => { const id = args[0] as string,
const id = args[0],
user = client.users.get(id); user = client.users.get(id);
if (user) { if (user) {
...@@ -100,8 +105,17 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { ...@@ -100,8 +105,17 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
} }
return sub; return sub;
}, })
); .replace(RE_CHANNELS, (sub: string, ...args: unknown[]) => {
const id = args[0] as string,
channel = client.channels.get(id);
if (channel?.channel_type === "TextChannel") {
return `[#${channel.name}](/server/${channel.server_id}/channel/${id})`;
}
return sub;
});
const useLargeEmojis = disallowBigEmoji const useLargeEmojis = disallowBigEmoji
? false ? false
...@@ -109,7 +123,7 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { ...@@ -109,7 +123,7 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
const toggle = useCallback((ev: MouseEvent) => { const toggle = useCallback((ev: MouseEvent) => {
if (ev.currentTarget) { if (ev.currentTarget) {
let element = ev.currentTarget as HTMLDivElement; const element = ev.currentTarget as HTMLDivElement;
if (element.classList.contains("spoiler")) { if (element.classList.contains("spoiler")) {
element.classList.add("shown"); element.classList.add("shown");
} }
...@@ -123,7 +137,7 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { ...@@ -123,7 +137,7 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
const pathname = url.pathname; const pathname = url.pathname;
if (pathname.startsWith("/@")) { if (pathname.startsWith("/@")) {
let id = pathname.substr(2); const id = pathname.substr(2);
if (/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}/.test(id)) { if (/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}/.test(id)) {
ev.preventDefault(); ev.preventDefault();
internalEmit("Intermediate", "openProfile", id); internalEmit("Intermediate", "openProfile", id);
...@@ -137,19 +151,20 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { ...@@ -137,19 +151,20 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
return ( return (
<span <span
ref={el => { ref={(el) => {
if (el) { if (el) {
(el.querySelectorAll<HTMLDivElement>('.spoiler')) el.querySelectorAll<HTMLDivElement>(".spoiler").forEach(
.forEach(element => { (element) => {
element.removeEventListener('click', toggle); element.removeEventListener("click", toggle);
element.addEventListener('click', toggle); element.addEventListener("click", toggle);
}); },
);
(el.querySelectorAll<HTMLAnchorElement>('a'))
.forEach(element => { el.querySelectorAll<HTMLAnchorElement>("a").forEach(
element.removeEventListener('click', handleLink); (element) => {
element.removeAttribute('data-type'); element.removeEventListener("click", handleLink);
element.removeAttribute('target'); element.removeAttribute("data-type");
element.removeAttribute("target");
let internal; let internal;
const href = element.href; const href = element.href;
...@@ -159,19 +174,26 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { ...@@ -159,19 +174,26 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
if (url.hostname === location.hostname) { if (url.hostname === location.hostname) {
internal = true; internal = true;
element.addEventListener('click', handleLink); element.addEventListener(
"click",
if (url.pathname.startsWith('/@')) { handleLink,
element.setAttribute('data-type', 'mention'); );
if (url.pathname.startsWith("/@")) {
element.setAttribute(
"data-type",
"mention",
);
} }
} }
} catch (err) {} } catch (err) {}
} }
if (!internal) { if (!internal) {
element.setAttribute('target', '_blank'); element.setAttribute("target", "_blank");
} }
}); },
);
} }
}} }}
className={styles.markdown} className={styles.markdown}
......
import { Wrench } from "@styled-icons/boxicons-solid";
import styled from "styled-components";
import UpdateIndicator from "../common/UpdateIndicator";
const TitlebarBase = styled.div`
height: var(--titlebar-height);
display: flex;
user-select: none;
align-items: center;
.drag {
flex-grow: 1;
-webkit-app-region: drag;
margin-top: 10px;
height: 100%;
}
.quick {
color: var(--secondary-foreground);
> div,
> div > div {
width: var(--titlebar-height) !important;
}
&.disabled {
color: var(--error);
}
&.unavailable {
background: var(--error);
}
}
.title {
-webkit-app-region: drag;
/*height: var(--titlebar-height);*/
font-size: 16px;
font-weight: 600;
margin-inline-start: 10px;
margin-top: 10px;
gap: 6px;
display: flex;
align-items: center;
justify-content: flex-start;
z-index: 90000;
color: var(--titlebar-logo-color);
svg {
margin-bottom: 10px;
}
svg:first-child {
height: calc(var(--titlebar-height) / 3);
}
}
.actions {
z-index: 100;
display: flex;
align-items: center;
margin-inline-start: 6px;
div {
width: calc(
var(--titlebar-height) + var(--titlebar-action-padding)
);
height: var(--titlebar-height);
display: grid;
place-items: center;
transition: 0.2s ease color;
transition: 0.2s ease background-color;
&:hover {
background: var(--primary-background);
}
&.error:hover {
background: var(--error);
}
}
}
`;
export function Titlebar() {
return (
<TitlebarBase>
<div class="title">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 193.733 37.438">
<path
d="M23.393,1.382c0,2.787-1.52,4.46-4.764,4.46H13.258V-2.977H18.63C21.873-2.977,23.393-1.254,23.393,1.382Zm-24-11.555,5.2,7.213V25.4h8.666V11.973h2.078l7.4,13.43h9.781l-8.21-14.089A10.355,10.355,0,0,0,32.212,1.027c0-6.183-4.358-11.2-13.075-11.2Zm60.035,0H37.634V25.4H59.426V18.46H46.3v-7.8H57.906V3.966H46.3V-2.969H59.426Zm20.981,26.86-8.818-26.86H62.365L74.984,25.4H85.83L98.449-10.173H89.276Zm56.659-9.173c0-10.693-8.058-18.194-18.194-18.194-10.085,0-18.3,7.5-18.3,18.194a17.9,17.9,0,0,0,18.3,18.244A17.815,17.815,0,0,0,137.066,7.514Zm-27.62,0c0-6.335,3.649-10.338,9.426-10.338,5.676,0,9.376,4,9.376,10.338,0,6.233-3.7,10.338-9.376,10.338C113.095,17.852,109.446,13.747,109.446,7.514ZM141.88-10.173V25.4H161.9v-6.95H150.545V-10.173Zm22.248,7.2h9.426V25.4h8.666V-2.975h9.426v-7.2H164.128Z"
transform="translate(1.586 11.18)"
fill="var(--titlebar-logo-color)"
stroke="var(--titlebar-logo-color)"
stroke-width="1"
/>
</svg>
{window.native.getConfig().build === "dev" && (
<Wrench size="12.5" />
)}
</div>
{/*<div class="actions quick">
<Tooltip
content="Mute"
placement="bottom">
<div onClick={window.native.min}>
<Microphone size={15}/>
</div>
</Tooltip>
<Tooltip
content="Deafen"
placement="bottom">
<div onClick={window.native.min}>
<VolumeFull size={15}/>
</div>
</Tooltip>
</div>*/}
<div class="drag" />
<UpdateIndicator style="titlebar" />
<div class="actions">
<div onClick={window.native.min}>
<svg
aria-hidden="false"
width="12"
height="12"
viewBox="0 0 12 12">
<rect
fill="currentColor"
width="10"
height="1"
x="1"
y="6"
/>
</svg>
</div>
<div onClick={window.native.max}>
<svg
aria-hidden="false"
width="12"
height="12"
viewBox="0 0 12 12">
<rect
width="9"
height="9"
x="1.5"
y="1.5"
fill="none"
stroke="currentColor"
/>
</svg>
</div>
<div onClick={window.native.close} class="error">
<svg
aria-hidden="false"
width="12"
height="12"
viewBox="0 0 12 12">
<polygon
fill="currentColor"
stroke-width="1"
fill-rule="evenodd"
points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"
style="stroke:currentColor;stroke-width:0.4"
/>
</svg>
</div>
</div>
</TitlebarBase>
);
}
import { Message, Group, Inbox } from "@styled-icons/boxicons-solid"; import { Message, Group } from "@styled-icons/boxicons-solid";
import { Search } from "@styled-icons/boxicons-regular"; import { observer } from "mobx-react-lite";
import { useHistory, useLocation } from "react-router"; import { useHistory, useLocation } from "react-router";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
...@@ -8,7 +8,7 @@ import ConditionalLink from "../../lib/ConditionalLink"; ...@@ -8,7 +8,7 @@ import ConditionalLink from "../../lib/ConditionalLink";
import { connectState } from "../../redux/connector"; import { connectState } from "../../redux/connector";
import { LastOpened } from "../../redux/reducers/last_opened"; import { LastOpened } from "../../redux/reducers/last_opened";
import { useSelf } from "../../context/revoltjs/hooks"; import { useClient } from "../../context/revoltjs/RevoltClient";
import UserIcon from "../common/user/UserIcon"; import UserIcon from "../common/user/UserIcon";
import IconButton from "../ui/IconButton"; import IconButton from "../ui/IconButton";
...@@ -51,8 +51,10 @@ interface Props { ...@@ -51,8 +51,10 @@ interface Props {
lastOpened: LastOpened; lastOpened: LastOpened;
} }
export function BottomNavigation({ lastOpened }: Props) { export const BottomNavigation = observer(({ lastOpened }: Props) => {
const user = useSelf(); const client = useClient();
const user = client.users.get(client.user!._id);
const history = useHistory(); const history = useHistory();
const path = useLocation().pathname; const path = useLocation().pathname;
...@@ -113,9 +115,8 @@ export function BottomNavigation({ lastOpened }: Props) { ...@@ -113,9 +115,8 @@ export function BottomNavigation({ lastOpened }: Props) {
</Button> </Button>
</Navbar> </Navbar>
</Base> </Base>
); );
} });
export default connectState(BottomNavigation, (state) => { export default connectState(BottomNavigation, (state) => {
return { return {
......
import { X, Crown } from "@styled-icons/boxicons-regular"; import { X, Crown } from "@styled-icons/boxicons-regular";
import { Channels, Users } from "revolt.js/dist/api/objects"; import { observer } from "mobx-react-lite";
import { Presence } from "revolt-api/types/Users";
import { Channel } from "revolt.js/dist/maps/Channels";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./Item.module.scss"; import styles from "./Item.module.scss";
import classNames from "classnames"; import classNames from "classnames";
...@@ -14,6 +17,7 @@ import { useIntermediate } from "../../../context/intermediate/Intermediate"; ...@@ -14,6 +17,7 @@ import { useIntermediate } from "../../../context/intermediate/Intermediate";
import ChannelIcon from "../../common/ChannelIcon"; import ChannelIcon from "../../common/ChannelIcon";
import Tooltip from "../../common/Tooltip"; import Tooltip from "../../common/Tooltip";
import UserIcon from "../../common/user/UserIcon"; import UserIcon from "../../common/user/UserIcon";
import { Username } from "../../common/user/UserShort";
import UserStatus from "../../common/user/UserStatus"; import UserStatus from "../../common/user/UserStatus";
import IconButton from "../../ui/IconButton"; import IconButton from "../../ui/IconButton";
...@@ -29,12 +33,12 @@ type CommonProps = Omit< ...@@ -29,12 +33,12 @@ type CommonProps = Omit<
}; };
type UserProps = CommonProps & { type UserProps = CommonProps & {
user: Users.User; user: User;
context?: Channels.Channel; context?: Channel;
channel?: Channels.DirectMessageChannel; channel?: Channel;
}; };
export function UserButton(props: UserProps) { export const UserButton = observer((props: UserProps) => {
const { active, alert, alertCount, user, context, channel, ...divProps } = const { active, alert, alertCount, user, context, channel, ...divProps } =
props; props;
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
...@@ -47,8 +51,7 @@ export function UserButton(props: UserProps) { ...@@ -47,8 +51,7 @@ export function UserButton(props: UserProps) {
data-alert={typeof alert === "string"} data-alert={typeof alert === "string"}
data-online={ data-online={
typeof channel !== "undefined" || typeof channel !== "undefined" ||
(user.online && (user.online && user.status?.presence !== Presence.Invisible)
user.status?.presence !== Users.Presence.Invisible)
} }
onContextMenu={attachContextMenu("Menu", { onContextMenu={attachContextMenu("Menu", {
user: user._id, user: user._id,
...@@ -63,11 +66,13 @@ export function UserButton(props: UserProps) { ...@@ -63,11 +66,13 @@ export function UserButton(props: UserProps) {
status status
/> />
<div className={styles.name}> <div className={styles.name}>
<div>{user.username}</div> <div>
<Username user={user} />
</div>
{ {
<div className={styles.subText}> <div className={styles.subText}>
{channel?.last_message && alert ? ( {channel?.last_message && alert ? (
channel.last_message.short (channel.last_message as { short: string }).short
) : ( ) : (
<UserStatus user={user} /> <UserStatus user={user} />
)} )}
...@@ -76,7 +81,7 @@ export function UserButton(props: UserProps) { ...@@ -76,7 +81,7 @@ export function UserButton(props: UserProps) {
</div> </div>
<div className={styles.button}> <div className={styles.button}>
{context?.channel_type === "Group" && {context?.channel_type === "Group" &&
context.owner === user._id && ( context.owner_id === user._id && (
<Localizer> <Localizer>
<Tooltip <Tooltip
content={<Text id="app.main.groups.owner" />}> content={<Text id="app.main.groups.owner" />}>
...@@ -106,15 +111,15 @@ export function UserButton(props: UserProps) { ...@@ -106,15 +111,15 @@ export function UserButton(props: UserProps) {
</div> </div>
</div> </div>
); );
} });
type ChannelProps = CommonProps & { type ChannelProps = CommonProps & {
channel: Channels.Channel & { unread?: string }; channel: Channel & { unread?: string };
user?: Users.User; user?: User;
compact?: boolean; compact?: boolean;
}; };
export function ChannelButton(props: ChannelProps) { export const ChannelButton = observer((props: ChannelProps) => {
const { active, alert, alertCount, channel, user, compact, ...divProps } = const { active, alert, alertCount, channel, user, compact, ...divProps } =
props; props;
...@@ -131,7 +136,7 @@ export function ChannelButton(props: ChannelProps) { ...@@ -131,7 +136,7 @@ export function ChannelButton(props: ChannelProps) {
{...divProps} {...divProps}
data-active={active} data-active={active}
data-alert={typeof alert === "string"} data-alert={typeof alert === "string"}
aria-label={{}} /*FIXME: ADD ARIA LABEL*/ aria-label={channel.name}
className={classNames(styles.item, { [styles.compact]: compact })} className={classNames(styles.item, { [styles.compact]: compact })}
onContextMenu={attachContextMenu("Menu", { onContextMenu={attachContextMenu("Menu", {
channel: channel._id, channel: channel._id,
...@@ -147,12 +152,12 @@ export function ChannelButton(props: ChannelProps) { ...@@ -147,12 +152,12 @@ export function ChannelButton(props: ChannelProps) {
{channel.channel_type === "Group" && ( {channel.channel_type === "Group" && (
<div className={styles.subText}> <div className={styles.subText}>
{channel.last_message && alert ? ( {channel.last_message && alert ? (
channel.last_message.short (channel.last_message as { short: string }).short
) : ( ) : (
<Text <Text
id="quantities.members" id="quantities.members"
plural={channel.recipients.length} plural={channel.recipients!.length}
fields={{ count: channel.recipients.length }} fields={{ count: channel.recipients!.length }}
/> />
)} )}
</div> </div>
...@@ -180,7 +185,7 @@ export function ChannelButton(props: ChannelProps) { ...@@ -180,7 +185,7 @@ export function ChannelButton(props: ChannelProps) {
</div> </div>
</div> </div>
); );
} });
type ButtonProps = CommonProps & { type ButtonProps = CommonProps & {
onClick?: () => void; onClick?: () => void;
......
...@@ -4,9 +4,9 @@ import { ...@@ -4,9 +4,9 @@ import {
Wrench, Wrench,
Notepad, Notepad,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link, Redirect, useLocation, useParams } from "react-router-dom"; import { Link, Redirect, useLocation, useParams } from "react-router-dom";
import { Channels } from "revolt.js/dist/api/objects"; import { RelationshipStatus } from "revolt-api/types/Users";
import { Users as UsersNS } from "revolt.js/dist/api/objects";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect } from "preact/hooks"; import { useContext, useEffect } from "preact/hooks";
...@@ -21,11 +21,6 @@ import { Unreads } from "../../../redux/reducers/unreads"; ...@@ -21,11 +21,6 @@ import { Unreads } from "../../../redux/reducers/unreads";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import {
useDMs,
useForceUpdate,
useUsers,
} from "../../../context/revoltjs/hooks";
import Category from "../../ui/Category"; import Category from "../../ui/Category";
import placeholderSVG from "../items/placeholder.svg"; import placeholderSVG from "../items/placeholder.svg";
...@@ -39,16 +34,21 @@ type Props = { ...@@ -39,16 +34,21 @@ type Props = {
unreads: Unreads; unreads: Unreads;
}; };
function HomeSidebar(props: Props) { const HomeSidebar = observer((props: Props) => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const client = useContext(AppContext); const client = useContext(AppContext);
const { channel } = useParams<{ channel: string }>(); const { channel } = useParams<{ channel: string }>();
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const ctx = useForceUpdate(); const channels = [...client.channels.values()]
const channels = useDMs(ctx); .filter(
(x) =>
x.channel_type === "DirectMessage" ||
x.channel_type === "Group",
)
.map((x) => mapChannelWithUnread(x, props.unreads));
const obj = channels.find((x) => x?._id === channel); const obj = client.channels.get(channel);
if (channel && !obj) return <Redirect to="/" />; if (channel && !obj) return <Redirect to="/" />;
if (obj) useUnreads({ ...props, channel: obj }); if (obj) useUnreads({ ...props, channel: obj });
...@@ -62,21 +62,7 @@ function HomeSidebar(props: Props) { ...@@ -62,21 +62,7 @@ function HomeSidebar(props: Props) {
}); });
}, [channel]); }, [channel]);
const channelsArr = channels channels.sort((b, a) => a.timestamp.localeCompare(b.timestamp));
.filter((x) => x.channel_type !== "SavedMessages")
.map((x) => mapChannelWithUnread(x, props.unreads));
const users = useUsers(
(
channelsArr as (
| Channels.DirectMessageChannel
| Channels.GroupChannel
)[]
).reduce((prev: any, cur) => [...prev, ...cur.recipients], []),
ctx,
);
channelsArr.sort((b, a) => a.timestamp.localeCompare(b.timestamp));
return ( return (
<GenericSidebarBase padding> <GenericSidebarBase padding>
...@@ -98,10 +84,10 @@ function HomeSidebar(props: Props) { ...@@ -98,10 +84,10 @@ function HomeSidebar(props: Props) {
<ButtonItem <ButtonItem
active={pathname === "/friends"} active={pathname === "/friends"}
alert={ alert={
typeof users.find( typeof [...client.users.values()].find(
(user) => (user) =>
user?.relationship === user?.relationship ===
UsersNS.Relationship.Incoming, RelationshipStatus.Incoming,
) !== "undefined" ) !== "undefined"
? "unread" ? "unread"
: undefined : undefined
...@@ -143,18 +129,18 @@ function HomeSidebar(props: Props) { ...@@ -143,18 +129,18 @@ function HomeSidebar(props: Props) {
}) })
} }
/> />
{channelsArr.length === 0 && <img src={placeholderSVG} />} {channels.length === 0 && (
{channelsArr.map((x) => { <img src={placeholderSVG} loading="eager" />
)}
{channels.map((x) => {
let user; let user;
if (x.channel_type === "DirectMessage") { if (x.channel.channel_type === "DirectMessage") {
if (!x.active) return null; if (!x.channel.active) return null;
user = x.channel.recipient;
let recipient = client.channels.getRecipient(x._id);
user = users.find((x) => x?._id === recipient);
if (!user) { if (!user) {
console.warn( console.warn(
`Skipped DM ${x._id} because user was missing.`, `Skipped DM ${x.channel._id} because user was missing.`,
); );
return null; return null;
} }
...@@ -162,14 +148,15 @@ function HomeSidebar(props: Props) { ...@@ -162,14 +148,15 @@ function HomeSidebar(props: Props) {
return ( return (
<ConditionalLink <ConditionalLink
active={x._id === channel} key={x.channel._id}
to={`/channel/${x._id}`}> active={x.channel._id === channel}
to={`/channel/${x.channel._id}`}>
<ChannelButton <ChannelButton
user={user} user={user}
channel={x} channel={x.channel}
alert={x.unread} alert={x.unread}
alertCount={x.alertCount} alertCount={x.alertCount}
active={x._id === channel} active={x.channel._id === channel}
/> />
</ConditionalLink> </ConditionalLink>
); );
...@@ -178,7 +165,7 @@ function HomeSidebar(props: Props) { ...@@ -178,7 +165,7 @@ function HomeSidebar(props: Props) {
</GenericSidebarList> </GenericSidebarList>
</GenericSidebarBase> </GenericSidebarBase>
); );
} });
export default connectState( export default connectState(
HomeSidebar, HomeSidebar,
......
import { Plus } from "@styled-icons/boxicons-regular"; import { Plus } from "@styled-icons/boxicons-regular";
import { useLocation, useParams } from "react-router-dom"; import { observer } from "mobx-react-lite";
import { Channel, Servers } from "revolt.js/dist/api/objects"; import { useHistory, useLocation, useParams } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { attachContextMenu, openContextMenu } from "preact-context-menu"; import { attachContextMenu } from "preact-context-menu";
import ConditionalLink from "../../../lib/ConditionalLink"; import ConditionalLink from "../../../lib/ConditionalLink";
import PaintCounter from "../../../lib/PaintCounter"; import PaintCounter from "../../../lib/PaintCounter";
...@@ -14,23 +15,17 @@ import { LastOpened } from "../../../redux/reducers/last_opened"; ...@@ -14,23 +15,17 @@ import { LastOpened } from "../../../redux/reducers/last_opened";
import { Unreads } from "../../../redux/reducers/unreads"; import { Unreads } from "../../../redux/reducers/unreads";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { import { useClient } from "../../../context/revoltjs/RevoltClient";
useChannels,
useForceUpdate,
useSelf,
useServers,
} from "../../../context/revoltjs/hooks";
import ServerIcon from "../../common/ServerIcon"; import ServerIcon from "../../common/ServerIcon";
import Tooltip from "../../common/Tooltip"; import Tooltip from "../../common/Tooltip";
import UserHover from "../../common/user/UserHover";
import UserIcon from "../../common/user/UserIcon"; import UserIcon from "../../common/user/UserIcon";
import IconButton from "../../ui/IconButton"; import IconButton from "../../ui/IconButton";
import LineDivider from "../../ui/LineDivider"; import LineDivider from "../../ui/LineDivider";
import { mapChannelWithUnread } from "./common"; import { mapChannelWithUnread } from "./common";
import logoSVG from '../../../assets/logo.svg';
import { Children } from "../../../types/Preact"; import { Children } from "../../../types/Preact";
import UserHover from "../../common/user/UserHover";
function Icon({ function Icon({
children, children,
...@@ -56,7 +51,7 @@ function Icon({ ...@@ -56,7 +51,7 @@ function Icon({
<circle cx="27" cy="5" r="5" fill={"white"} /> <circle cx="27" cy="5" r="5" fill={"white"} />
)} )}
{unread === "mention" && ( {unread === "mention" && (
<circle cx="27" cy="5" r="5" fill={"red"} /> <circle cx="27" cy="5" r="5" fill={"var(--error)"} />
)} )}
</svg> </svg>
); );
...@@ -65,6 +60,7 @@ function Icon({ ...@@ -65,6 +60,7 @@ function Icon({
const ServersBase = styled.div` const ServersBase = styled.div`
width: 56px; width: 56px;
height: 100%; height: 100%;
padding-left: 2px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -78,7 +74,8 @@ const ServerList = styled.div` ...@@ -78,7 +74,8 @@ const ServerList = styled.div`
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
overflow-y: scroll; overflow-y: scroll;
padding-bottom: 48px; padding-bottom: 20px;
/*width: 58px;*/
flex-direction: column; flex-direction: column;
scrollbar-width: none; scrollbar-width: none;
...@@ -90,6 +87,11 @@ const ServerList = styled.div` ...@@ -90,6 +87,11 @@ const ServerList = styled.div`
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 0px; width: 0px;
} }
/*${isTouchscreenDevice &&
css`
width: 58px;
`}*/
`; `;
const ServerEntry = styled.div<{ active: boolean; home?: boolean }>` const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
...@@ -97,10 +99,13 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>` ...@@ -97,10 +99,13 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
display: flex; display: flex;
align-items: center; align-items: center;
:focus {
outline: 3px solid blue;
}
> div { > div {
height: 42px; height: 42px;
padding-left: 4px; padding-inline-start: 6px;
padding-right: 6px;
display: grid; display: grid;
place-items: center; place-items: center;
...@@ -129,14 +134,10 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>` ...@@ -129,14 +134,10 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
!props.active && !props.active &&
css` css`
display: none; display: none;
` } `}
svg { svg {
width: 57px; margin-top: 5px;
height: 117px;
margin-top: 4px;
display: relative;
pointer-events: none; pointer-events: none;
// outline: 1px solid red; // outline: 1px solid red;
} }
...@@ -152,13 +153,26 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>` ...@@ -152,13 +153,26 @@ const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
function Swoosh() { function Swoosh() {
return ( return (
<span> <span>
<svg xmlns="http://www.w3.org/2000/svg" width="57" height="117" fill="var(--sidebar-active)"> <svg
<path d="M27.746 86.465c14 0 28 11.407 28 28s.256-56 .256-56-42.256 28-28.256 28z"/> width="54"
<path d="M56 58.465c0 15.464-12.536 28-28 28s-28-12.536-28-28 12.536-28 28-28 28 12.536 28 28z"/> height="106"
<path d="M28.002 30.465c14 0 28-11.407 28-28s0 56 0 56-42-28-28-28z"/> viewBox="0 0 54 106"
xmlns="http://www.w3.org/2000/svg">
<path
d="M54 53C54 67.9117 41.9117 80 27 80C12.0883 80 0 67.9117 0 53C0 38.0883 12.0883 26 27 26C41.9117 26 54 38.0883 54 53Z"
fill="var(--sidebar-active)"
/>
<path
d="M27 80C4.5 80 54 53 54 53L54.0001 106C54.0001 106 49.5 80 27 80Z"
fill="var(--sidebar-active)"
/>
<path
d="M27 26C4.5 26 54 53 54 53L53.9999 0C53.9999 0 49.5 26 27 26Z"
fill="var(--sidebar-active)"
/>
</svg> </svg>
</span> </span>
) );
} }
interface Props { interface Props {
...@@ -166,28 +180,32 @@ interface Props { ...@@ -166,28 +180,32 @@ interface Props {
lastOpened: LastOpened; lastOpened: LastOpened;
} }
export function ServerListSidebar({ unreads, lastOpened }: Props) { export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
const ctx = useForceUpdate(); const client = useClient();
const self = useSelf(ctx);
const activeServers = useServers(undefined, ctx) as Servers.Server[]; const { server: server_id } = useParams<{ server?: string }>();
const channels = (useChannels(undefined, ctx) as Channel[]).map((x) => const server = server_id ? client.servers.get(server_id) : undefined;
const activeServers = [...client.servers.values()];
const channels = [...client.channels.values()].map((x) =>
mapChannelWithUnread(x, unreads), mapChannelWithUnread(x, unreads),
); );
const unreadChannels = channels.filter((x) => x.unread).map((x) => x._id); const unreadChannels = channels
.filter((x) => x.unread)
.map((x) => x.channel?._id);
const servers = activeServers.map((server) => { const servers = activeServers.map((server) => {
let alertCount = 0; let alertCount = 0;
for (let id of server.channels) { for (const id of server.channel_ids) {
let channel = channels.find((x) => x._id === id); const channel = channels.find((x) => x.channel?._id === id);
if (channel?.alertCount) { if (channel?.alertCount) {
alertCount += channel.alertCount; alertCount += channel.alertCount;
} }
} }
return { return {
...server, server,
unread: (typeof server.channels.find((x) => unread: (typeof server.channel_ids.find((x) =>
unreadChannels.includes(x), unreadChannels.includes(x),
) !== "undefined" ) !== "undefined"
? alertCount > 0 ? alertCount > 0
...@@ -198,18 +216,17 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) { ...@@ -198,18 +216,17 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) {
}; };
}); });
const history = useHistory();
const path = useLocation().pathname; const path = useLocation().pathname;
const { server: server_id } = useParams<{ server?: string }>();
const server = servers.find((x) => x!._id == server_id);
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
let homeUnread: "mention" | "unread" | undefined; let homeUnread: "mention" | "unread" | undefined;
let alertCount = 0; let alertCount = 0;
for (let x of channels) { for (const x of channels) {
if ( if (
((x.channel_type === "DirectMessage" && x.active) || (x.channel?.channel_type === "DirectMessage"
x.channel_type === "Group") && ? x.channel?.active
: x.channel?.channel_type === "Group") &&
x.unread x.unread
) { ) {
homeUnread = "unread"; homeUnread = "unread";
...@@ -217,6 +234,14 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) { ...@@ -217,6 +234,14 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) {
} }
} }
if (
[...client.users.values()].find(
(x) => x.relationship === RelationshipStatus.Incoming,
)
) {
alertCount++;
}
if (alertCount > 0) homeUnread = "mention"; if (alertCount > 0) homeUnread = "mention";
const homeActive = const homeActive =
typeof server === "undefined" && !path.startsWith("/invite"); typeof server === "undefined" && !path.startsWith("/invite");
...@@ -229,45 +254,50 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) { ...@@ -229,45 +254,50 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) {
to={lastOpened.home ? `/channel/${lastOpened.home}` : "/"}> to={lastOpened.home ? `/channel/${lastOpened.home}` : "/"}>
<ServerEntry home active={homeActive}> <ServerEntry home active={homeActive}>
<Swoosh /> <Swoosh />
{ isTouchscreenDevice ? <div
<Icon size={42} unread={homeUnread}> onContextMenu={attachContextMenu("Status")}
<img style={{ width: 32, height: 32 }} src={logoSVG} /> onClick={() =>
</Icon> : homeActive && history.push("/settings")
<div }>
onContextMenu={attachContextMenu("Status")} <UserHover user={client.user}>
onClick={() => <Icon size={42} unread={homeUnread}>
homeActive && openContextMenu("Status") <UserIcon
}> target={client.user}
<UserHover user={self}> size={32}
<Icon size={42} unread={homeUnread}> status
<UserIcon target={self} size={32} status /> hover
</Icon> />
</UserHover> </Icon>
</div> </UserHover>
} </div>
</ServerEntry> </ServerEntry>
</ConditionalLink> </ConditionalLink>
<LineDivider /> <LineDivider />
{servers.map((entry) => { {servers.map((entry) => {
const active = entry!._id === server?._id; const active = entry.server._id === server?._id;
const id = lastOpened[entry!._id]; const id = lastOpened[entry.server._id];
return ( return (
<ConditionalLink <ConditionalLink
key={entry.server._id}
active={active} active={active}
to={ to={`/server/${entry.server._id}${
`/server/${entry!._id}` + id ? `/channel/${id}` : ""
(id ? `/channel/${id}` : "") }`}>
}>
<ServerEntry <ServerEntry
active={active} active={active}
onContextMenu={attachContextMenu("Menu", { onContextMenu={attachContextMenu("Menu", {
server: entry!._id, server: entry.server._id,
})}> })}>
<Swoosh /> <Swoosh />
<Tooltip content={entry.name} placement="right"> <Tooltip
content={entry.server.name}
placement="right">
<Icon size={42} unread={entry.unread}> <Icon size={42} unread={entry.unread}>
<ServerIcon size={32} target={entry} /> <ServerIcon
size={32}
target={entry.server}
/>
</Icon> </Icon>
</Tooltip> </Tooltip>
</ServerEntry> </ServerEntry>
...@@ -283,11 +313,20 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) { ...@@ -283,11 +313,20 @@ export function ServerListSidebar({ unreads, lastOpened }: Props) {
}> }>
<Plus size={36} /> <Plus size={36} />
</IconButton> </IconButton>
{/*<IconButton
onClick={() =>
openScreen({
id: "special_input",
type: "create_server",
})
}>
<Compass size={36} />
</IconButton>*/}
<PaintCounter small /> <PaintCounter small />
</ServerList> </ServerList>
</ServersBase> </ServersBase>
); );
} });
export default connectState(ServerListSidebar, (state) => { export default connectState(ServerListSidebar, (state) => {
return { return {
......
import { observer } from "mobx-react-lite";
import { Redirect, useParams } from "react-router"; import { Redirect, useParams } from "react-router";
import { Channels } from "revolt.js/dist/api/objects"; import styled, { css } from "styled-components";
import styled from "styled-components";
import { attachContextMenu } from "preact-context-menu"; import { attachContextMenu } from "preact-context-menu";
import { useEffect } from "preact/hooks"; import { useEffect } from "preact/hooks";
import ConditionalLink from "../../../lib/ConditionalLink"; import ConditionalLink from "../../../lib/ConditionalLink";
import PaintCounter from "../../../lib/PaintCounter"; import PaintCounter from "../../../lib/PaintCounter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { dispatch } from "../../../redux"; import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector"; import { connectState } from "../../../redux/connector";
import { Unreads } from "../../../redux/reducers/unreads"; import { Unreads } from "../../../redux/reducers/unreads";
import { import { useClient } from "../../../context/revoltjs/RevoltClient";
useChannels,
useForceUpdate,
useServer,
} from "../../../context/revoltjs/hooks";
import CollapsibleSection from "../../common/CollapsibleSection"; import CollapsibleSection from "../../common/CollapsibleSection";
import ServerHeader from "../../common/ServerHeader"; import ServerHeader from "../../common/ServerHeader";
...@@ -37,10 +34,13 @@ const ServerBase = styled.div` ...@@ -37,10 +34,13 @@ const ServerBase = styled.div`
flex-shrink: 0; flex-shrink: 0;
flex-direction: column; flex-direction: column;
background: var(--secondary-background); background: var(--secondary-background);
border-start-start-radius: 8px; border-start-start-radius: 8px;
border-end-start-radius: 8px;
overflow: hidden; overflow: hidden;
${isTouchscreenDevice &&
css`
padding-bottom: 50px;
`}
`; `;
const ServerList = styled.div` const ServerList = styled.div`
...@@ -53,23 +53,17 @@ const ServerList = styled.div` ...@@ -53,23 +53,17 @@ const ServerList = styled.div`
} }
`; `;
function ServerSidebar(props: Props) { const ServerSidebar = observer((props: Props) => {
const client = useClient();
const { server: server_id, channel: channel_id } = const { server: server_id, channel: channel_id } =
useParams<{ server?: string; channel?: string }>(); useParams<{ server: string; channel?: string }>();
const ctx = useForceUpdate();
const server = useServer(server_id, ctx); const server = client.servers.get(server_id);
if (!server) return <Redirect to="/" />; if (!server) return <Redirect to="/" />;
const channels = ( const channel = channel_id ? client.channels.get(channel_id) : undefined;
useChannels(server.channels, ctx).filter(
(entry) => typeof entry !== "undefined",
) as Readonly<Channels.TextChannel | Channels.VoiceChannel>[]
).map((x) => mapChannelWithUnread(x, props.unreads));
const channel = channels.find((x) => x?._id === channel_id);
if (channel_id && !channel) return <Redirect to={`/server/${server_id}`} />; if (channel_id && !channel) return <Redirect to={`/server/${server_id}`} />;
if (channel) useUnreads({ ...props, channel }, ctx); if (channel) useUnreads({ ...props, channel });
useEffect(() => { useEffect(() => {
if (!channel_id) return; if (!channel_id) return;
...@@ -79,13 +73,13 @@ function ServerSidebar(props: Props) { ...@@ -79,13 +73,13 @@ function ServerSidebar(props: Props) {
parent: server_id!, parent: server_id!,
child: channel_id!, child: channel_id!,
}); });
}, [channel_id]); }, [channel_id, server_id]);
let uncategorised = new Set(server.channels); const uncategorised = new Set(server.channel_ids);
let elements = []; const elements = [];
function addChannel(id: string) { function addChannel(id: string) {
const entry = channels.find((x) => x._id === id); const entry = client.channels.get(id);
if (!entry) return; if (!entry) return;
const active = channel?._id === entry._id; const active = channel?._id === entry._id;
...@@ -98,7 +92,8 @@ function ServerSidebar(props: Props) { ...@@ -98,7 +92,8 @@ function ServerSidebar(props: Props) {
<ChannelButton <ChannelButton
channel={entry} channel={entry}
active={active} active={active}
alert={entry.unread} // ! FIXME: pull it out directly
alert={mapChannelWithUnread(entry, props.unreads).unread}
compact compact
/> />
</ConditionalLink> </ConditionalLink>
...@@ -106,9 +101,9 @@ function ServerSidebar(props: Props) { ...@@ -106,9 +101,9 @@ function ServerSidebar(props: Props) {
} }
if (server.categories) { if (server.categories) {
for (let category of server.categories) { for (const category of server.categories) {
let channels = []; const channels = [];
for (let id of category.channels) { for (const id of category.channels) {
uncategorised.delete(id); uncategorised.delete(id);
channels.push(addChannel(id)); channels.push(addChannel(id));
} }
...@@ -124,13 +119,13 @@ function ServerSidebar(props: Props) { ...@@ -124,13 +119,13 @@ function ServerSidebar(props: Props) {
} }
} }
for (let id of Array.from(uncategorised).reverse()) { for (const id of Array.from(uncategorised).reverse()) {
elements.unshift(addChannel(id)); elements.unshift(addChannel(id));
} }
return ( return (
<ServerBase> <ServerBase>
<ServerHeader server={server} ctx={ctx} /> <ServerHeader server={server} />
<ConnectionStatus /> <ConnectionStatus />
<ServerList <ServerList
onContextMenu={attachContextMenu("Menu", { onContextMenu={attachContextMenu("Menu", {
...@@ -141,7 +136,7 @@ function ServerSidebar(props: Props) { ...@@ -141,7 +136,7 @@ function ServerSidebar(props: Props) {
<PaintCounter small /> <PaintCounter small />
</ServerBase> </ServerBase>
); );
} });
export default connectState(ServerSidebar, (state) => { export default connectState(ServerSidebar, (state) => {
return { return {
......
import { Channel } from "revolt.js"; import { reaction } from "mobx";
import { Channel } from "revolt.js/dist/maps/Channels";
import { useLayoutEffect } from "preact/hooks"; import { useLayoutEffect } from "preact/hooks";
import { dispatch } from "../../../redux"; import { dispatch } from "../../../redux";
import { Unreads } from "../../../redux/reducers/unreads"; import { Unreads } from "../../../redux/reducers/unreads";
import { HookContext, useForceUpdate } from "../../../context/revoltjs/hooks";
type UnreadProps = { type UnreadProps = {
channel: Channel; channel: Channel;
unreads: Unreads; unreads: Unreads;
}; };
export function useUnreads( export function useUnreads({ channel, unreads }: UnreadProps) {
{ channel, unreads }: UnreadProps,
context?: HookContext,
) {
const ctx = useForceUpdate(context);
useLayoutEffect(() => { useLayoutEffect(() => {
function checkUnread(target?: Channel) { function checkUnread(target: Channel) {
if (!target) return; if (!target) return;
if (target._id !== channel._id) return; if (target._id !== channel._id) return;
if ( if (
...@@ -41,19 +35,16 @@ export function useUnreads( ...@@ -41,19 +35,16 @@ export function useUnreads(
message, message,
}); });
ctx.client.req( channel.ack(message);
"PUT",
`/channels/${channel._id}/ack/${message}` as "/channels/id/ack/id",
);
} }
} }
} }
checkUnread(channel); checkUnread(channel);
return reaction(
ctx.client.channels.addListener("mutation", checkUnread); () => channel.last_message,
return () => () => checkUnread(channel),
ctx.client.channels.removeListener("mutation", checkUnread); );
}, [channel, unreads]); }, [channel, unreads]);
} }
...@@ -63,12 +54,12 @@ export function mapChannelWithUnread(channel: Channel, unreads: Unreads) { ...@@ -63,12 +54,12 @@ export function mapChannelWithUnread(channel: Channel, unreads: Unreads) {
channel.channel_type === "DirectMessage" || channel.channel_type === "DirectMessage" ||
channel.channel_type === "Group" channel.channel_type === "Group"
) { ) {
last_message_id = channel.last_message?._id; last_message_id = (channel.last_message as { _id: string })?._id;
} else if (channel.channel_type === "TextChannel") { } else if (channel.channel_type === "TextChannel") {
last_message_id = channel.last_message; last_message_id = channel.last_message as string;
} else { } else {
return { return {
...channel, channel,
unread: undefined, unread: undefined,
alertCount: undefined, alertCount: undefined,
timestamp: channel._id, timestamp: channel._id,
...@@ -85,7 +76,7 @@ export function mapChannelWithUnread(channel: Channel, unreads: Unreads) { ...@@ -85,7 +76,7 @@ export function mapChannelWithUnread(channel: Channel, unreads: Unreads) {
unread = "mention"; unread = "mention";
} else if ( } else if (
u.last_id && u.last_id &&
last_message_id.localeCompare(u.last_id) > 0 (last_message_id as string).localeCompare(u.last_id) > 0
) { ) {
unread = "unread"; unread = "unread";
} }
...@@ -95,7 +86,7 @@ export function mapChannelWithUnread(channel: Channel, unreads: Unreads) { ...@@ -95,7 +86,7 @@ export function mapChannelWithUnread(channel: Channel, unreads: Unreads) {
} }
return { return {
...channel, channel,
timestamp: last_message_id ?? channel._id, timestamp: last_message_id ?? channel._id,
unread, unread,
alertCount, alertCount,
......
/* eslint-disable react-hooks/rules-of-hooks */
import { useRenderState } from "../../../lib/renderer/Singleton"; import { useRenderState } from "../../../lib/renderer/Singleton";
interface Props { interface Props {
...@@ -6,7 +7,7 @@ interface Props { ...@@ -6,7 +7,7 @@ interface Props {
export function ChannelDebugInfo({ id }: Props) { export function ChannelDebugInfo({ id }: Props) {
if (process.env.NODE_ENV !== "development") return null; if (process.env.NODE_ENV !== "development") return null;
let view = useRenderState(id); const view = useRenderState(id);
if (!view) return null; if (!view) return null;
return ( return (
......
...@@ -6,6 +6,7 @@ interface Props { ...@@ -6,6 +6,7 @@ interface Props {
readonly contrast?: boolean; readonly contrast?: boolean;
readonly plain?: boolean; readonly plain?: boolean;
readonly error?: boolean; readonly error?: boolean;
readonly gold?: boolean;
readonly iconbutton?: boolean; readonly iconbutton?: boolean;
} }
...@@ -125,4 +126,22 @@ export default styled.button<Props>` ...@@ -125,4 +126,22 @@ export default styled.button<Props>`
background: var(--error); background: var(--error);
} }
`} `}
${(props) =>
props.gold &&
css`
color: black;
font-weight: 600;
background: goldenrod;
&:hover {
filter: brightness(1.2);
background: goldenrod;
}
&:disabled {
cursor: not-allowed;
background: goldenrod;
}
`}
`; `;
...@@ -44,7 +44,7 @@ type Props = Omit< ...@@ -44,7 +44,7 @@ type Props = Omit<
}; };
export default function Category(props: Props) { export default function Category(props: Props) {
let { text, action, ...otherProps } = props; const { text, action, ...otherProps } = props;
return ( return (
<CategoryBase {...otherProps}> <CategoryBase {...otherProps}>
......
...@@ -55,7 +55,11 @@ const SwatchesBase = styled.div` ...@@ -55,7 +55,11 @@ const SwatchesBase = styled.div`
div { div {
width: 8px; width: 8px;
height: 68px; height: 68px;
background: linear-gradient(to right, var(--primary-background), transparent); background: linear-gradient(
to right,
var(--primary-background),
transparent
);
} }
} }
`; `;
...@@ -127,8 +131,10 @@ export default function ColourSwatches({ value, onChange }: Props) { ...@@ -127,8 +131,10 @@ export default function ColourSwatches({ value, onChange }: Props) {
<Palette size={32} /> <Palette size={32} />
</Swatch> </Swatch>
<div class="overlay"><div /></div> <div class="overlay">
<div />
</div>
<Rows> <Rows>
{presets.map((row, i) => ( {presets.map((row, i) => (
<div key={i}> <div key={i}>
...@@ -144,8 +150,6 @@ export default function ColourSwatches({ value, onChange }: Props) { ...@@ -144,8 +150,6 @@ export default function ColourSwatches({ value, onChange }: Props) {
</div> </div>
))} ))}
</Rows> </Rows>
</SwatchesBase> </SwatchesBase>
); );
} }
...@@ -5,7 +5,7 @@ export default styled.select` ...@@ -5,7 +5,7 @@ export default styled.select`
padding: 10px; padding: 10px;
cursor: pointer; cursor: pointer;
border-radius: var(--border-radius); border-radius: var(--border-radius);
font-family: inherit; font-family: inherit;
font-size: var(--text-size); font-size: var(--text-size);
color: var(--secondary-foreground); color: var(--secondary-foreground);
......