mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-24 19:32:34 +01:00
markdown
This commit is contained in:
parent
e9f5994260
commit
1ab749a762
13
package.json
13
package.json
@ -7,7 +7,7 @@
|
|||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@fontsource/roboto": "^4.5.8",
|
"@fontsource/roboto": "^4.5.8",
|
||||||
"@fontsource/source-code-pro": "^4.5.14",
|
"@fontsource/roboto-mono": "^5.0.8",
|
||||||
"@hcaptcha/react-hcaptcha": "^1.8.1",
|
"@hcaptcha/react-hcaptcha": "^1.8.1",
|
||||||
"@mattjennings/react-modal-stack": "^1.0.4",
|
"@mattjennings/react-modal-stack": "^1.0.4",
|
||||||
"@mdi/js": "^7.2.96",
|
"@mdi/js": "^7.2.96",
|
||||||
@ -34,6 +34,7 @@
|
|||||||
"missing-native-js-functions": "^1.4.3",
|
"missing-native-js-functions": "^1.4.3",
|
||||||
"mobx": "^6.10.2",
|
"mobx": "^6.10.2",
|
||||||
"mobx-react-lite": "^3.4.3",
|
"mobx-react-lite": "^3.4.3",
|
||||||
|
"prismjs": "^1.29.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-advanced-cropper": "^0.18.0",
|
"react-advanced-cropper": "^0.18.0",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
@ -42,14 +43,21 @@
|
|||||||
"react-hook-form": "^7.46.1",
|
"react-hook-form": "^7.46.1",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-loading-skeleton": "^3.3.1",
|
"react-loading-skeleton": "^3.3.1",
|
||||||
|
"react-markdown": "^8.0.7",
|
||||||
"react-router-dom": "^6.16.0",
|
"react-router-dom": "^6.16.0",
|
||||||
"react-secure-storage": "^1.3.0",
|
"react-secure-storage": "^1.3.0",
|
||||||
"react-select-search": "^4.1.6",
|
"react-select-search": "^4.1.6",
|
||||||
"react-spinners": "^0.13.8",
|
"react-spinners": "^0.13.8",
|
||||||
"react-string-replace": "^1.1.1",
|
"react-string-replace": "^1.1.1",
|
||||||
"react-use-error-boundary": "^3.0.0",
|
"react-use-error-boundary": "^3.0.0",
|
||||||
|
"rehype-prism": "^2.2.2",
|
||||||
|
"rehype-react": "^8.0.0",
|
||||||
|
"remark-gfm": "^4.0.0",
|
||||||
|
"remark-parse": "^11.0.0",
|
||||||
|
"remark-rehype": "^11.0.0",
|
||||||
"reoverlay": "^1.0.3",
|
"reoverlay": "^1.0.3",
|
||||||
"styled-components": "^5.3.10"
|
"styled-components": "^5.3.10",
|
||||||
|
"unified": "^11.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@craco/craco": "^7.1.0",
|
"@craco/craco": "^7.1.0",
|
||||||
@ -57,6 +65,7 @@
|
|||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/loadable__component": "^5.13.5",
|
"@types/loadable__component": "^5.13.5",
|
||||||
"@types/node": "^16.18.50",
|
"@types/node": "^16.18.50",
|
||||||
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.15",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/styled-components": "^5.1.27",
|
"@types/styled-components": "^5.1.27",
|
||||||
|
1146
pnpm-lock.yaml
1146
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
17
src/components/markdown/Markdown.tsx
Normal file
17
src/components/markdown/Markdown.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Suspense, lazy } from "react";
|
||||||
|
|
||||||
|
const Renderer = lazy(() => import("./RemarkRenderer"));
|
||||||
|
|
||||||
|
export interface MarkdownProps {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Markdown(props: MarkdownProps) {
|
||||||
|
if (!props.content) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={props.content}>
|
||||||
|
<Renderer {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
186
src/components/markdown/RemarkRenderer.tsx
Normal file
186
src/components/markdown/RemarkRenderer.tsx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
// adapted from Revite
|
||||||
|
// https://github.com/revoltchat/revite/blob/fe63c6633f32b54aa1989cb34627e72bb3377efd/src/components/markdown/RemarkRenderer.tsx
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import * as prod from "react/jsx-runtime";
|
||||||
|
import rehypePrism from "rehype-prism";
|
||||||
|
import rehypeReact from "rehype-react";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import remarkParse from "remark-parse";
|
||||||
|
import remarkRehype from "remark-rehype";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { unified } from "unified";
|
||||||
|
import Link from "../Link";
|
||||||
|
import { MarkdownProps } from "./Markdown";
|
||||||
|
import RenderCodeblock from "./plugins/Codeblock";
|
||||||
|
import "./prism";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Null element
|
||||||
|
*/
|
||||||
|
const Null: React.FC = () => null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Markdown components
|
||||||
|
*/
|
||||||
|
const components = {
|
||||||
|
// emoji: RenderEmoji,
|
||||||
|
// mention: RenderMention,
|
||||||
|
// spoiler: RenderSpoiler,
|
||||||
|
// channel: RenderChannel,
|
||||||
|
a: Link,
|
||||||
|
p: styled.p`
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
> code {
|
||||||
|
padding: 1px 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
h1: styled.h1`
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
h2: styled.h2`
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
h3: styled.h3`
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
h4: styled.h4`
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
h5: styled.h5`
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
h6: styled.h6`
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
pre: RenderCodeblock,
|
||||||
|
code: styled.code`
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--background-secondary);
|
||||||
|
|
||||||
|
font-size: 90%;
|
||||||
|
font-family: var(--font-family-code);
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
box-decoration-break: clone;
|
||||||
|
`,
|
||||||
|
table: styled.table`
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid var(--background-teritary);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
ul: styled.ul`
|
||||||
|
list-style-position: inside;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
ol: styled.ol`
|
||||||
|
list-style-position: inside;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.2em 0;
|
||||||
|
`,
|
||||||
|
|
||||||
|
blockquote: styled.blockquote`
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 2px 0;
|
||||||
|
background: red;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-inline-start: 4px solid var(--background-teritary);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
// Block image elements
|
||||||
|
img: Null,
|
||||||
|
// Catch literally everything else just in case
|
||||||
|
video: Null,
|
||||||
|
figure: Null,
|
||||||
|
picture: Null,
|
||||||
|
source: Null,
|
||||||
|
audio: Null,
|
||||||
|
script: Null,
|
||||||
|
style: Null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = unified()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkGfm)
|
||||||
|
.use(remarkRehype)
|
||||||
|
.use(rehypePrism)
|
||||||
|
// @ts-expect-error typescript doesn't like this
|
||||||
|
.use(rehypeReact, { Fragment: prod.Fragment, jsx: prod.jsx, jsxs: prod.jsxs, components });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching execessive recursion of blockquotes and lists
|
||||||
|
*/
|
||||||
|
const RE_RECURSIVE = /(^(?:[>*+-][^\S\r\n]*){5})(?:[>*+-][^\S\r\n]*)+(.*$)/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching multi-line blockquotes
|
||||||
|
*/
|
||||||
|
const RE_BLOCKQUOTE = /^([^\S\r\n]*>[^\n]+\n?)+/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching HTML tags
|
||||||
|
*/
|
||||||
|
const RE_HTML_TAGS = /^(<\/?[a-zA-Z0-9]+>)(.*$)/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching empty lines
|
||||||
|
*/
|
||||||
|
const RE_EMPTY_LINE = /^\s*?$/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex for matching line starting with plus
|
||||||
|
*/
|
||||||
|
const RE_PLUS = /^\s*\+(?:$|[^+])/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitise Markdown input before rendering
|
||||||
|
* @param content Input string
|
||||||
|
* @returns Sanitised string
|
||||||
|
*/
|
||||||
|
function sanitize(content: string) {
|
||||||
|
return (
|
||||||
|
content
|
||||||
|
// Strip excessive blockquote or list indentation
|
||||||
|
.replace(RE_RECURSIVE, (_, m0, m1) => m0 + m1)
|
||||||
|
|
||||||
|
// Append empty character if string starts with html tag
|
||||||
|
// This is to avoid inconsistencies in rendering Markdown inside/after HTML tags
|
||||||
|
// https://github.com/revoltchat/revite/issues/733
|
||||||
|
.replace(RE_HTML_TAGS, (match) => `\u200E${match}`)
|
||||||
|
|
||||||
|
// Append empty character if line starts with a plus
|
||||||
|
// which would usually open a new list but we want
|
||||||
|
// to avoid that behaviour in our case.
|
||||||
|
.replace(RE_PLUS, (match) => `\u200E${match}`)
|
||||||
|
|
||||||
|
// Replace empty lines with non-breaking space
|
||||||
|
// because remark renderer is collapsing empty
|
||||||
|
// or otherwise whitespace-only lines of text
|
||||||
|
.replace(RE_EMPTY_LINE, "")
|
||||||
|
|
||||||
|
// Ensure empty line after blockquotes for correct rendering
|
||||||
|
.replace(RE_BLOCKQUOTE, (match) => `${match}\n`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(({ content }: MarkdownProps) => {
|
||||||
|
const sanitizedContent = React.useMemo(() => sanitize(content), [content]);
|
||||||
|
|
||||||
|
const [parsedContent, setParsedContent] = React.useState<React.ReactElement>(null!);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
render.process(sanitizedContent).then((file) => setParsedContent(file.result));
|
||||||
|
}, [sanitizedContent]);
|
||||||
|
|
||||||
|
return parsedContent;
|
||||||
|
});
|
74
src/components/markdown/plugins/Codeblock.tsx
Normal file
74
src/components/markdown/plugins/Codeblock.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// adapted from Revite
|
||||||
|
// https://github.com/revoltchat/revite/blob/fe63c6633f32b54aa1989cb34627e72bb3377efd/src/components/markdown/plugins/Codeblock.tsx
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import Tooltip from "../../Tooltip";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base codeblock styles
|
||||||
|
*/
|
||||||
|
const Base = styled.pre`
|
||||||
|
padding: 1em;
|
||||||
|
overflow-x: scroll;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy codeblock contents button styles
|
||||||
|
*/
|
||||||
|
const Lang = styled.div`
|
||||||
|
width: fit-content;
|
||||||
|
position: absolute;
|
||||||
|
right: 60px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
user-select: none;
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--background-tertiary);
|
||||||
|
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
class?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a codeblock with copy text button
|
||||||
|
*/
|
||||||
|
|
||||||
|
function RenderCodeblock(props: Props) {
|
||||||
|
const ref = React.useRef<HTMLPreElement>(null);
|
||||||
|
|
||||||
|
let text = "Copy";
|
||||||
|
if (props.class) {
|
||||||
|
text = props.class.split("-")[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCopy = React.useCallback(() => {
|
||||||
|
const text = ref.current?.querySelector("code")?.innerText;
|
||||||
|
text && navigator.clipboard.writeText(text);
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Base ref={ref}>
|
||||||
|
<Lang>
|
||||||
|
<Tooltip title="Copy to Clipboard" placement="top">
|
||||||
|
<a onClick={onCopy}>{text}</a>
|
||||||
|
</Tooltip>
|
||||||
|
</Lang>
|
||||||
|
{props.children}
|
||||||
|
</Base>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RenderCodeblock;
|
140
src/components/markdown/prism.css
Normal file
140
src/components/markdown/prism.css
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Synthwave '84 Theme originally by Robb Owen [@Robb0wen] for Visual Studio Code
|
||||||
|
* Demo: https://marc.dev/demo/prism-synthwave84
|
||||||
|
*
|
||||||
|
* Ported for PrismJS by Marc Backes [@themarcba]
|
||||||
|
*/
|
||||||
|
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: #f92aad;
|
||||||
|
text-shadow: 0 0 2px #100c0f, 0 0 5px #dc078e33, 0 0 10px #fff3;
|
||||||
|
background: none;
|
||||||
|
font-family: var(--font-family-code);
|
||||||
|
font-size: 1em;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre) > code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background-color: transparent !important;
|
||||||
|
background-image: linear-gradient(to bottom, #2a2139 75%, #34294f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: 0.1em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.block-comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #8e8e8e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.tag,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.namespace,
|
||||||
|
.token.number,
|
||||||
|
.token.unit,
|
||||||
|
.token.hexcode,
|
||||||
|
.token.deleted {
|
||||||
|
color: #e2777a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.selector {
|
||||||
|
color: #72f1b8;
|
||||||
|
text-shadow: 0 0 2px #100c0f, 0 0 10px #257c5575, 0 0 35px #21272475;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.function-name {
|
||||||
|
color: #6196cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.boolean,
|
||||||
|
.token.selector .token.id,
|
||||||
|
.token.function {
|
||||||
|
color: #fdfdfd;
|
||||||
|
text-shadow: 0 0 2px #001716, 0 0 3px #03edf975, 0 0 5px #03edf975, 0 0 8px #03edf975;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.class-name {
|
||||||
|
color: #fff5f6;
|
||||||
|
text-shadow: 0 0 2px #000, 0 0 10px #fc1f2c75, 0 0 5px #fc1f2c75, 0 0 25px #fc1f2c75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol {
|
||||||
|
color: #f92aad;
|
||||||
|
text-shadow: 0 0 2px #100c0f, 0 0 5px #dc078e33, 0 0 10px #fff3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important,
|
||||||
|
.token.atrule,
|
||||||
|
.token.keyword,
|
||||||
|
.token.selector .token.class,
|
||||||
|
.token.builtin {
|
||||||
|
color: #f4eee4;
|
||||||
|
text-shadow: 0 0 2px #393a33, 0 0 8px #f39f0575, 0 0 2px #f39f0575;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.regex,
|
||||||
|
.token.variable {
|
||||||
|
color: #f87c32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url {
|
||||||
|
color: #67cdcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.inserted {
|
||||||
|
color: green;
|
||||||
|
}
|
@ -1,222 +1,52 @@
|
|||||||
import { MessageType } from "@spacebarchat/spacebar-api-types/v9";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import React, { Fragment } from "react";
|
import { memo } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
|
||||||
import { MessageLike } from "../../stores/objects/Message";
|
import { MessageLike } from "../../stores/objects/Message";
|
||||||
|
import { QueuedMessageStatus } from "../../stores/objects/QueuedMessage";
|
||||||
import Avatar from "../Avatar";
|
import Avatar from "../Avatar";
|
||||||
import Link from "../Link";
|
import Markdown from "../markdown/RemarkRenderer";
|
||||||
import { IContextMenuItem } from "./../ContextMenuItem";
|
|
||||||
import MessageAttachment from "./MessageAttachment";
|
import MessageAttachment from "./MessageAttachment";
|
||||||
import MessageAuthor from "./MessageAuthor";
|
import MessageAuthor from "./MessageAuthor";
|
||||||
import MessageBase from "./MessageBase";
|
import MessageBase, { MessageContent, MessageDetails, MessageInfo } from "./MessageBase";
|
||||||
import MessageEmbed from "./MessageEmbed";
|
|
||||||
import MessageTimestamp from "./MessageTimestamp";
|
|
||||||
import SystemMessage from "./SystemMessage";
|
|
||||||
import AttachmentUploadProgress from "./attachments/AttachmentUploadProgress";
|
import AttachmentUploadProgress from "./attachments/AttachmentUploadProgress";
|
||||||
|
|
||||||
const MessageListItem = styled.li`
|
|
||||||
list-style: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Container = styled.div<{ isHeader?: boolean }>`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
position: relative;
|
|
||||||
padding: 2px 12px;
|
|
||||||
margin-top: ${(props) => (props.isHeader ? "20px" : undefined)};
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--background-primary-highlight);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MessageContentContainer = styled.div<{ isHeader?: boolean }>`
|
|
||||||
flex: 1;
|
|
||||||
margin-left: ${(props) => (props.isHeader ? undefined : "50px")};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MessageHeader = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MessageContent = styled.div<{ sending?: boolean; failed?: boolean }>`
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: var(--font-weight-light);
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: anywhere;
|
|
||||||
opacity: ${(props) => (props.sending ? 0.5 : undefined)};
|
|
||||||
color: ${(props) => (props.failed ? "var(--error)" : undefined)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MessageHeaderWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function parseMessageContent(content?: string | null) {
|
|
||||||
if (!content) return null;
|
|
||||||
// replace links with Link components
|
|
||||||
const replacedText = reactStringReplace(content, /(https?:\/\/\S+)/g, (match, i) => (
|
|
||||||
<Link key={match + i} href={match} target="_blank" rel="noreferrer">
|
|
||||||
{match}
|
|
||||||
</Link>
|
|
||||||
));
|
|
||||||
|
|
||||||
return replacedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: MessageLike;
|
message: MessageLike;
|
||||||
isHeader?: boolean;
|
header?: boolean;
|
||||||
isSending?: boolean;
|
|
||||||
isFailed?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function Message({ message, header }: Props) {
|
||||||
* Component for rendering a single message
|
|
||||||
*/
|
|
||||||
function Message({ message, isHeader, isSending, isFailed }: Props) {
|
|
||||||
const contextMenu = React.useContext(ContextMenuContext);
|
|
||||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([
|
|
||||||
{
|
|
||||||
label: "Copy Message ID",
|
|
||||||
onClick: () => {
|
|
||||||
navigator.clipboard.writeText(message.id);
|
|
||||||
},
|
|
||||||
iconProps: {
|
|
||||||
icon: "mdiIdentifier",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const withMessageHeader = React.useCallback(
|
|
||||||
(children: React.ReactNode, showHeader = false) => (
|
|
||||||
<MessageHeaderWrapper>
|
|
||||||
{showHeader && (
|
|
||||||
<Avatar
|
|
||||||
key={message.author.id}
|
|
||||||
user={message.author}
|
|
||||||
size={40}
|
|
||||||
style={{
|
|
||||||
marginRight: 10,
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<MessageContentContainer isHeader={showHeader}>
|
|
||||||
{showHeader && (
|
|
||||||
<MessageHeader>
|
|
||||||
<MessageAuthor message={message} />
|
|
||||||
<MessageTimestamp date={message.timestamp} />
|
|
||||||
</MessageHeader>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{"files" in message && message.files?.length !== 0 && (
|
|
||||||
<AttachmentUploadProgress message={message} />
|
|
||||||
)}
|
|
||||||
</MessageContentContainer>
|
|
||||||
</MessageHeaderWrapper>
|
|
||||||
),
|
|
||||||
[message, contextMenuItems],
|
|
||||||
);
|
|
||||||
|
|
||||||
const constructDefaultMessage = React.useCallback(
|
|
||||||
() =>
|
|
||||||
withMessageHeader(
|
|
||||||
<MessageContent sending={isSending} failed={isFailed}>
|
|
||||||
{message.type !== MessageType.Default && (
|
|
||||||
<div style={{ color: "var(--text-secondary)", fontSize: "12px" }}>
|
|
||||||
MessageType({MessageType[message.type]})
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{parseMessageContent(message.content)}
|
|
||||||
{"edited_timestamp" in message && message.edited_timestamp && (
|
|
||||||
<MessageTimestamp date={message.edited_timestamp}>
|
|
||||||
<span style={{ color: "var(--text-secondary)", fontSize: "12px", paddingLeft: "5px" }}>
|
|
||||||
(edited)
|
|
||||||
</span>
|
|
||||||
</MessageTimestamp>
|
|
||||||
)}
|
|
||||||
{"attachments" in message
|
|
||||||
? message.attachments.map((attachment, index) => (
|
|
||||||
<Fragment key={index}>
|
|
||||||
<MessageAttachment
|
|
||||||
key={index}
|
|
||||||
attachment={attachment}
|
|
||||||
contextMenuItems={contextMenuItems}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
{"embeds" in message
|
|
||||||
? message.embeds.map((embed, index) => (
|
|
||||||
<Fragment key={index}>
|
|
||||||
<MessageEmbed key={index} embed={embed} contextMenuItems={contextMenuItems} />
|
|
||||||
</Fragment>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
</MessageContent>,
|
|
||||||
isHeader,
|
|
||||||
),
|
|
||||||
[message, isHeader],
|
|
||||||
);
|
|
||||||
|
|
||||||
const constructJoinMessage = React.useCallback(() => {
|
|
||||||
const joinMessage = message.getJoinMessage();
|
|
||||||
return (
|
|
||||||
<SystemMessage
|
|
||||||
message={message}
|
|
||||||
iconProps={{ icon: "mdiArrowRight", size: "16px", color: "var(--success)" }}
|
|
||||||
>
|
|
||||||
{reactStringReplace(joinMessage, "{author}", (_, i) => (
|
|
||||||
<Link color="var(--text)" style={{ fontWeight: "var(--font-weight-medium)" }} key={i}>
|
|
||||||
{message.author.username}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</SystemMessage>
|
|
||||||
);
|
|
||||||
}, [message]);
|
|
||||||
|
|
||||||
// handles creating the message content based on the message type
|
|
||||||
const renderMessageContent = React.useCallback(() => {
|
|
||||||
switch (message.type) {
|
|
||||||
case MessageType.Default:
|
|
||||||
return constructDefaultMessage();
|
|
||||||
case MessageType.UserJoin:
|
|
||||||
return constructJoinMessage();
|
|
||||||
default:
|
|
||||||
return constructDefaultMessage();
|
|
||||||
}
|
|
||||||
}, [message, isSending, isFailed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageListItem>
|
<MessageBase
|
||||||
<Container
|
header={header}
|
||||||
isHeader={isHeader}
|
sending={"status" in message && message.status === QueuedMessageStatus.SENDING}
|
||||||
onContextMenu={(e) => {
|
failed={"status" in message && message.status === QueuedMessageStatus.FAILED}
|
||||||
e.preventDefault();
|
>
|
||||||
e.stopPropagation();
|
<MessageInfo click={typeof header !== "undefined"}>
|
||||||
contextMenu.open({
|
{header ? (
|
||||||
position: {
|
<Avatar key={message.author.id} user={message.author} size={40} />
|
||||||
x: e.pageX,
|
) : (
|
||||||
y: e.pageY,
|
<MessageDetails message={message} position="left" />
|
||||||
},
|
)}
|
||||||
items: contextMenuItems,
|
</MessageInfo>
|
||||||
});
|
<MessageContent>
|
||||||
}}
|
{header && (
|
||||||
>
|
<span className="message-details">
|
||||||
<MessageBase>{renderMessageContent()}</MessageBase>
|
<MessageAuthor message={message} />
|
||||||
</Container>
|
<MessageDetails message={message} position="top" />
|
||||||
</MessageListItem>
|
</span>
|
||||||
|
)}
|
||||||
|
{message.content && <Markdown content={message.content} />}
|
||||||
|
{"attachments" in message &&
|
||||||
|
message.attachments.map((attachment, index) => (
|
||||||
|
<MessageAttachment key={index} attachment={attachment} />
|
||||||
|
))}
|
||||||
|
{/* {message.embeds?.map((embed, index) => (
|
||||||
|
<MessageEmbed key={index} embed={embed} />
|
||||||
|
))} */}
|
||||||
|
{"files" in message && message.files?.length !== 0 && <AttachmentUploadProgress message={message} />}
|
||||||
|
</MessageContent>
|
||||||
|
</MessageBase>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(Message);
|
export default memo(observer(Message));
|
||||||
|
@ -23,7 +23,7 @@ const Image = styled.img`
|
|||||||
|
|
||||||
interface AttachmentProps {
|
interface AttachmentProps {
|
||||||
attachment: APIAttachment;
|
attachment: APIAttachment;
|
||||||
contextMenuItems: IContextMenuItem[];
|
contextMenuItems?: IContextMenuItem[];
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
}
|
}
|
||||||
@ -70,7 +70,7 @@ export default function MessageAttachment({ attachment, contextMenuItems, maxWid
|
|||||||
y: e.pageY,
|
y: e.pageY,
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
...contextMenuItems,
|
...(contextMenuItems ?? []),
|
||||||
{
|
{
|
||||||
label: "Copy Attachment URL",
|
label: "Copy Attachment URL",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
@ -1,17 +1,128 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import Message, { MessageLike } from "../../stores/objects/Message";
|
||||||
|
import { calendarStrings } from "../../utils/i18n";
|
||||||
|
import Tooltip from "../Tooltip";
|
||||||
|
|
||||||
const Container = styled.div`
|
interface Props {
|
||||||
|
header?: boolean;
|
||||||
|
failed?: boolean;
|
||||||
|
sending?: boolean;
|
||||||
|
mention?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default styled.div<Props>`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
overflow: none;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
position: relative;
|
${(props) => props.header && "margin-top: 20px;"}
|
||||||
padding: 2px 12px;
|
${(props) => props.failed && "color: var(--error);"}
|
||||||
|
${(props) => props.sending && "opacity: 0.5;"}
|
||||||
|
${(props) => props.mention && "background-color: var(--mention);"}
|
||||||
|
|
||||||
|
.message-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-details > .name {
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--background-primary-highlight);
|
background-color: var(--background-primary-highlight);
|
||||||
|
|
||||||
|
time,
|
||||||
|
.edited {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
function MessageBase({ children }: { children: React.ReactNode }) {
|
|
||||||
return <Container>{children}</Container>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MessageBase;
|
export const MessageInfo = styled.div<{ click: boolean }>`
|
||||||
|
width: 62px;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 2px;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.messageTimestampWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
time,
|
||||||
|
.edited {
|
||||||
|
opacity: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MessageContent = styled.div`
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding-right: 48px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DetailBase = styled.div`
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
display: inline-flex;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding-left: 4px;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
.edited {
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MessageDetails = observer(({ message, position }: { message: MessageLike; position: "left" | "top" }) => {
|
||||||
|
if (position === "left") {
|
||||||
|
if (message instanceof Message && message.edited_timestamp) {
|
||||||
|
return (
|
||||||
|
<div className="messageTimestampWrapper">
|
||||||
|
<Tooltip title={dayjs(message.timestamp).format("dddd, MMMM MM, h:mm A")}>
|
||||||
|
<time className="copyTime" dateTime={message.edited_timestamp.toISOString()}>
|
||||||
|
{dayjs(message.edited_timestamp).format("h:mm A")}
|
||||||
|
</time>
|
||||||
|
</Tooltip>
|
||||||
|
<span className="edited">
|
||||||
|
<Tooltip title={dayjs(message.edited_timestamp).format("dddd, MMMM MM, h:mm A")}>
|
||||||
|
<span>(edited)</span>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<time dateTime={message.timestamp.toISOString()}>{dayjs(message.timestamp).format("h:mm A")}</time>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DetailBase>
|
||||||
|
<Tooltip title={dayjs(message.timestamp).format("dddd, MMMM MM, h:mm A")}>
|
||||||
|
<time className="copyTime" dateTime={message.timestamp.toISOString()}>
|
||||||
|
{dayjs(message.timestamp).calendar(undefined, calendarStrings)}
|
||||||
|
</time>
|
||||||
|
</Tooltip>
|
||||||
|
{message instanceof Message && message.edited_timestamp && (
|
||||||
|
<Tooltip title={dayjs(message.edited_timestamp).format("dddd, MMMM MM, h:mm A")}>
|
||||||
|
<span className="edited">(edited)</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</DetailBase>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import { MessageType } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
import { MessageGroup as MessageGroupType } from "../../stores/MessageStore";
|
import { MessageGroup as MessageGroupType } from "../../stores/MessageStore";
|
||||||
import { QueuedMessageStatus } from "../../stores/objects/QueuedMessage";
|
|
||||||
import Message from "./Message";
|
import Message from "./Message";
|
||||||
|
import SystemMessage from "./SystemMessage";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
group: MessageGroupType;
|
group: MessageGroupType;
|
||||||
@ -11,19 +13,15 @@ interface Props {
|
|||||||
* Component that handles rendering a group of messages from the same author
|
* Component that handles rendering a group of messages from the same author
|
||||||
*/
|
*/
|
||||||
function MessageGroup({ group }: Props) {
|
function MessageGroup({ group }: Props) {
|
||||||
|
const app = useAppStore();
|
||||||
const { messages } = group;
|
const { messages } = group;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{messages.map((message, index) => {
|
{messages.map((message, index) => {
|
||||||
return (
|
if (message.type === MessageType.Default || message.type === MessageType.Reply) {
|
||||||
<Message
|
return <Message key={index} message={message} header={index === messages.length - 1} />;
|
||||||
key={message.id}
|
} else return <SystemMessage key={index} message={message} />;
|
||||||
message={message}
|
|
||||||
isHeader={index === messages.length - 1}
|
|
||||||
isSending={"status" in message && message.status === QueuedMessageStatus.SENDING}
|
|
||||||
isFailed={"status" in message && message.status === QueuedMessageStatus.FAILED}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -90,7 +90,7 @@ function MessageList({ guild, channel }: Props) {
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column-reverse",
|
flexDirection: "column-reverse",
|
||||||
marginBottom: "30px",
|
marginBottom: 30,
|
||||||
}} // to put endMessage and loader to the top.
|
}} // to put endMessage and loader to the top.
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
inverse={true}
|
inverse={true}
|
||||||
@ -100,7 +100,7 @@ function MessageList({ guild, channel }: Props) {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignContent: "center",
|
alignContent: "center",
|
||||||
marginBottom: "30px",
|
marginBottom: 30,
|
||||||
}}
|
}}
|
||||||
color="var(--primary)"
|
color="var(--primary)"
|
||||||
/>
|
/>
|
||||||
@ -121,8 +121,8 @@ function MessageList({ guild, channel }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginBottom: "30px",
|
marginBottom: 30,
|
||||||
paddingLeft: "20px",
|
paddingLeft: 20,
|
||||||
color: "var(--text-secondary)",
|
color: "var(--text-secondary)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import { calendarStrings } from "../../utils/i18n";
|
|
||||||
import Tooltip from "../Tooltip";
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: var(--font-weight-regular);
|
|
||||||
margin-left: 10px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
user-select: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
date: string | number | Date | dayjs.Dayjs;
|
|
||||||
children?: React.ReactElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MessageTimestamp({ date, children }: Props) {
|
|
||||||
return (
|
|
||||||
<Tooltip title={dayjs(date).format("dddd, MMMM MM, h:mm A")} placement="top">
|
|
||||||
{children ? children : <Container>{dayjs(date).calendar(undefined, calendarStrings)}</Container>}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MessageTimestamp;
|
|
@ -1,36 +1,82 @@
|
|||||||
import React from "react";
|
import * as Icons from "@mdi/js";
|
||||||
|
import { MessageType } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import reactStringReplace from "react-string-replace";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { MessageLike } from "../../stores/objects/Message";
|
import { MessageLike } from "../../stores/objects/Message";
|
||||||
import Icon, { IconProps } from "../Icon";
|
import Icon from "../Icon";
|
||||||
import MessageTimestamp from "./MessageTimestamp";
|
import MessageBase, { MessageDetails, MessageInfo } from "./MessageBase";
|
||||||
|
|
||||||
const Container = styled.div`
|
const SystemContent = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding: 2px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SystemUser = styled.span`
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: MessageLike;
|
message: MessageLike;
|
||||||
children: React.ReactNode;
|
highlight?: boolean;
|
||||||
iconProps?: IconProps;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SystemMessage({ message, children, iconProps }: Props) {
|
const ICONS: Partial<Record<MessageType, { icon: keyof typeof Icons; color?: string }>> = {
|
||||||
|
[MessageType.UserJoin]: {
|
||||||
|
icon: "mdiArrowRight",
|
||||||
|
color: "var(--success)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function SystemMessage({ message, highlight }: Props) {
|
||||||
|
const icon = ICONS[message.type] ?? {
|
||||||
|
icon: "mdiInformation",
|
||||||
|
};
|
||||||
|
|
||||||
|
let children;
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.UserJoin: {
|
||||||
|
const joinMessage = message.getJoinMessage();
|
||||||
|
children = (
|
||||||
|
<div>
|
||||||
|
{reactStringReplace(joinMessage, "{author}", (_, i) => (
|
||||||
|
<SystemUser key={i}>{message.author.username}</SystemUser>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MessageType.Default:
|
||||||
|
children = <ReactMarkdown children={message.content} />;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// children = <span>Unimplemented system message type '{MessageType[message.type]}'</span>;
|
||||||
|
children = <ReactMarkdown children={message.content} />;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<MessageBase header>
|
||||||
<div style={{ margin: "0 10px", display: "flex" }}>{iconProps && <Icon {...iconProps} />}</div>
|
<MessageInfo click={false}>
|
||||||
<div
|
<Icon icon={icon.icon} size="16px" color={icon.color ?? "var(--text-secondary)"} />
|
||||||
style={{
|
</MessageInfo>
|
||||||
color: "var(--text-secondary)",
|
<SystemContent>{children}</SystemContent>
|
||||||
fontWeight: "var(--font-weight-regular)",
|
<MessageDetails message={message} position="top" />
|
||||||
fontSize: "16px",
|
</MessageBase>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
<MessageTimestamp date={message.timestamp} />
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SystemMessage;
|
export default observer(SystemMessage);
|
||||||
|
@ -12,6 +12,7 @@ const font: ThemeFont["font"] = {
|
|||||||
black: 900,
|
black: 900,
|
||||||
},
|
},
|
||||||
family: "Roboto, Arial, Helvetica, sans-serif",
|
family: "Roboto, Arial, Helvetica, sans-serif",
|
||||||
|
familyCode: '"Roboto Mono", monospace',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ThemeVariables =
|
export type ThemeVariables =
|
||||||
@ -70,6 +71,7 @@ export type ThemeFont = {
|
|||||||
black?: number;
|
black?: number;
|
||||||
};
|
};
|
||||||
family: string;
|
family: string;
|
||||||
|
familyCode: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import "@fontsource/source-code-pro";
|
import "@fontsource/roboto-mono/100.css";
|
||||||
// import "@fontsource/source-sans-pro/200.css";
|
import "@fontsource/roboto-mono/200.css";
|
||||||
// import "@fontsource/source-sans-pro/300.css";
|
import "@fontsource/roboto-mono/300.css";
|
||||||
// import "@fontsource/source-sans-pro/400.css";
|
import "@fontsource/roboto-mono/400.css";
|
||||||
// import "@fontsource/source-sans-pro/600.css";
|
import "@fontsource/roboto-mono/500.css";
|
||||||
// import "@fontsource/source-sans-pro/700.css";
|
import "@fontsource/roboto-mono/600.css";
|
||||||
// import "@fontsource/source-sans-pro/900.css";
|
import "@fontsource/roboto-mono/700.css";
|
||||||
import "@fontsource/roboto/100.css";
|
import "@fontsource/roboto/100.css";
|
||||||
import "@fontsource/roboto/300.css";
|
import "@fontsource/roboto/300.css";
|
||||||
import "@fontsource/roboto/400.css";
|
import "@fontsource/roboto/400.css";
|
||||||
|
@ -28,7 +28,6 @@ export default class GuildMemberStore {
|
|||||||
}
|
}
|
||||||
const m = new GuildMember(this.app, this.guild, member);
|
const m = new GuildMember(this.app, this.guild, member);
|
||||||
this.members.set(member.user.id, m);
|
this.members.set(member.user.id, m);
|
||||||
console.log(`added member ${m.user?.username}`);
|
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +103,6 @@ export default class Guild {
|
|||||||
this.roles.addAll(data.roles);
|
this.roles.addAll(data.roles);
|
||||||
// FIXME: hack to prevent errors after guild creation where channels is undefined
|
// FIXME: hack to prevent errors after guild creation where channels is undefined
|
||||||
if (data.channels) {
|
if (data.channels) {
|
||||||
console.log(data.channels);
|
|
||||||
this.channels.addAll(data.channels);
|
this.channels.addAll(data.channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user