(basic) MFM support
This commit is contained in:
parent
06adc19ea9
commit
4a93c2272b
11 changed files with 315 additions and 15 deletions
27
package-lock.json
generated
27
package-lock.json
generated
|
@ -13,6 +13,7 @@
|
|||
"@testing-library/user-event": "^13.5.0",
|
||||
"canvas-confetti": "^1.5.1",
|
||||
"customize-cra": "^1.0.0",
|
||||
"mfm-js": "^0.22.1",
|
||||
"polished": "^4.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
|
@ -10982,6 +10983,14 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mfm-js": {
|
||||
"version": "0.22.1",
|
||||
"resolved": "https://registry.npmjs.org/mfm-js/-/mfm-js-0.22.1.tgz",
|
||||
"integrity": "sha512-UV5zvDKlWPpBFeABhyCzuOTJ3RwrNrmVpJ+zz/dFX6D/ntEywljgxkfsLamcy0ZSwUAr0O+WQxGHvAwyxUgsAQ==",
|
||||
"dependencies": {
|
||||
"twemoji-parser": "14.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
|
||||
|
@ -15101,6 +15110,11 @@
|
|||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/twemoji-parser": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz",
|
||||
"integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA=="
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
@ -24211,6 +24225,14 @@
|
|||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
},
|
||||
"mfm-js": {
|
||||
"version": "0.22.1",
|
||||
"resolved": "https://registry.npmjs.org/mfm-js/-/mfm-js-0.22.1.tgz",
|
||||
"integrity": "sha512-UV5zvDKlWPpBFeABhyCzuOTJ3RwrNrmVpJ+zz/dFX6D/ntEywljgxkfsLamcy0ZSwUAr0O+WQxGHvAwyxUgsAQ==",
|
||||
"requires": {
|
||||
"twemoji-parser": "14.0.x"
|
||||
}
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
|
||||
|
@ -27067,6 +27089,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"twemoji-parser": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz",
|
||||
"integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA=="
|
||||
},
|
||||
"type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"@testing-library/user-event": "^13.5.0",
|
||||
"canvas-confetti": "^1.5.1",
|
||||
"customize-cra": "^1.0.0",
|
||||
"mfm-js": "^0.22.1",
|
||||
"polished": "^4.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { parse as mfmParse, toString as mfmUnparse } from 'mfm-js';
|
||||
|
||||
import XTERM_COLORS from '../../../xterm';
|
||||
|
||||
import { Wrapper, Author, Body, Emote, Attachments } from './message.style';
|
||||
|
||||
import * as mfmElements from './mfm';
|
||||
|
||||
const stringHashcode = (str) => {
|
||||
var hash = 0, i, chr;
|
||||
if (str.length === 0) return hash;
|
||||
|
@ -16,18 +19,33 @@ const stringHashcode = (str) => {
|
|||
return hash;
|
||||
}
|
||||
|
||||
const emoteRegex = /:([^:\s]+):/g;
|
||||
|
||||
const elForType = {
|
||||
image: (src) => <img src={src} />,
|
||||
audio: (src) => <audio src={src} />,
|
||||
video: (src) => <video src={src} />,
|
||||
};
|
||||
|
||||
function Message({
|
||||
user_id, user_name, user_type, user_color, text, emotes, attachments,
|
||||
deemphasize,
|
||||
}) {
|
||||
const renderMfm = (mfmNode, note) => {
|
||||
const ElForNode = mfmElements[mfmNode.type];
|
||||
|
||||
let node;
|
||||
if (!ElForNode) {
|
||||
const text = mfmUnparse(mfmNode);
|
||||
const TextNode = mfmElements.text;
|
||||
node = <TextNode note={note} text={text} />;
|
||||
} else {
|
||||
const children = mfmNode.children?.map((node) => renderMfm(node, note));
|
||||
node = <ElForNode note={note} {...mfmNode.props} children={children} />;
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
function Message(note) {
|
||||
const {
|
||||
user_id, user_name, user_type, user_color, text, emotes, attachments, deemphasize,
|
||||
} = note;
|
||||
|
||||
const fallbackColor = XTERM_COLORS[(stringHashcode(user_id.toString()) % 144) + 88];
|
||||
const displayFiles = useMemo(() => {
|
||||
if (attachments) {
|
||||
|
@ -39,21 +57,17 @@ function Message({
|
|||
return null;
|
||||
}, [attachments]);
|
||||
|
||||
const mfmTree = useMemo(() => mfmParse(text), [text]);
|
||||
const tree = mfmTree.map((node) => renderMfm(node, note));
|
||||
|
||||
return (
|
||||
<Wrapper type={user_type} color={user_color ?? fallbackColor} deemphasize={deemphasize}>
|
||||
<Author>
|
||||
[ {user_name} ]
|
||||
</Author>
|
||||
{!!text && (
|
||||
{mfmTree.length !== 0 && (
|
||||
<Body>
|
||||
{text.split(emoteRegex).map((textOrEmote, index) => {
|
||||
if (textOrEmote === "") return null;
|
||||
if (index % 2 === 0) {
|
||||
return <span>{textOrEmote}</span>;
|
||||
} else {
|
||||
return <Emote src={emotes?.[textOrEmote]} title={textOrEmote} />;
|
||||
}
|
||||
}).filter((item) => item !== null)}
|
||||
{tree}
|
||||
</Body>
|
||||
)}
|
||||
{!!displayFiles && (
|
||||
|
|
11
src/components/chat-log/message/mfm/bold.jsx
Normal file
11
src/components/chat-log/message/mfm/bold.jsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
const Embolden = styled.span`
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const BoldText = ({ children }) => (
|
||||
<Embolden>{children}</Embolden>
|
||||
);
|
||||
|
||||
export default BoldText;
|
36
src/components/chat-log/message/mfm/emojiCode.jsx
Normal file
36
src/components/chat-log/message/mfm/emojiCode.jsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types'
|
||||
import Text from './text';
|
||||
|
||||
const Emote = styled.img`
|
||||
height: 2em;
|
||||
vertical-align: middle;
|
||||
`;
|
||||
|
||||
const EmojiCode = ({
|
||||
note, name,
|
||||
}) => {
|
||||
if (note.emotes?.[name]) {
|
||||
return (
|
||||
<Emote src={note.emotes[name]} title={name} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text text={`:${name}:`} />
|
||||
);
|
||||
};
|
||||
|
||||
EmojiCode.propTypes = {
|
||||
note: PropTypes.shape({
|
||||
emotes: PropTypes.shape({
|
||||
[PropTypes.string]: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
EmojiCode.defaultProps = {
|
||||
note: { emotes: {} },
|
||||
};
|
||||
|
||||
export default EmojiCode;
|
178
src/components/chat-log/message/mfm/fn.jsx
Normal file
178
src/components/chat-log/message/mfm/fn.jsx
Normal file
|
@ -0,0 +1,178 @@
|
|||
import styled, { css, keyframes } from 'styled-components';
|
||||
|
||||
const tada = keyframes`
|
||||
from {
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
10%, 20% {
|
||||
transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
30%, 50%, 70%, 90% {
|
||||
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
|
||||
}
|
||||
40%, 60%, 80% {
|
||||
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
to {
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
`;
|
||||
const spin = keyframes`
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
`;
|
||||
const spinX = keyframes`
|
||||
0% { transform: perspective(128px) rotateX(0deg); }
|
||||
100% { transform: perspective(128px) rotateX(360deg); }
|
||||
`;
|
||||
const spinY = keyframes`
|
||||
0% { transform: perspective(128px) rotateY(0deg); }
|
||||
100% { transform: perspective(128px) rotateY(360deg); }
|
||||
`;
|
||||
const jump = keyframes`
|
||||
0% { transform: translateY(0); }
|
||||
25% { transform: translateY(-16px); }
|
||||
50% { transform: translateY(0); }
|
||||
75% { transform: translateY(-8px); }
|
||||
100% { transform: translateY(0); }
|
||||
`;
|
||||
const bounce = keyframes`
|
||||
0% { transform: translateY(0) scale(1, 1); }
|
||||
25% { transform: translateY(-16px) scale(1, 1); }
|
||||
50% { transform: translateY(0) scale(1, 1); }
|
||||
75% { transform: translateY(0) scale(1.5, 0.75); }
|
||||
100% { transform: translateY(0) scale(1, 1); }
|
||||
`;
|
||||
const twitch = keyframes`
|
||||
0% { transform: translate(7px, -2px) }
|
||||
5% { transform: translate(-3px, 1px) }
|
||||
10% { transform: translate(-7px, -1px) }
|
||||
15% { transform: translate(0px, -1px) }
|
||||
20% { transform: translate(-8px, 6px) }
|
||||
25% { transform: translate(-4px, -3px) }
|
||||
30% { transform: translate(-4px, -6px) }
|
||||
35% { transform: translate(-8px, -8px) }
|
||||
40% { transform: translate(4px, 6px) }
|
||||
45% { transform: translate(-3px, 1px) }
|
||||
50% { transform: translate(2px, -10px) }
|
||||
55% { transform: translate(-7px, 0px) }
|
||||
60% { transform: translate(-2px, 4px) }
|
||||
65% { transform: translate(3px, -8px) }
|
||||
70% { transform: translate(6px, 7px) }
|
||||
75% { transform: translate(-7px, -2px) }
|
||||
80% { transform: translate(-7px, -8px) }
|
||||
85% { transform: translate(9px, 3px) }
|
||||
90% { transform: translate(-3px, -2px) }
|
||||
95% { transform: translate(-10px, 2px) }
|
||||
100% { transform: translate(-2px, -6px) }
|
||||
`;
|
||||
const shake = keyframes`
|
||||
0% { transform: translate(-3px, -1px) rotate(-8deg) }
|
||||
5% { transform: translate(0px, -1px) rotate(-10deg) }
|
||||
10% { transform: translate(1px, -3px) rotate(0deg) }
|
||||
15% { transform: translate(1px, 1px) rotate(11deg) }
|
||||
20% { transform: translate(-2px, 1px) rotate(1deg) }
|
||||
25% { transform: translate(-1px, -2px) rotate(-2deg) }
|
||||
30% { transform: translate(-1px, 2px) rotate(-3deg) }
|
||||
35% { transform: translate(2px, 1px) rotate(6deg) }
|
||||
40% { transform: translate(-2px, -3px) rotate(-9deg) }
|
||||
45% { transform: translate(0px, -1px) rotate(-12deg) }
|
||||
50% { transform: translate(1px, 2px) rotate(10deg) }
|
||||
55% { transform: translate(0px, -3px) rotate(8deg) }
|
||||
60% { transform: translate(1px, -1px) rotate(8deg) }
|
||||
65% { transform: translate(0px, -1px) rotate(-7deg) }
|
||||
70% { transform: translate(-1px, -3px) rotate(6deg) }
|
||||
75% { transform: translate(0px, -2px) rotate(4deg) }
|
||||
80% { transform: translate(-2px, -1px) rotate(3deg) }
|
||||
85% { transform: translate(1px, -3px) rotate(-10deg) }
|
||||
90% { transform: translate(1px, 0px) rotate(3deg) }
|
||||
95% { transform: translate(-2px, 0px) rotate(-3deg) }
|
||||
100% { transform: translate(2px, 1px) rotate(2deg) }
|
||||
`;
|
||||
const rubberBand = keyframes`
|
||||
from { transform: scale3d(1, 1, 1); }
|
||||
30% { transform: scale3d(1.25, 0.75, 1); }
|
||||
40% { transform: scale3d(0.75, 1.25, 1); }
|
||||
50% { transform: scale3d(1.15, 0.85, 1); }
|
||||
65% { transform: scale3d(0.95, 1.05, 1); }
|
||||
75% { transform: scale3d(1.05, 0.95, 1); }
|
||||
to { transform: scale3d(1, 1, 1); }
|
||||
`;
|
||||
const rainbow = keyframes`
|
||||
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
|
||||
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
|
||||
`;
|
||||
|
||||
const validTime = (t, d = null) => {
|
||||
if (t == null) return d;
|
||||
return t.match(/^[0-9.]+s$/) ? t : d;
|
||||
};
|
||||
|
||||
// TODO: font, sparkle
|
||||
const mfmFnStyles = {
|
||||
tada: css`
|
||||
font-size: 150%;
|
||||
animation: ${tada} 1s linear infinite both;
|
||||
`,
|
||||
jelly: css`animation: ${rubberBand} ${(props) => validTime(props.speed, '1s')} linear infinite both;`,
|
||||
twitch: css`animation: ${twitch} ${(props) => validTime(props.speed, '0.5s')} ease infinite;`,
|
||||
shake: css`animation: ${shake} ${(props) => validTime(props.speed, '0.5s')} ease infinite;`,
|
||||
spin: (props) => {
|
||||
let direction;
|
||||
if (props.left) direction = 'reverse'
|
||||
else if (props.alternate) direction = 'alternate'
|
||||
else direction = 'normal';
|
||||
|
||||
let anime;
|
||||
if (props.x) anime = spinX
|
||||
else if (props.y) anime = spinY
|
||||
else anime = spin;
|
||||
|
||||
return css`
|
||||
animation: ${anime} ${(props) => validTime(props.speed, '1.5s')} linear infinite;
|
||||
animation-direction: ${direction};
|
||||
`;
|
||||
},
|
||||
jump: css`animation: ${jump} 0.75s linear infinite;`,
|
||||
bounce: css`
|
||||
animation: ${bounce} 0.75s linear infinite;
|
||||
transform-origin: center bottom;
|
||||
`,
|
||||
flip: (props) => {
|
||||
let transRights;
|
||||
if (props.h && props.v) transRights = 'scale(-1, -1)';
|
||||
else if (props.v) transRights = 'scaleY(-1)';
|
||||
else transRights = 'scaleX(-1)';
|
||||
|
||||
return css`
|
||||
transform: ${transRights};
|
||||
`;
|
||||
},
|
||||
x2: css`font-size: 200%;`,
|
||||
x3: css`font-size: 400%;`,
|
||||
x4: css`font-size: 600%;`,
|
||||
rainbow: css`
|
||||
color: #f42069;
|
||||
animation: ${rainbow} 1s linear infinite;`,
|
||||
rotate: css`
|
||||
transform: rotate(${(props) => parseInt(props.deg) || '90'}deg);
|
||||
transform-origin: center center;
|
||||
`,
|
||||
};
|
||||
|
||||
const MfmFnRunner = styled.span`
|
||||
> span {
|
||||
display: inline-block;
|
||||
${(props) => mfmFnStyles[props.mfmFn]};
|
||||
}
|
||||
`;
|
||||
|
||||
const MfmFunction = ({ children, name, args }) => (
|
||||
<MfmFnRunner mfmFn={name} {...args}>
|
||||
<span>
|
||||
{children}
|
||||
</span>
|
||||
</MfmFnRunner>
|
||||
);
|
||||
|
||||
export default MfmFunction;
|
7
src/components/chat-log/message/mfm/index.js
Normal file
7
src/components/chat-log/message/mfm/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export { default as emojiCode } from './emojiCode';
|
||||
export { default as italic } from './italic';
|
||||
export { default as bold } from './bold';
|
||||
export { default as fn } from './fn';
|
||||
export { default as unicodeEmoji } from './unicodeEmoji';
|
||||
export { default as url } from './url';
|
||||
export { default as text } from './text';
|
11
src/components/chat-log/message/mfm/italic.jsx
Normal file
11
src/components/chat-log/message/mfm/italic.jsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
const Italic = styled.span`
|
||||
font-style: italic;
|
||||
`;
|
||||
|
||||
const ItalicText = ({ children }) => (
|
||||
<Italic>{children}</Italic>
|
||||
);
|
||||
|
||||
export default ItalicText;
|
5
src/components/chat-log/message/mfm/text.jsx
Normal file
5
src/components/chat-log/message/mfm/text.jsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
const Text = ({ text }) => (
|
||||
<span>{text}</span>
|
||||
);
|
||||
|
||||
export default Text;
|
5
src/components/chat-log/message/mfm/unicodeEmoji.jsx
Normal file
5
src/components/chat-log/message/mfm/unicodeEmoji.jsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
const UnicodeEmoji = ({ emoji }) => (
|
||||
<span>{emoji}</span>
|
||||
);
|
||||
|
||||
export default UnicodeEmoji;
|
5
src/components/chat-log/message/mfm/url.jsx
Normal file
5
src/components/chat-log/message/mfm/url.jsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
const Url = ({ url }) => (
|
||||
<a>{url}</a>
|
||||
);
|
||||
|
||||
export default Url;
|
Loading…
Add table
Reference in a new issue