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 385 additions and 193 deletions
...@@ -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 { 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";
...@@ -11,17 +12,12 @@ import { useLayoutEffect, useState } from "preact/hooks"; ...@@ -11,17 +12,12 @@ import { useLayoutEffect, useState } from "preact/hooks";
import { useRenderState } from "../../../../lib/renderer/Singleton"; import { useRenderState } from "../../../../lib/renderer/Singleton";
import { useData } from "../../../../mobx/State";
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;
} }
...@@ -36,12 +32,24 @@ export const ReplyBase = styled.div<{ ...@@ -36,12 +32,24 @@ export const ReplyBase = styled.div<{
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;
...@@ -49,12 +57,13 @@ export const ReplyBase = styled.div<{ ...@@ -49,12 +57,13 @@ export const ReplyBase = styled.div<{
} }
.user { .user {
gap: 4px; gap: 6px;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
font-weight: 600; font-weight: 600;
overflow: visible; overflow: visible;
align-items: center; align-items: center;
padding: 2px 0;
span { span {
cursor: pointer; cursor: pointer;
...@@ -72,6 +81,7 @@ export const ReplyBase = styled.div<{ ...@@ -72,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;
...@@ -93,9 +103,9 @@ export const ReplyBase = styled.div<{ ...@@ -93,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 {
...@@ -124,13 +134,11 @@ export const ReplyBase = styled.div<{ ...@@ -124,13 +134,11 @@ export const ReplyBase = styled.div<{
`; `;
export const MessageReply = observer(({ 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);
...@@ -138,16 +146,13 @@ export const MessageReply = observer(({ index, channel, id }: Props) => { ...@@ -138,16 +146,13 @@ export const MessageReply = observer(({ 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>
...@@ -155,45 +160,52 @@ export const MessageReply = observer(({ index, channel, id }: Props) => { ...@@ -155,45 +160,52 @@ export const MessageReply = observer(({ index, channel, id }: Props) => {
); );
} }
const store = useData();
const user = store.users.get(message.author);
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={(
......
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,
File,
XCircle,
} from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { SYSTEM_USER_ID } from "revolt.js"; import { SYSTEM_USER_ID } from "revolt.js";
import styled from "styled-components"; import styled from "styled-components";
...@@ -14,11 +10,8 @@ import { StateUpdater, useEffect } from "preact/hooks"; ...@@ -14,11 +10,8 @@ import { StateUpdater, useEffect } from "preact/hooks";
import { internalSubscribe } from "../../../../lib/eventEmitter"; import { internalSubscribe } from "../../../../lib/eventEmitter";
import { useRenderState } from "../../../../lib/renderer/Singleton"; import { useRenderState } from "../../../../lib/renderer/Singleton";
import { useData } from "../../../../mobx/State";
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";
...@@ -34,26 +27,50 @@ interface Props { ...@@ -34,26 +27,50 @@ 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
...@@ -66,9 +83,9 @@ export default observer(({ channel, replies, setReplies }: Props) => { ...@@ -66,9 +83,9 @@ export default observer(({ 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;
...@@ -76,9 +93,6 @@ export default observer(({ channel, replies, setReplies }: Props) => { ...@@ -76,9 +93,6 @@ export default observer(({ channel, replies, setReplies }: Props) => {
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 store = useData();
const users = messages.map((x) => store.users.get(x.author));
return ( return (
<div> <div>
{replies.map((reply, index) => { {replies.map((reply, index) => {
...@@ -94,26 +108,37 @@ export default observer(({ channel, replies, setReplies }: Props) => { ...@@ -94,26 +108,37 @@ export default observer(({ channel, replies, setReplies }: Props) => {
</span> </span>
); );
const user = users[index];
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_id === SYSTEM_USER_ID ? (
<SystemMessage message={message} />
) : (
<Markdown
disallowBigEmoji
content={(
message.content as string
).replace(/\n/g, " ")}
/>
)} )}
{message.author === SYSTEM_USER_ID ? ( </div>
<SystemMessage message={message} />
) : (
<Markdown
disallowBigEmoji
content={(
message.content as string
).replace(/\n/g, " ")}
/>
)}
</ReplyBase> </ReplyBase>
<span class="actions"> <span class="actions">
<IconButton <IconButton
......
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { User } from "revolt.js"; 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 { TextReact } from "../../../../lib/i18n";
import { useData } from "../../../../mobx/State";
import { connectState } from "../../../../redux/connector";
import { TypingUser } from "../../../../redux/reducers/typing";
import {
AppContext,
useClient,
} from "../../../../context/revoltjs/RevoltClient";
import { useUsers } from "../../../../context/revoltjs/hooks";
import { Username } from "../../user/UserShort";
interface Props { interface Props {
typing?: TypingUser[]; channel: Channel;
} }
const Base = styled.div` const Base = styled.div`
...@@ -66,43 +52,44 @@ const Base = styled.div` ...@@ -66,43 +52,44 @@ const Base = styled.div`
} }
`; `;
export const TypingIndicator = observer(({ typing }: Props) => { export default observer(({ channel }: Props) => {
if (typing && typing.length > 0) { const users = channel.typing.filter(
const client = useClient(); (x) =>
const store = useData(); typeof x !== "undefined" &&
const users = typing x._id !== x.client.user!._id &&
.map((x) => store.users.get(x.id)!) x.relationship !== RelationshipStatus.Blocked,
.filter((x) => typeof x !== "undefined"); );
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 userlist = [...users].map((x) => <Username user={x} />); const userlist = [...users].map((x) => x!.username);
const user = userlist.pop(); const user = userlist.pop();
for (let i = 0; i < userlist.length - 1; i++) { /*for (let i = 0; i < userlist.length - 1; i++) {
userlist.splice(i * 2 + 1, 0, <>, </>); userlist.splice(i * 2 + 1, 0, ", ");
} }*/
text = ( text = (
<TextReact <Text
id="app.main.channel.typing.multiple" id="app.main.channel.typing.multiple"
fields={{ fields={{
user, user,
userlist, userlist: userlist.join(", "),
}} }}
/> />
); );
} else { } else {
text = ( text = (
<TextReact <Text
id="app.main.channel.typing.single" id="app.main.channel.typing.single"
fields={{ user: <Username user={users[0]} /> }} fields={{ user: users[0]!.username }}
/> />
); );
} }
...@@ -113,12 +100,9 @@ export const TypingIndicator = observer(({ typing }: Props) => { ...@@ -113,12 +100,9 @@ export const TypingIndicator = observer(({ 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>
...@@ -130,9 +114,3 @@ export const TypingIndicator = observer(({ typing }: Props) => { ...@@ -130,9 +114,3 @@ export const TypingIndicator = observer(({ 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 "../../../mobx"; import { User } from "revolt.js/dist/maps/Users";
import Checkbox, { CheckboxProps } from "../../ui/Checkbox"; import Checkbox, { CheckboxProps } from "../../ui/Checkbox";
......
import { Cog } from "@styled-icons/boxicons-solid"; import { Cog } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
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";
import { User } from "../../../mobx";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import Header from "../../ui/Header"; 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`
......
import { User } from "revolt.js/dist/maps/Users";
import styled from "styled-components"; import styled from "styled-components";
import { User } from "../../../mobx";
import { Children } from "../../../types/Preact"; import { Children } from "../../../types/Preact";
import Tooltip from "../Tooltip"; import Tooltip from "../Tooltip";
import { Username } from "./UserShort"; import { Username } from "./UserShort";
......
import { MicrophoneOff } from "@styled-icons/boxicons-regular"; import { MicrophoneOff } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; 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";
...@@ -8,7 +9,6 @@ import { useContext } from "preact/hooks"; ...@@ -8,7 +9,6 @@ import { useContext } from "preact/hooks";
import { ThemeContext } from "../../../context/Theme"; import { ThemeContext } from "../../../context/Theme";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { User } from "../../../mobx";
import IconBase, { IconBaseProps } from "../IconBase"; import IconBase, { IconBaseProps } from "../IconBase";
import fallback from "../assets/user.png"; import fallback from "../assets/user.png";
...@@ -22,10 +22,10 @@ interface Props extends IconBaseProps<User> { ...@@ -22,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"];
...@@ -52,19 +52,23 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>` ...@@ -52,19 +52,23 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
`; `;
export default observer( export default observer(
(props: Props & Omit<JSX.SVGAttributes<SVGSVGElement>, keyof Props>) => { (
props: Props &
Omit<
JSX.SVGAttributes<SVGSVGElement>,
keyof Props | "children" | "as"
>,
) => {
const client = useContext(AppContext); const client = useContext(AppContext);
const { const {
target, target,
attachment, attachment,
size, size,
voice,
status, status,
animate, animate,
mask, mask,
children, hover,
as,
...svgProps ...svgProps
} = props; } = props;
const iconURL = const iconURL =
...@@ -72,14 +76,14 @@ export default observer( ...@@ -72,14 +76,14 @@ export default observer(
target?.avatar ?? attachment, target?.avatar ?? attachment,
{ max_side: 256 }, { max_side: 256 },
animate, animate,
) ?? ) ?? (target ? target.defaultAvatarURL : fallback);
(target ? client.users.getDefaultAvatarURL(target._id) : fallback);
return ( return (
<IconBase <IconBase
{...svgProps} {...svgProps}
width={size} width={size}
height={size} height={size}
hover={hover}
aria-hidden="true" aria-hidden="true"
viewBox="0 0 32 32"> viewBox="0 0 32 32">
<foreignObject <foreignObject
...@@ -87,6 +91,7 @@ export default observer( ...@@ -87,6 +91,7 @@ export default observer(
y="0" y="0"
width="32" width="32"
height="32" height="32"
class="icon"
mask={mask ?? (status ? "url(#user)" : undefined)}> mask={mask ?? (status ? "url(#user)" : undefined)}>
{<img src={iconURL} draggable={false} loading="lazy" />} {<img src={iconURL} draggable={false} loading="lazy" />}
</foreignObject> </foreignObject>
......
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom"; 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 { User } from "../../../mobx"; import { useClient } from "../../../context/revoltjs/RevoltClient";
import {
useForceUpdate,
useMember,
useServer,
} from "../../../context/revoltjs/hooks";
import UserIcon from "./UserIcon"; import UserIcon from "./UserIcon";
...@@ -21,30 +16,35 @@ export const Username = observer( ...@@ -21,30 +16,35 @@ export const Username = observer(
let username = user?.username; let username = user?.username;
let color; let color;
/* // ! FIXME: this must be really bad for perf.
if (user) { if (user) {
let { server } = useParams<{ server?: string }>(); const { server } = useParams<{ server?: string }>();
if (server) { if (server) {
let ctx = useForceUpdate(); const client = useClient();
let member = useMember(`${server}${user._id}`, ctx); const member = client.members.getKey({
server,
user: user._id,
});
if (member) { if (member) {
if (member.nickname) { if (member.nickname) {
username = member.nickname; username = member.nickname;
} }
if (member.roles && member.roles.length > 0) { if (member.roles && member.roles.length > 0) {
let s = useServer(server, ctx); const srv = client.servers.get(member._id.server);
for (let role of member.roles) { if (srv?.roles) {
let c = s?.roles?.[role].colour; for (const role of member.roles) {
if (c) { const c = srv.roles[role].colour;
color = c; if (c) {
continue; color = c;
continue;
}
} }
} }
} }
} }
} }
} */ }
return ( return (
<span {...otherProps} style={{ color }}> <span {...otherProps} style={{ color }}>
......
import { observer } from "mobx-react-lite"; 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 { User } from "../../../mobx";
import Tooltip from "../Tooltip"; import Tooltip from "../Tooltip";
interface Props { interface Props {
...@@ -25,15 +25,15 @@ export default observer(({ user, tooltip }: Props) => { ...@@ -25,15 +25,15 @@ export default observer(({ 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" />;
} }
......
...@@ -118,6 +118,7 @@ ...@@ -118,6 +118,7 @@
> * { > * {
opacity: 0; opacity: 0;
pointer-events: none;
} }
&:global(.shown) { &:global(.shown) {
...@@ -128,6 +129,7 @@ ...@@ -128,6 +129,7 @@
> * { > * {
opacity: 1; opacity: 1;
pointer-events: unset;
} }
} }
} }
......
...@@ -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
......
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 { Search } from "@styled-icons/boxicons-regular"; import { Message, Group } from "@styled-icons/boxicons-solid";
import { Message, Group, Inbox } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; 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";
import ConditionalLink from "../../lib/ConditionalLink"; import ConditionalLink from "../../lib/ConditionalLink";
import { useData } from "../../mobx/State";
import { connectState } from "../../redux/connector"; import { connectState } from "../../redux/connector";
import { LastOpened } from "../../redux/reducers/last_opened"; import { LastOpened } from "../../redux/reducers/last_opened";
...@@ -55,8 +53,7 @@ interface Props { ...@@ -55,8 +53,7 @@ interface Props {
export const BottomNavigation = observer(({ lastOpened }: Props) => { export const BottomNavigation = observer(({ lastOpened }: Props) => {
const client = useClient(); const client = useClient();
const store = useData(); const user = client.users.get(client.user!._id);
const user = store.users.get(client.user!._id);
const history = useHistory(); const history = useHistory();
const path = useLocation().pathname; const path = useLocation().pathname;
......