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 329 additions and 230 deletions
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
display: grid; display: grid;
grid-auto-flow: row dense; grid-auto-flow: row dense;
grid-auto-columns: min(100%, var(--attachment-max-width)); grid-auto-columns: min(100%, var(--attachment-max-width));
margin: .125rem 0 .125rem; margin: 0.125rem 0 0.125rem;
width: max-content; width: max-content;
max-width: 100%; max-width: 100%;
&[data-spoiler="true"] { &[data-spoiler="true"] {
filter: blur(30px); filter: blur(30px);
pointer-events: none; pointer-events: none;
...@@ -50,18 +50,18 @@ ...@@ -50,18 +50,18 @@
overflow-y: auto; overflow-y: auto;
border-radius: 0 !important; border-radius: 0 !important;
background: var(--secondary-header); background: var(--secondary-header);
pre { pre {
margin: 0; margin: 0;
} }
pre code { pre code {
font-family: var(--monoscape-font), sans-serif; font-family: var(--monospace-font), sans-serif;
} }
&[data-loading="true"] { &[data-loading="true"] {
display: flex; display: flex;
> * { > * {
flex-grow: 1; flex-grow: 1;
} }
...@@ -84,6 +84,8 @@ ...@@ -84,6 +84,8 @@
} }
} }
.container, .attachment, .image { .container,
.attachment,
.image {
border-radius: var(--border-radius); border-radius: var(--border-radius);
} }
import { Attachment as AttachmentRJS } from "revolt.js/dist/api/objects"; import { Attachment as AttachmentI } from "revolt-api/types/Autumn";
import styles from "./Attachment.module.scss"; import styles from "./Attachment.module.scss";
import classNames from "classnames"; import classNames from "classnames";
...@@ -13,7 +13,7 @@ import Spoiler from "./Spoiler"; ...@@ -13,7 +13,7 @@ import Spoiler from "./Spoiler";
import TextFile from "./TextFile"; import TextFile from "./TextFile";
interface Props { interface Props {
attachment: AttachmentRJS; attachment: AttachmentI;
hasContent: boolean; hasContent: boolean;
} }
......
...@@ -5,7 +5,7 @@ import { ...@@ -5,7 +5,7 @@ import {
Headphone, Headphone,
Video, Video,
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { Attachment } from "revolt.js/dist/api/objects"; import { Attachment } from "revolt-api/types/Autumn";
import styles from "./AttachmentActions.module.scss"; import styles from "./AttachmentActions.module.scss";
import classNames from "classnames"; import classNames from "classnames";
......
import { Reply } from "@styled-icons/boxicons-regular";
import { File } from "@styled-icons/boxicons-solid"; import { File } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users";
import { SYSTEM_USER_ID } from "revolt.js"; import { SYSTEM_USER_ID } from "revolt.js";
import { Users } from "revolt.js/dist/api/objects"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
...@@ -10,15 +12,12 @@ import { useLayoutEffect, useState } from "preact/hooks"; ...@@ -10,15 +12,12 @@ import { useLayoutEffect, useState } from "preact/hooks";
import { useRenderState } from "../../../../lib/renderer/Singleton"; import { useRenderState } from "../../../../lib/renderer/Singleton";
import { useForceUpdate, useUser } from "../../../../context/revoltjs/hooks";
import { mapMessage, MessageObject } from "../../../../context/revoltjs/util";
import Markdown from "../../../markdown/Markdown"; import Markdown from "../../../markdown/Markdown";
import UserShort from "../../user/UserShort"; import UserShort from "../../user/UserShort";
import { SystemMessage } from "../SystemMessage"; import { SystemMessage } from "../SystemMessage";
interface Props { interface Props {
channel: string; channel: Channel;
index: number; index: number;
id: string; id: string;
} }
...@@ -29,15 +28,28 @@ export const ReplyBase = styled.div<{ ...@@ -29,15 +28,28 @@ export const ReplyBase = styled.div<{
preview?: boolean; preview?: boolean;
}>` }>`
gap: 4px; gap: 4px;
min-width: 0;
display: flex; display: flex;
margin-inline-start: 30px; margin-inline-start: 30px;
margin-inline-end: 12px; margin-inline-end: 12px;
margin-bottom: 4px; /*margin-bottom: 4px;*/
font-size: 0.8em; font-size: 0.8em;
user-select: none; user-select: none;
align-items: center; align-items: center;
color: var(--secondary-foreground); color: var(--secondary-foreground);
&::before {
content: "";
height: 10px;
width: 28px;
margin-inline-end: 2px;
align-self: flex-end;
display: flex;
border-top: 2.2px solid var(--tertiary-foreground);
border-inline-start: 2.2px solid var(--tertiary-foreground);
border-start-start-radius: 6px;
}
* { * {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
...@@ -45,11 +57,13 @@ export const ReplyBase = styled.div<{ ...@@ -45,11 +57,13 @@ export const ReplyBase = styled.div<{
} }
.user { .user {
gap: 6px;
display: flex; display: flex;
gap: 4px;
flex-shrink: 0; flex-shrink: 0;
font-weight: 600; font-weight: 600;
overflow: visible;
align-items: center; align-items: center;
padding: 2px 0;
span { span {
cursor: pointer; cursor: pointer;
...@@ -67,6 +81,7 @@ export const ReplyBase = styled.div<{ ...@@ -67,6 +81,7 @@ export const ReplyBase = styled.div<{
} }
.content { .content {
padding: 2px 0;
gap: 4px; gap: 4px;
display: flex; display: flex;
cursor: pointer; cursor: pointer;
...@@ -88,9 +103,9 @@ export const ReplyBase = styled.div<{ ...@@ -88,9 +103,9 @@ export const ReplyBase = styled.div<{
pointer-events: none; pointer-events: none;
} }
> span { /*> span > p {
display: flex; display: flex;
} }*/
} }
> svg:first-child { > svg:first-child {
...@@ -118,14 +133,12 @@ export const ReplyBase = styled.div<{ ...@@ -118,14 +133,12 @@ export const ReplyBase = styled.div<{
`} `}
`; `;
export function MessageReply({ index, channel, id }: Props) { export const MessageReply = observer(({ index, channel, id }: Props) => {
const ctx = useForceUpdate(); const view = useRenderState(channel._id);
const view = useRenderState(channel);
if (view?.type !== "RENDER") return null; if (view?.type !== "RENDER") return null;
const [message, setMessage] = useState<MessageObject | undefined>( const [message, setMessage] = useState<Message | undefined>(undefined);
undefined,
);
useLayoutEffect(() => { useLayoutEffect(() => {
// ! FIXME: We should do this through the message renderer, so it can fetch it from cache if applicable. // ! FIXME: We should do this through the message renderer, so it can fetch it from cache if applicable.
const m = view.messages.find((x) => x._id === id); const m = view.messages.find((x) => x._id === id);
...@@ -133,16 +146,13 @@ export function MessageReply({ index, channel, id }: Props) { ...@@ -133,16 +146,13 @@ export function MessageReply({ index, channel, id }: Props) {
if (m) { if (m) {
setMessage(m); setMessage(m);
} else { } else {
ctx.client.channels channel.fetchMessage(id).then(setMessage);
.fetchMessage(channel, id)
.then((m) => setMessage(mapMessage(m)));
} }
}, [view.messages]); }, [id, channel, view.messages]);
if (!message) { if (!message) {
return ( return (
<ReplyBase head={index === 0} fail> <ReplyBase head={index === 0} fail>
<Reply size={16} />
<span> <span>
<Text id="app.main.channel.misc.failed_load" /> <Text id="app.main.channel.misc.failed_load" />
</span> </span>
...@@ -150,44 +160,52 @@ export function MessageReply({ index, channel, id }: Props) { ...@@ -150,44 +160,52 @@ export function MessageReply({ index, channel, id }: Props) {
); );
} }
const user = useUser(message.author, ctx);
const history = useHistory(); const history = useHistory();
return ( return (
<ReplyBase head={index === 0}> <ReplyBase head={index === 0}>
<Reply size={16} /> {message.author?.relationship === RelationshipStatus.Blocked ? (
{user?.relationship === Users.Relationship.Blocked ? ( <Text id="app.main.channel.misc.blocked_user" />
<>
<Text id="app.main.channel.misc.blocked_user" />
</>
) : ( ) : (
<> <>
{message.author === SYSTEM_USER_ID ? ( {message.author_id === SYSTEM_USER_ID ? (
<SystemMessage message={message} hideInfo /> <SystemMessage message={message} hideInfo />
) : ( ) : (
<> <>
<div className="user"> <div className="user">
<UserShort user={user} size={16} /> <UserShort user={message.author} size={16} />
</div> </div>
<div <div
className="content" className="content"
onClick={() => { onClick={() => {
const obj = const channel = message.channel!;
ctx.client.channels.get(channel); if (
if (obj?.channel_type === "TextChannel") { channel.channel_type === "TextChannel"
) {
console.log(
`/server/${channel.server_id}/channel/${channel._id}/${message._id}`,
);
history.push( history.push(
`/server/${obj.server}/channel/${obj._id}/${message._id}`, `/server/${channel.server_id}/channel/${channel._id}/${message._id}`,
); );
} else { } else {
history.push( history.push(
`/channel/${channel}/${message._id}`, `/channel/${channel._id}/${message._id}`,
); );
} }
}}> }}>
{message.attachments && {message.attachments && (
message.attachments.length > 0 && ( <>
<File size={16} /> <File size={16} />
)} <em>
{message.attachments.length > 1 ? (
<Text id="app.main.channel.misc.sent_multiple_files" />
) : (
<Text id="app.main.channel.misc.sent_file" />
)}
</em>
</>
)}
<Markdown <Markdown
disallowBigEmoji disallowBigEmoji
content={( content={(
...@@ -201,4 +219,4 @@ export function MessageReply({ index, channel, id }: Props) { ...@@ -201,4 +219,4 @@ export function MessageReply({ index, channel, id }: Props) {
)} )}
</ReplyBase> </ReplyBase>
); );
} });
import axios from "axios"; import axios from "axios";
import { Attachment } from "revolt.js/dist/api/objects"; import { Attachment } from "revolt-api/types/Autumn";
import styles from "./Attachment.module.scss"; import styles from "./Attachment.module.scss";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
...@@ -60,7 +60,7 @@ export default function TextFile({ attachment }: Props) { ...@@ -60,7 +60,7 @@ export default function TextFile({ attachment }: Props) {
setLoading(false); setLoading(false);
}); });
} }
}, [content, loading, status]); }, [content, loading, status, attachment._id, attachment.size, url]);
return ( return (
<div <div
......
/* eslint-disable react-hooks/rules-of-hooks */
import { XCircle, Plus, Share, X, File } from "@styled-icons/boxicons-regular"; import { XCircle, Plus, Share, X, File } from "@styled-icons/boxicons-regular";
import styled from "styled-components"; import styled from "styled-components";
...@@ -186,7 +187,9 @@ export default function FilePreview({ state, addFile, removeFile }: Props) { ...@@ -186,7 +187,9 @@ export default function FilePreview({ state, addFile, removeFile }: Props) {
<Container> <Container>
<Carousel> <Carousel>
{state.files.map((file, index) => ( {state.files.map((file, index) => (
<> // @ts-expect-error brokey
// eslint-disable-next-line react/jsx-no-undef
<Fragment key={file.name}>
{index === CAN_UPLOAD_AT_ONCE && <Divider />} {index === CAN_UPLOAD_AT_ONCE && <Divider />}
<FileEntry <FileEntry
index={index} index={index}
...@@ -198,7 +201,7 @@ export default function FilePreview({ state, addFile, removeFile }: Props) { ...@@ -198,7 +201,7 @@ export default function FilePreview({ state, addFile, removeFile }: Props) {
: undefined : undefined
} }
/> />
</> </Fragment>
))} ))}
{state.type === "attached" && ( {state.type === "attached" && (
<EmptyEntry onClick={addFile}> <EmptyEntry onClick={addFile}>
......
import { import { At, Reply as ReplyIcon } from "@styled-icons/boxicons-regular";
At, import { File, XCircle } from "@styled-icons/boxicons-solid";
Reply as ReplyIcon, import { observer } from "mobx-react-lite";
File,
XCircle,
} from "@styled-icons/boxicons-regular";
import { SYSTEM_USER_ID } from "revolt.js"; import { SYSTEM_USER_ID } from "revolt.js";
import styled from "styled-components"; import styled from "styled-components";
...@@ -15,8 +12,6 @@ import { useRenderState } from "../../../../lib/renderer/Singleton"; ...@@ -15,8 +12,6 @@ import { useRenderState } from "../../../../lib/renderer/Singleton";
import { Reply } from "../../../../redux/reducers/queue"; import { Reply } from "../../../../redux/reducers/queue";
import { useUsers } from "../../../../context/revoltjs/hooks";
import IconButton from "../../../ui/IconButton"; import IconButton from "../../../ui/IconButton";
import Markdown from "../../../markdown/Markdown"; import Markdown from "../../../markdown/Markdown";
...@@ -32,31 +27,55 @@ interface Props { ...@@ -32,31 +27,55 @@ interface Props {
const Base = styled.div` const Base = styled.div`
display: flex; display: flex;
padding: 0 22px; height: 30px;
padding: 0 12px;
user-select: none; user-select: none;
align-items: center; align-items: center;
background: var(--message-box); background: var(--message-box);
div { > div {
flex-grow: 1; flex-grow: 1;
} margin-bottom: 0;
.actions { &::before {
gap: 12px; display: none;
display: flex; }
} }
.toggle { .toggle {
gap: 4px; gap: 4px;
display: flex; display: flex;
font-size: 0.7em; font-size: 12px;
align-items: center; align-items: center;
font-weight: 600;
} }
.username {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
}
.message {
display: flex;
}
.actions {
gap: 12px;
display: flex;
}
/*@media (pointer: coarse) { //FIXME: Make action buttons bigger on pointer coarse
.actions > svg {
height: 25px;
}
}*/
`; `;
// ! FIXME: Move to global config // ! FIXME: Move to global config
const MAX_REPLIES = 5; const MAX_REPLIES = 5;
export default function ReplyBar({ channel, replies, setReplies }: Props) { export default observer(({ channel, replies, setReplies }: Props) => {
useEffect(() => { useEffect(() => {
return internalSubscribe( return internalSubscribe(
"ReplyBar", "ReplyBar",
...@@ -64,16 +83,15 @@ export default function ReplyBar({ channel, replies, setReplies }: Props) { ...@@ -64,16 +83,15 @@ export default function ReplyBar({ channel, replies, setReplies }: Props) {
(id) => (id) =>
replies.length < MAX_REPLIES && replies.length < MAX_REPLIES &&
!replies.find((x) => x.id === id) && !replies.find((x) => x.id === id) &&
setReplies([...replies, { id, mention: false }]), setReplies([...replies, { id: id as string, mention: false }]),
); );
}, [replies]); }, [replies, setReplies]);
const view = useRenderState(channel); const view = useRenderState(channel);
if (view?.type !== "RENDER") return null; if (view?.type !== "RENDER") return null;
const ids = replies.map((x) => x.id); const ids = replies.map((x) => x.id);
const messages = view.messages.filter((x) => ids.includes(x._id)); const messages = view.messages.filter((x) => ids.includes(x._id));
const users = useUsers(messages.map((x) => x.author));
return ( return (
<div> <div>
...@@ -90,28 +108,37 @@ export default function ReplyBar({ channel, replies, setReplies }: Props) { ...@@ -90,28 +108,37 @@ export default function ReplyBar({ channel, replies, setReplies }: Props) {
</span> </span>
); );
const user = users.find((x) => message!.author === x?._id);
if (!user) return;
return ( return (
<Base key={reply.id}> <Base key={reply.id}>
<ReplyBase preview> <ReplyBase preview>
<ReplyIcon size={22} /> <ReplyIcon size={22} />
<UserShort user={user} size={16} /> <div class="username">
{message.attachments && <UserShort user={message.author} size={16} />
message.attachments.length > 0 && ( </div>
<File size={16} /> <div class="message">
{message.attachments && (
<>
<File size={16} />
<em>
{message.attachments.length > 1 ? (
<Text id="app.main.channel.misc.sent_multiple_files" />
) : (
<Text id="app.main.channel.misc.sent_file" />
)}
</em>
</>
)} )}
{message.author === SYSTEM_USER_ID ? ( {message.author_id === SYSTEM_USER_ID ? (
<SystemMessage message={message} /> <SystemMessage message={message} />
) : ( ) : (
<Markdown <Markdown
disallowBigEmoji disallowBigEmoji
content={( content={(
message.content as string message.content as string
).replace(/\n/g, " ")} ).replace(/\n/g, " ")}
/> />
)} )}
</div>
</ReplyBase> </ReplyBase>
<span class="actions"> <span class="actions">
<IconButton <IconButton
...@@ -143,4 +170,4 @@ export default function ReplyBar({ channel, replies, setReplies }: Props) { ...@@ -143,4 +170,4 @@ export default function ReplyBar({ channel, replies, setReplies }: Props) {
})} })}
</div> </div>
); );
} });
import { User } from "revolt.js"; import { observer } from "mobx-react-lite";
import { RelationshipStatus } from "revolt-api/types/Users";
import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components"; import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext } from "preact/hooks";
import { connectState } from "../../../../redux/connector";
import { TypingUser } from "../../../../redux/reducers/typing";
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
import { useUsers } from "../../../../context/revoltjs/hooks";
interface Props { interface Props {
typing?: TypingUser[]; channel: Channel;
} }
const Base = styled.div` const Base = styled.div`
...@@ -57,28 +52,36 @@ const Base = styled.div` ...@@ -57,28 +52,36 @@ const Base = styled.div`
} }
`; `;
export function TypingIndicator({ typing }: Props) { export default observer(({ channel }: Props) => {
if (typing && typing.length > 0) { const users = channel.typing.filter(
const client = useContext(AppContext); (x) =>
const users = useUsers(typing.map((x) => x.id)).filter( typeof x !== "undefined" &&
(x) => typeof x !== "undefined", x._id !== x.client.user!._id &&
) as User[]; x.relationship !== RelationshipStatus.Blocked,
);
if (users.length > 0) {
users.sort((a, b) => users.sort((a, b) =>
a._id.toUpperCase().localeCompare(b._id.toUpperCase()), a!._id.toUpperCase().localeCompare(b!._id.toUpperCase()),
); );
let text; let text;
if (users.length >= 5) { if (users.length >= 5) {
text = <Text id="app.main.channel.typing.several" />; text = <Text id="app.main.channel.typing.several" />;
} else if (users.length > 1) { } else if (users.length > 1) {
const usersCopy = [...users]; const userlist = [...users].map((x) => x!.username);
const user = userlist.pop();
/*for (let i = 0; i < userlist.length - 1; i++) {
userlist.splice(i * 2 + 1, 0, ", ");
}*/
text = ( text = (
<Text <Text
id="app.main.channel.typing.multiple" id="app.main.channel.typing.multiple"
fields={{ fields={{
user: usersCopy.pop()?.username, user,
userlist: usersCopy.map((x) => x.username).join(", "), userlist: userlist.join(", "),
}} }}
/> />
); );
...@@ -86,7 +89,7 @@ export function TypingIndicator({ typing }: Props) { ...@@ -86,7 +89,7 @@ export function TypingIndicator({ typing }: Props) {
text = ( text = (
<Text <Text
id="app.main.channel.typing.single" id="app.main.channel.typing.single"
fields={{ user: users[0].username }} fields={{ user: users[0]!.username }}
/> />
); );
} }
...@@ -97,12 +100,9 @@ export function TypingIndicator({ typing }: Props) { ...@@ -97,12 +100,9 @@ export function TypingIndicator({ typing }: Props) {
<div className="avatars"> <div className="avatars">
{users.map((user) => ( {users.map((user) => (
<img <img
key={user!._id}
loading="eager" loading="eager"
src={client.users.getAvatarURL( src={user!.generateAvatarURL({ max_side: 256 })}
user._id,
{ max_side: 256 },
true,
)}
/> />
))} ))}
</div> </div>
...@@ -113,10 +113,4 @@ export function TypingIndicator({ typing }: Props) { ...@@ -113,10 +113,4 @@ export function TypingIndicator({ typing }: Props) {
} }
return null; return null;
}
export default connectState<{ id: string }>(TypingIndicator, (state, props) => {
return {
typing: state.typing && state.typing[props.id],
};
}); });
import { Embed as EmbedRJS } from "revolt.js/dist/api/objects"; import { Embed as EmbedI } from "revolt-api/types/January";
import styles from "./Embed.module.scss"; import styles from "./Embed.module.scss";
import classNames from "classnames"; import classNames from "classnames";
...@@ -11,7 +11,7 @@ import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/Me ...@@ -11,7 +11,7 @@ import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/Me
import EmbedMedia from "./EmbedMedia"; import EmbedMedia from "./EmbedMedia";
interface Props { interface Props {
embed: EmbedRJS; embed: EmbedI;
} }
const MAX_EMBED_WIDTH = 480; const MAX_EMBED_WIDTH = 480;
......
import { Embed } from "revolt.js/dist/api/objects"; /* eslint-disable react-hooks/rules-of-hooks */
import { Embed } from "revolt-api/types/January";
import styles from "./Embed.module.scss"; import styles from "./Embed.module.scss";
......
import { LinkExternal } from "@styled-icons/boxicons-regular"; import { LinkExternal } from "@styled-icons/boxicons-regular";
import { EmbedImage } from "revolt.js/dist/api/objects"; import { EmbedImage } from "revolt-api/types/January";
import styles from "./Embed.module.scss"; import styles from "./Embed.module.scss";
......
import { User } from "revolt.js"; import { User } from "revolt.js/dist/maps/Users";
import Checkbox, { CheckboxProps } from "../../ui/Checkbox"; import Checkbox, { CheckboxProps } from "../../ui/Checkbox";
import UserIcon from "./UserIcon"; import UserIcon from "./UserIcon";
import { Username } from "./UserShort";
type UserProps = Omit<CheckboxProps, "children"> & { user: User }; type UserProps = Omit<CheckboxProps, "children"> & { user: User };
...@@ -10,7 +11,7 @@ export default function UserCheckbox({ user, ...props }: UserProps) { ...@@ -10,7 +11,7 @@ export default function UserCheckbox({ user, ...props }: UserProps) {
return ( return (
<Checkbox {...props}> <Checkbox {...props}>
<UserIcon target={user} size={32} /> <UserIcon target={user} size={32} />
{user.username} <Username user={user} />
</Checkbox> </Checkbox>
); );
} }
import { Cog } from "@styled-icons/boxicons-solid"; import { Cog } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { User } from "revolt.js"; import { User } from "revolt.js/dist/maps/Users";
import styled from "styled-components"; import styled from "styled-components";
import { openContextMenu } from "preact-context-menu"; import { openContextMenu } from "preact-context-menu";
import { Text } from "preact-i18n"; import { Text, Localizer } from "preact-i18n";
import { Localizer } from "preact-i18n";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
...@@ -15,7 +15,6 @@ import Header from "../../ui/Header"; ...@@ -15,7 +15,6 @@ import Header from "../../ui/Header";
import IconButton from "../../ui/IconButton"; import IconButton from "../../ui/IconButton";
import Tooltip from "../Tooltip"; import Tooltip from "../Tooltip";
import UserIcon from "./UserIcon";
import UserStatus from "./UserStatus"; import UserStatus from "./UserStatus";
const HeaderBase = styled.div` const HeaderBase = styled.div`
...@@ -49,7 +48,7 @@ interface Props { ...@@ -49,7 +48,7 @@ interface Props {
user: User; user: User;
} }
export default function UserHeader({ user }: Props) { export default observer(({ user }: Props) => {
const { writeClipboard } = useIntermediate(); const { writeClipboard } = useIntermediate();
return ( return (
...@@ -81,4 +80,4 @@ export default function UserHeader({ user }: Props) { ...@@ -81,4 +80,4 @@ export default function UserHeader({ user }: Props) {
)} )}
</Header> </Header>
); );
} });
import { InfoCircle } from "@styled-icons/boxicons-regular"; import { User } from "revolt.js/dist/maps/Users";
import { User } from "revolt.js";
import styled from "styled-components"; import styled from "styled-components";
import { Children } from "../../../types/Preact"; import { Children } from "../../../types/Preact";
......
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";
...@@ -10,7 +11,7 @@ interface Props { ...@@ -10,7 +11,7 @@ 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) {
...@@ -24,15 +25,15 @@ export default function UserStatus({ user, tooltip }: Props) { ...@@ -24,15 +25,15 @@ export default function UserStatus({ user, tooltip }: Props) {
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" />;
} }
...@@ -40,4 +41,4 @@ export default function UserStatus({ user, tooltip }: Props) { ...@@ -40,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";
...@@ -67,6 +68,8 @@ export const md: MarkdownIt = MarkdownIt({ ...@@ -67,6 +68,8 @@ export const md: MarkdownIt = MarkdownIt({
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
...@@ -92,10 +95,9 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { ...@@ -92,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.
const 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) {
...@@ -103,20 +105,17 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { ...@@ -103,20 +105,17 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
} }
return sub; return sub;
}, })
).replace( .replace(RE_CHANNELS, (sub: string, ...args: unknown[]) => {
RE_CHANNELS, const id = args[0] as string,
(sub: string, ...args: any[]) => {
const id = args[0],
channel = client.channels.get(id); channel = client.channels.get(id);
if (channel?.channel_type === 'TextChannel') { if (channel?.channel_type === "TextChannel") {
return `[#${channel.name}](/server/${channel.server}/channel/${id})`; return `[#${channel.name}](/server/${channel.server_id}/channel/${id})`;
} }
return sub; return sub;
}, });
);
const useLargeEmojis = disallowBigEmoji const useLargeEmojis = disallowBigEmoji
? false ? false
......