expand Markdown parsing, add embeds

This commit is contained in:
HF 2022-02-08 22:21:50 +01:00
parent 3eeece6a54
commit e80f9b4447
36 changed files with 1087 additions and 605 deletions

View File

@ -1,5 +1,6 @@
const pkg = require('./package.json');
module.exports = function (api) {
api.cache(true);
const plugins = [
'@babel/plugin-transform-flow-strip-types',
'@babel/plugin-proposal-throw-expressions',
@ -13,11 +14,18 @@ module.exports = function (api) {
const presets = [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
api.caller(caller => caller && caller.target === "node")
? {
targets: {
node: pkg.engines.node.replace(/^\D+/g, ''),
},
modules: false,
}
: {
targets: {
browsers: pkg.browserslist,
},
}
}
],
'@babel/react',
];

View File

@ -83,6 +83,14 @@ msgid ""
"one (Note: you can use those links just once)"
msgstr ""
#: src/ssr-components/Main.jsx:70
msgid "PixelPlanet.fun"
msgstr ""
#: src/ssr-components/Main.jsx:72
msgid "Place color pixels on an map styled canvas with other players online"
msgstr ""
#: src/ssr-components/Globe.jsx:44
msgid "Double click on globe to go back."
msgstr ""
@ -99,14 +107,6 @@ msgstr ""
msgid "A 3D globe of our whole map"
msgstr ""
#: src/ssr-components/Main.jsx:70
msgid "PixelPlanet.fun"
msgstr ""
#: src/ssr-components/Main.jsx:72
msgid "Place color pixels on an map styled canvas with other players online"
msgstr ""
#: src/core/mail.js:65
#, javascript-format
msgid ""
@ -305,6 +305,21 @@ msgstr ""
msgid "Password must be shorter than 60 characters."
msgstr ""
#: src/routes/api/auth/verify.js:25
#: src/routes/api/auth/verify.js:32
msgid "Mail verification"
msgstr ""
#: src/routes/api/auth/verify.js:26
msgid "You are now verified :)"
msgstr ""
#: src/routes/api/auth/verify.js:32
msgid ""
"Your mail verification code is invalid or already expired :(, please "
"request a new one."
msgstr ""
#: src/routes/api/auth/register.js:33
msgid "No Captcha given"
msgstr ""
@ -325,21 +340,6 @@ msgstr ""
msgid "Failed to establish session after register :("
msgstr ""
#: src/routes/api/auth/verify.js:25
#: src/routes/api/auth/verify.js:32
msgid "Mail verification"
msgstr ""
#: src/routes/api/auth/verify.js:26
msgid "You are now verified :)"
msgstr ""
#: src/routes/api/auth/verify.js:32
msgid ""
"Your mail verification code is invalid or already expired :(, please "
"request a new one."
msgstr ""
#: src/routes/api/auth/logout.js:13
msgid "You are not even logged in."
msgstr ""

View File

@ -3,48 +3,6 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Plural-Forms: nplurals=2; plural=(n!=1);\n"
#: src/controls/keypress.js:41
#, javascript-format
msgid "Switched to ${ canvasName }"
msgstr ""
#: src/controls/keypress.js:64
msgid "Grid ON"
msgstr ""
#: src/controls/keypress.js:65
msgid "Grid OFF"
msgstr ""
#: src/controls/keypress.js:75
msgid "Pixel Notify ON"
msgstr ""
#: src/controls/keypress.js:76
msgid "Pixel Notify OFF"
msgstr ""
#: src/controls/keypress.js:81
msgid "Muted Sound"
msgstr ""
#: src/controls/keypress.js:82
msgid "Unmuted Sound"
msgstr ""
#: src/components/CoordinatesBox.jsx:29
#: src/controls/keypress.js:88
msgid "Copied!"
msgstr ""
#: src/controls/keypress.js:94
msgid "Show Hidden Canvases"
msgstr ""
#: src/controls/keypress.js:95
msgid "Hide Hidden Canvases"
msgstr ""
#: src/ui/placePixel.js:53
msgid "Error :("
msgstr ""
@ -146,6 +104,48 @@ msgstr ""
msgid "Error ${ retCode }"
msgstr ""
#: src/controls/keypress.js:41
#, javascript-format
msgid "Switched to ${ canvasName }"
msgstr ""
#: src/controls/keypress.js:64
msgid "Grid ON"
msgstr ""
#: src/controls/keypress.js:65
msgid "Grid OFF"
msgstr ""
#: src/controls/keypress.js:75
msgid "Pixel Notify ON"
msgstr ""
#: src/controls/keypress.js:76
msgid "Pixel Notify OFF"
msgstr ""
#: src/controls/keypress.js:81
msgid "Muted Sound"
msgstr ""
#: src/controls/keypress.js:82
msgid "Unmuted Sound"
msgstr ""
#: src/components/CoordinatesBox.jsx:29
#: src/controls/keypress.js:88
msgid "Copied!"
msgstr ""
#: src/controls/keypress.js:94
msgid "Show Hidden Canvases"
msgstr ""
#: src/controls/keypress.js:95
msgid "Hide Hidden Canvases"
msgstr ""
#: src/ui/renderer.js:36
msgid "Canvas Error"
msgstr ""
@ -170,11 +170,6 @@ msgstr ""
msgid "Look at past Canvases"
msgstr ""
#: src/components/Converter.jsx:559
#: src/components/CoordinatesBox.jsx:32
msgid "Copy to Clipboard"
msgstr ""
#: src/components/OnlineBox.jsx:41
msgid "Online Users on Canvas"
msgstr ""
@ -187,6 +182,11 @@ msgstr ""
msgid "Pixels placed"
msgstr ""
#: src/components/Converter.jsx:559
#: src/components/CoordinatesBox.jsx:32
msgid "Copy to Clipboard"
msgstr ""
#: src/components/ModalRoot.jsx:69
#: src/components/Modtools.jsx:224
#: src/components/Window.jsx:138
@ -198,6 +198,27 @@ msgstr ""
msgid "Restore"
msgstr ""
#: src/components/buttons/ChatButton.jsx:92
msgid "Close Chat"
msgstr ""
#: src/components/buttons/ChatButton.jsx:92
msgid "Open Chat"
msgstr ""
#: src/components/buttons/CanvasSwitchButton.jsx:22
#: src/components/windows/index.js:19
msgid "Canvas Selection"
msgstr ""
#: src/components/buttons/ExpandMenuButton.jsx:23
msgid "Close Menu"
msgstr ""
#: src/components/buttons/ExpandMenuButton.jsx:23
msgid "Open Menu"
msgstr ""
#: src/actions/fetch.js:39
msgid "You made too many requests"
msgstr ""
@ -227,25 +248,8 @@ msgstr ""
msgid "Server answered with gibberish :("
msgstr ""
#: src/components/buttons/CanvasSwitchButton.jsx:22
#: src/components/windows/index.js:22
msgid "Canvas Selection"
msgstr ""
#: src/components/buttons/ChatButton.jsx:92
msgid "Close Chat"
msgstr ""
#: src/components/buttons/ChatButton.jsx:92
msgid "Open Chat"
msgstr ""
#: src/components/buttons/ExpandMenuButton.jsx:23
msgid "Close Menu"
msgstr ""
#: src/components/buttons/ExpandMenuButton.jsx:23
msgid "Open Menu"
#: src/components/buttons/GlobeButton.jsx:35
msgid "Globe View"
msgstr ""
#: src/components/HistorySelect.jsx:144
@ -256,6 +260,33 @@ msgstr ""
msgid "Select Date above"
msgstr ""
#: src/components/buttons/PalselButton.jsx:31
msgid "Close Palette"
msgstr ""
#: src/components/buttons/PalselButton.jsx:31
msgid "Open Palette"
msgstr ""
#: src/components/buttons/HelpButton.jsx:23
#: src/components/windows/index.js:13
msgid "Help"
msgstr ""
#: src/components/buttons/SettingsButton.jsx:23
#: src/components/windows/index.js:14
msgid "Settings"
msgstr ""
#: src/components/buttons/DownloadButton.jsx:37
msgid "Make Screenshot"
msgstr ""
#: src/components/buttons/LogInButton.jsx:23
#: src/components/windows/index.js:15
msgid "User Area"
msgstr ""
#: src/components/Window.jsx:117
msgid "Clone"
msgstr ""
@ -272,37 +303,6 @@ msgstr ""
msgid "Resize"
msgstr ""
#: src/components/buttons/GlobeButton.jsx:35
msgid "Globe View"
msgstr ""
#: src/components/buttons/PalselButton.jsx:31
msgid "Close Palette"
msgstr ""
#: src/components/buttons/PalselButton.jsx:31
msgid "Open Palette"
msgstr ""
#: src/components/buttons/HelpButton.jsx:23
#: src/components/windows/index.js:16
msgid "Help"
msgstr ""
#: src/components/buttons/SettingsButton.jsx:23
#: src/components/windows/index.js:17
msgid "Settings"
msgstr ""
#: src/components/buttons/LogInButton.jsx:23
#: src/components/windows/index.js:18
msgid "User Area"
msgstr ""
#: src/components/buttons/DownloadButton.jsx:37
msgid "Make Screenshot"
msgstr ""
#: src/components/contextmenus/UserContextMenu.jsx:53
msgid "Ping"
msgstr ""
@ -319,19 +319,19 @@ msgstr ""
msgid "Mute"
msgstr ""
#: src/components/windows/index.js:19
#: src/components/windows/index.js:16
msgid "Registration"
msgstr ""
#: src/components/windows/index.js:20
#: src/components/windows/index.js:17
msgid "Forgot Password"
msgstr ""
#: src/components/windows/index.js:21
#: src/components/windows/index.js:18
msgid "Chat"
msgstr ""
#: src/components/windows/index.js:23
#: src/components/windows/index.js:20
msgid "Canvas Archive"
msgstr ""
@ -538,30 +538,6 @@ msgstr ""
msgid "Credit for the Palette of the Top10 canvas goes to ${ vinikLink }."
msgstr ""
#: src/components/windows/UserArea.jsx:27
msgid "Profile"
msgstr ""
#: src/components/windows/UserArea.jsx:30
msgid "Ranking"
msgstr ""
#: src/components/windows/UserArea.jsx:33
msgid "Converter"
msgstr ""
#: src/components/windows/UserArea.jsx:39
msgid "Modtools"
msgstr ""
#: src/components/windows/UserArea.jsx:40
msgid "Loading..."
msgstr ""
#: src/components/windows/UserArea.jsx:47
msgid "Consider joining us on Guilded:"
msgstr ""
#: src/components/windows/Settings.jsx:133
msgid "Show Grid"
msgstr ""
@ -655,48 +631,6 @@ msgstr ""
msgid "Select Language"
msgstr ""
#: src/components/windows/Register.jsx:85
msgid "Register new account here"
msgstr ""
#: src/components/windows/Register.jsx:90
#: src/components/windows/Register.jsx:96
msgid "Name"
msgstr ""
#: src/components/windows/ForgotPassword.jsx:82
#: src/components/windows/Register.jsx:98
#: src/components/windows/Register.jsx:104
msgid "Email"
msgstr ""
#: src/components/ChangeMail.jsx:80
#: src/components/DeleteAccount.jsx:62
#: src/components/LogInForm.jsx:83
#: src/components/windows/Register.jsx:106
#: src/components/windows/Register.jsx:112
msgid "Password"
msgstr ""
#: src/components/windows/Register.jsx:114
#: src/components/windows/Register.jsx:120
msgid "Confirm Password"
msgstr ""
#: src/components/windows/Register.jsx:122
msgid "Captcha"
msgstr ""
#: src/components/Modtools.jsx:311
#: src/components/Modtools.jsx:392
#: src/components/Modtools.jsx:467
#: src/components/Modtools.jsx:512
#: src/components/Modtools.jsx:595
#: src/components/windows/ForgotPassword.jsx:86
#: src/components/windows/Register.jsx:125
msgid "Submit"
msgstr ""
#: src/components/windows/CanvasSelect.jsx:33
msgid ""
"Select the canvas you want to use. Every canvas is unique and has "
@ -749,6 +683,22 @@ msgstr ""
msgid "Enter your mail address and we will send you a new password:"
msgstr ""
#: src/components/windows/ForgotPassword.jsx:82
#: src/components/windows/Register.jsx:98
#: src/components/windows/Register.jsx:104
msgid "Email"
msgstr ""
#: src/components/Modtools.jsx:311
#: src/components/Modtools.jsx:392
#: src/components/Modtools.jsx:467
#: src/components/Modtools.jsx:512
#: src/components/Modtools.jsx:595
#: src/components/windows/ForgotPassword.jsx:86
#: src/components/windows/Register.jsx:125
msgid "Submit"
msgstr ""
#: src/components/windows/Chat.jsx:146
msgid "Channel settings"
msgstr ""
@ -765,6 +715,56 @@ msgstr ""
msgid "You must be logged in to chat"
msgstr ""
#: src/components/windows/UserArea.jsx:27
msgid "Profile"
msgstr ""
#: src/components/windows/UserArea.jsx:30
msgid "Ranking"
msgstr ""
#: src/components/windows/UserArea.jsx:33
msgid "Converter"
msgstr ""
#: src/components/windows/UserArea.jsx:39
msgid "Modtools"
msgstr ""
#: src/components/windows/UserArea.jsx:40
msgid "Loading..."
msgstr ""
#: src/components/windows/UserArea.jsx:47
msgid "Consider joining us on Guilded:"
msgstr ""
#: src/components/windows/Register.jsx:85
msgid "Register new account here"
msgstr ""
#: src/components/windows/Register.jsx:90
#: src/components/windows/Register.jsx:96
msgid "Name"
msgstr ""
#: src/components/ChangeMail.jsx:80
#: src/components/DeleteAccount.jsx:62
#: src/components/LogInForm.jsx:83
#: src/components/windows/Register.jsx:106
#: src/components/windows/Register.jsx:112
msgid "Password"
msgstr ""
#: src/components/windows/Register.jsx:114
#: src/components/windows/Register.jsx:120
msgid "Confirm Password"
msgstr ""
#: src/components/windows/Register.jsx:122
msgid "Captcha"
msgstr ""
#: src/components/Captcha.jsx:50
#: src/components/Captcha.jsx:105
msgid "Could not load captcha"
@ -846,28 +846,56 @@ msgstr ""
msgid "Password must be shorter than 60 characters."
msgstr ""
#: src/components/LogInArea.jsx:21
msgid "Login to access more features and stats."
#: src/components/ChangeMail.jsx:91
#: src/components/ChangeName.jsx:68
#: src/components/ChangePassword.jsx:110
#: src/components/LanguageSelect.jsx:73
msgid "Save"
msgstr ""
#: src/components/LogInArea.jsx:23
msgid "Login with Name or Mail:"
#: src/components/CanvasItem.jsx:30
msgid "Online Users"
msgstr ""
#: src/components/LogInArea.jsx:30
msgid "I forgot my Password."
#: src/components/CanvasItem.jsx:35
msgid "Cooldown"
msgstr ""
#: src/components/LogInArea.jsx:31
msgid "or login with:"
#: src/components/CanvasItem.jsx:41
msgid "Stacking till"
msgstr ""
#: src/components/LogInArea.jsx:72
msgid "or register here:"
#: src/components/CanvasItem.jsx:43
msgid "Ranked"
msgstr ""
#: src/components/LogInArea.jsx:79
msgid "Register"
#: src/components/CanvasItem.jsx:45
msgid "Yes"
msgstr ""
#: src/components/CanvasItem.jsx:45
msgid "No"
msgstr ""
#: src/components/CanvasItem.jsx:51
msgid "Requirements"
msgstr ""
#: src/components/CanvasItem.jsx:54
msgid "User Account"
msgstr ""
#: src/components/CanvasItem.jsx:56
#, javascript-format
msgid "and ${ canvas.req } Pixels set"
msgstr ""
#: src/components/CanvasItem.jsx:59
msgid "Top 10 Daily Ranking"
msgstr ""
#: src/components/CanvasItem.jsx:65
msgid "Dimensions"
msgstr ""
#: src/components/UserAreaContent.jsx:63
@ -1083,64 +1111,28 @@ msgstr ""
msgid "Download Template"
msgstr ""
#: src/components/ChangeMail.jsx:91
#: src/components/ChangeName.jsx:68
#: src/components/ChangePassword.jsx:110
#: src/components/LanguageSelect.jsx:73
msgid "Save"
#: src/components/LogInArea.jsx:21
msgid "Login to access more features and stats."
msgstr ""
#: src/components/CanvasItem.jsx:30
msgid "Online Users"
#: src/components/LogInArea.jsx:23
msgid "Login with Name or Mail:"
msgstr ""
#: src/components/CanvasItem.jsx:35
msgid "Cooldown"
#: src/components/LogInArea.jsx:30
msgid "I forgot my Password."
msgstr ""
#: src/components/CanvasItem.jsx:41
msgid "Stacking till"
#: src/components/LogInArea.jsx:31
msgid "or login with:"
msgstr ""
#: src/components/CanvasItem.jsx:43
msgid "Ranked"
#: src/components/LogInArea.jsx:72
msgid "or register here:"
msgstr ""
#: src/components/CanvasItem.jsx:45
msgid "Yes"
msgstr ""
#: src/components/CanvasItem.jsx:45
msgid "No"
msgstr ""
#: src/components/CanvasItem.jsx:51
msgid "Requirements"
msgstr ""
#: src/components/CanvasItem.jsx:54
msgid "User Account"
msgstr ""
#: src/components/CanvasItem.jsx:56
#, javascript-format
msgid "and ${ canvas.req } Pixels set"
msgstr ""
#: src/components/CanvasItem.jsx:59
msgid "Top 10 Daily Ranking"
msgstr ""
#: src/components/CanvasItem.jsx:65
msgid "Dimensions"
msgstr ""
#: src/components/LogInForm.jsx:76
msgid "Name or Email"
msgstr ""
#: src/components/LogInForm.jsx:87
msgid "LogIn"
#: src/components/LogInArea.jsx:79
msgid "Register"
msgstr ""
#: src/components/UserMessages.jsx:28
@ -1177,6 +1169,10 @@ msgstr ""
msgid "Confirm New Password"
msgstr ""
#: src/components/ChangeName.jsx:64
msgid "New Username"
msgstr ""
#: src/components/ChangeMail.jsx:59
msgid ""
"Changed Mail successfully. We sent you a verification mail, "
@ -1187,14 +1183,6 @@ msgstr ""
msgid "New Mail"
msgstr ""
#: src/components/ChangeName.jsx:64
msgid "New Username"
msgstr ""
#: src/components/DeleteAccount.jsx:66
msgid "Yes, Delete My Account!"
msgstr ""
#: src/components/SocialSettings.jsx:38
msgid "Block all Private Messages"
msgstr ""
@ -1207,6 +1195,18 @@ msgstr ""
msgid "You have no users blocked"
msgstr ""
#: src/components/DeleteAccount.jsx:66
msgid "Yes, Delete My Account!"
msgstr ""
#: src/components/LogInForm.jsx:76
msgid "Name or Email"
msgstr ""
#: src/components/LogInForm.jsx:87
msgid "LogIn"
msgstr ""
#: src/components/windows/Help.jsx:14
#: src/components/windows/Settings.jsx:134
msgctxt "keybinds"

BIN
public/embico/direct.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

BIN
public/embico/matrix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

BIN
public/embico/tiktok.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

BIN
public/embico/youtube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

View File

@ -760,6 +760,16 @@ export function addToChatInputMessage(windowId, msg) {
};
}
export function addToChatInputMessageAndFocus(windowId, msg) {
return (dispatch) => {
dispatch(addToChatInputMessage(windowId, msg));
const inputElem = document.getElementById(`chtipt-${windowId}`);
if (inputElem) {
inputElem.focus();
}
};
}
export function closeWindow(windowId) {
return {
type: 'CLOSE_WINDOW',

View File

@ -3,25 +3,29 @@
* @flow
*/
import React from 'react';
import { useDispatch } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
import { showContextMenu } from '../actions';
import { MarkdownParagraph } from './Markdown';
import { colorFromText, setBrightness } from '../core/utils';
import { parseParagraph } from '../core/MarkdownParser';
function ChatMessage({
name,
uid,
country,
dark,
windowId,
msgArray,
msg,
}) {
if (!name || !msgArray) {
if (!name) {
return null;
}
const dispatch = useDispatch();
const isDarkMode = useSelector(
(state) => state.gui.style.indexOf('dark') !== -1,
);
const isInfo = (name === 'info');
const isEvent = (name === 'event');
@ -30,90 +34,61 @@ function ChatMessage({
className += ' info';
} else if (isEvent) {
className += ' event';
} else if (msgArray[0][1].charAt(0) === '>') {
} else if (msg.charAt(0) === '>') {
className += ' greentext';
} else if (msg.charAt(0) === '<') {
className += ' redtext';
}
const pArray = parseParagraph(msg);
return (
<p className="chatmsg">
{
<div className="chatmsg">
<div className="chatname">
{
(!isInfo && !isEvent)
&& (
<span>
<img
alt=""
title={country}
src={`${window.ssv.assetserver}/cf/${country}.gif`}
onError={(e) => {
e.target.onerror = null;
e.target.src = './cf/xx.gif';
}}
/>
&nbsp;
<span
className="chatname"
style={{
color: setBrightness(colorFromText(name), dark),
cursor: 'pointer',
}}
role="button"
tabIndex={-1}
onClick={(event) => {
const {
clientX,
clientY,
} = event;
dispatch(showContextMenu('USER', clientX, clientY, {
windowId,
uid,
name,
}));
}}
>
{name}
</span>
:&nbsp;
</span>
<>
<img
alt=""
title={country}
src={`${window.ssv.assetserver}/cf/${country}.gif`}
onError={(e) => {
e.target.onerror = null;
e.target.src = './cf/xx.gif';
}}
/>
&nbsp;
<span
style={{
color: setBrightness(colorFromText(name), isDarkMode),
cursor: 'pointer',
}}
role="button"
tabIndex={-1}
onClick={(event) => {
const {
clientX,
clientY,
} = event;
dispatch(showContextMenu('USER', clientX, clientY, {
windowId,
uid,
name,
}));
}}
>
{name}
</span>
:&nbsp;
</>
)
}
{
msgArray.map((msgPart) => {
const [type, txt] = msgPart;
if (type === 't') {
return (<span className={className}>{txt}</span>);
} if (type === 'c') {
return (<a href={`./${txt}`}>{txt}</a>);
} if (type === 'l') {
return (
<a
href={txt}
target="_blank"
rel="noopener noreferrer"
>{txt}</a>
);
} if (type === 'p') {
return (
<span
className="ping"
style={{
color: setBrightness(colorFromText(txt.substring(1)), dark),
}}
>{txt}</span>
);
} if (type === 'm') {
return (
<span
className="mention"
style={{
color: setBrightness(colorFromText(txt.substring(1)), dark),
}}
>{txt}</span>
);
}
return null;
})
}
</p>
</div>
<span className={className}>
<MarkdownParagraph pArray={pArray} />
</span>
</div>
);
}

148
src/components/Markdown.jsx Normal file
View File

@ -0,0 +1,148 @@
/*
* Renders Markdown that got parsed by core/MarkdownParser
*/
import React from 'react';
import MdLink from './MdLink';
import MdMention from './MdMention';
// eslint-disable-next-line max-len
export const MarkdownParagraph = React.memo(({ pArray }) => pArray.map((part) => {
if (!Array.isArray(part)) {
return part;
}
const type = part[0];
switch (type) {
case 'c':
return (<code>{part[1]}</code>);
case '*':
return (
<strong>
<MarkdownParagraph pArray={part[1]} />
</strong>
);
case '~':
return (
<s>
<MarkdownParagraph pArray={part[1]} />
</s>
);
case '+':
return (
<em>
<MarkdownParagraph pArray={part[1]} />
</em>
);
case '_':
return (
<u>
<MarkdownParagraph pArray={part[1]} />
</u>
);
case 'img':
case 'l': {
return (
<MdLink href={part[2]} title={part[1]} />
);
}
case '@': {
return (
<MdMention uid={part[2]} name={part[1]} />
);
}
default:
return type;
}
}));
const Markdown = ({ mdArray }) => mdArray.map((part) => {
const type = part[0];
switch (type) {
/* Heading */
case 'a': {
const level = Number(part[1]);
const heading = part[2];
const children = part[3];
let headingElem = [];
switch (level) {
case 1:
headingElem = <h1>{heading}</h1>;
break;
case 2:
headingElem = <h2>{heading}</h2>;
break;
case 3:
headingElem = <h3>{heading}</h3>;
break;
default:
headingElem = <h4>{heading}</h4>;
}
return (
<>
{headingElem}
<section>
<Markdown mdArray={children} />
</section>
</>
);
}
/* Paragraph */
case 'p': {
return (
<p>
<MarkdownParagraph pArray={part[1]} />
</p>
);
}
/* Code Block */
case 'cb': {
const content = part[1];
return <pre>{content}</pre>;
}
case '>':
case '<': {
const children = part[1];
return (
<blockquote
className={(type === '>') ? 'gt' : 'rt'}
>
<Markdown mdArray={children} />
</blockquote>
);
}
case 'ul': {
const children = part[1];
return (
<ul>
<Markdown mdArray={children} />
</ul>
);
}
case 'ol': {
const children = part[1];
return (
<ol>
<Markdown mdArray={children} />
</ol>
);
}
case '-': {
const children = part[1];
return (
<li>
<Markdown mdArray={children} />
</li>
);
}
default:
return part[0];
}
});
const MarkdownArticle = ({ mdArray }) => (
<article>
<Markdown mdArray={mdArray} />
</article>
);
export default React.memo(MarkdownArticle);

94
src/components/MdLink.jsx Normal file
View File

@ -0,0 +1,94 @@
/*
* Renders a markdown link
* Also provides previews
* Links are assumed to start with protocol (http:// etc.)
*/
import React, { useState } from 'react';
import { HiArrowsExpand, HiStop } from 'react-icons/hi';
import { getLinkDesc } from '../core/utils';
import EMBEDS from './embeds';
const titleAllowed = [
'odysee',
'twitter',
'matrix.pixelplanet.fun',
'youtube',
'youtu.be',
];
const MdLink = ({ href, title }) => {
const [showEmbed, setShowEmbed] = useState(false);
const desc = getLinkDesc(href);
// treat pixelplanet links seperately
if (desc === window.location.hostname && href.includes('/#')) {
const coords = href.substring(href.indexOf('/#') + 1);
return (
<a href={`./${coords}`}>{title || coords}</a>
);
}
const embedObj = EMBEDS[desc];
const embedAvailable = embedObj && embedObj[1](href);
const Embed = embedObj && embedObj[0];
let parsedTitle;
if (title && titleAllowed.includes(desc)) {
parsedTitle = title;
} else if (embedAvailable && embedObj[2]) {
parsedTitle = embedObj[2](href);
} else {
parsedTitle = href;
}
return (
<>
<a
href={href}
target="_blank"
rel="noopener noreferrer"
>
{parsedTitle}
</a>
{(embedAvailable) && (
<>
&nbsp;
{(embedObj[3])
&& (
<img
style={{
width: '1em',
height: '1em',
verticalAlign: 'middle',
}}
src={embedObj[3]}
alt={`${desc}-icon`}
/>
)}
<span
style={{ cursor: 'pointer' }}
onClick={() => setShowEmbed(!showEmbed)}
>
{(showEmbed)
? (
<HiStop
style={{ verticalAlign: 'middle', color: 'red' }}
/>
)
: (
<HiArrowsExpand
style={{ verticalAlign: 'middle', color: '#4646ff' }}
/>
)}
</span>
</>
)}
{(showEmbed && embedAvailable) && <Embed url={href} />}
</>
);
};
export default React.memo(MdLink);

View File

@ -0,0 +1,30 @@
/*
* Parse Mention of Username
*/
import React from 'react';
import { useSelector } from 'react-redux';
import { colorFromText, setBrightness } from '../core/utils';
const MdMention = ({ name, uid }) => {
const id = uid && uid.trim();
const isDarkMode = useSelector(
(state) => state.gui.style.indexOf('dark') !== -1,
);
const ownId = useSelector((state) => state.user.id);
return (
<span
className={
// eslint-disable-next-line eqeqeq
(id == ownId) ? 'ping' : 'mention'
}
style={{
color: setBrightness(colorFromText(name), isDarkMode),
}}
>{`@${name}`}</span>
);
};
export default React.memo(MdMention);

View File

@ -12,7 +12,7 @@ import {
} from '../hooks/clickOutside';
import {
hideContextMenu,
addToChatInputMessage,
addToChatInputMessageAndFocus,
startDm,
setUserBlock,
setChatChannel,
@ -45,7 +45,9 @@ const UserContextMenu = () => {
role="button"
tabIndex={0}
onClick={() => {
dispatch(addToChatInputMessage(windowId, `@${name} `));
dispatch(
addToChatInputMessageAndFocus(windowId, `@[${name}](${uid}) `),
);
close();
}}
style={{ borderTop: 'none' }}

View File

@ -0,0 +1,52 @@
/* eslint-disable jsx-a11y/media-has-caption */
import React from 'react';
import { getExt } from '../../core/utils';
const videoExts = [
'webm',
'mp4',
];
const imageExts = [
'jpg',
'jpeg',
'png',
'webp',
'gif',
];
const DirectLinkMedia = ({ url }) => {
const ext = getExt(url);
if (videoExts.includes(ext)) {
return (
<div className="vemb">
<video
className="vembc"
controls
autoPlay
src={url}
referrerPolicy="no-referrer"
/>
</div>
);
}
return (
<img
alt={`Matrix ${url}`}
src={url}
style={{ maxWidth: '100%' }}
referrerPolicy="no-referrer"
/>
);
};
export default [
React.memo(DirectLinkMedia),
(url) => {
const ext = getExt(url);
return (videoExts.includes(ext) || imageExts.includes(ext));
},
null,
`${window.ssv.assetserver}/embico/direct.png`,
];

View File

@ -0,0 +1,33 @@
/* eslint-disable jsx-a11y/media-has-caption */
import React from 'react';
const Matrix = ({ url }) => {
const cleanUrl = url.substring(0, url.indexOf('?type='));
if (url.includes('?type=video')) {
return (
<div className="vemb">
<video
className="vembc"
controls
autoPlay
src={cleanUrl}
/>
</div>
);
}
return (
<img
alt={`Matrix ${cleanUrl}`}
src={cleanUrl}
style={{ maxWidth: '100%' }}
/>
);
};
export default [
React.memo(Matrix),
(url) => url.includes('?type=video') || url.includes('?type=image'),
null,
`${window.ssv.assetserver}/embico/matrix.png`,
];

View File

@ -0,0 +1,59 @@
import React, { useState, useEffect } from 'react';
function getUserFromUrl(url) {
let aPos = url.indexOf('/@');
if (aPos === -1) {
return url;
}
aPos += 1;
let bPos = url.indexOf('/', aPos);
if (bPos === -1) {
bPos = url.length;
}
return url.substring(aPos, bPos);
}
const TikTok = ({ url }) => {
const [embedCode, setEmbedCode] = useState(null);
useEffect(async () => {
const prot = window.location.protocol.startsWith('http')
? window.location.protocol : 'https';
// eslint-disable-next-line max-len
const tkurl = `${prot}//www.tiktok.com/oembed?url=${encodeURIComponent(url)}`;
const resp = await fetch(tkurl);
const embedData = await resp.json();
if (embedData.html) {
setEmbedCode(embedData.html);
}
}, []);
if (!embedCode) {
return <div>LOADING</div>;
}
return (
<iframe
style={{
width: '100%',
height: 756,
}}
srcDoc={embedCode}
frameBorder="0"
referrerPolicy="no-referrer"
allow="autoplay; picture-in-picture"
scrolling="no"
// eslint-disable-next-line max-len
sandbox="allow-scripts allow-modals allow-forms allow-popups allow-same-origin allow-presentation"
allowFullScreen
title="Embedded youtube"
/>
);
};
export default [
React.memo(TikTok),
(url) => url.includes('/video/'),
(url) => getUserFromUrl(url),
`${window.ssv.assetserver}/embico/tiktok.png`,
];

View File

@ -0,0 +1,49 @@
import React from 'react';
function getIdFromURL(url) {
let vPos = -1;
if (url.includes('youtube')) {
vPos = url.indexOf('v=');
}
if (url.includes('youtu.be')) {
vPos = url.indexOf('e/');
}
if (vPos === -1) {
return null;
}
vPos += 2;
let vEnd;
for (vEnd = vPos;
vEnd < url.length && !['&', '?', '/'].includes(url[vEnd]);
vEnd += 1);
return url.substring(vPos, vEnd);
}
const YouTube = ({ url }) => {
const id = getIdFromURL(url);
if (!id) {
return null;
}
return (
<div className="vemb" style={{ paddingBottom: '56.25%' }}>
<iframe
className="vembc"
src={`https://www.youtube.com/embed/${id}`}
frameBorder="0"
referrerPolicy="no-referrer"
allow="autoplay; picture-in-picture"
// eslint-disable-next-line max-len
sandbox="allow-scripts allow-modals allow-forms allow-popups allow-same-origin allow-presentation"
allowFullScreen
title="Embedded youtube"
/>
</div>
);
};
export default [
React.memo(YouTube),
getIdFromURL,
(url) => getIdFromURL(url),
`${window.ssv.assetserver}/embico/youtube.png`,
];

View File

@ -0,0 +1,28 @@
/*
* Embeds for external content like youtube, etc.
*
*/
import TikTok from './TikTok';
import YouTube from './YouTube';
import Matrix from './Matrix';
import DirectLinkMedia from './DirectLinkMedia';
/*
* key is the domain (with .com and www. stripped)
* value is an Array with
* [
* ReactElement: takes url as prop,
* isEmbedAvailable: function that takes url as argument and returns boolean
* whether embed is available for this url of this domain
* ]
*/
export default {
tiktok: TikTok,
youtube: YouTube,
'youtu.be': YouTube,
'matrix.pixelplanet.fun': Matrix,
'i.4cdn.org': DirectLinkMedia,
'i.imgur': DirectLinkMedia,
'litter.catbox.moe': DirectLinkMedia,
'files.catbox.moe': DirectLinkMedia,
};

View File

@ -22,11 +22,7 @@ import {
setWindowTitle,
} from '../../actions';
import SocketClient from '../../socket/SocketClient';
import splitChatMessage from '../../core/chatMessageFilter';
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
const Chat = ({
windowId,
@ -34,7 +30,6 @@ const Chat = ({
const listRef = useRef();
const targetRef = useRef();
const [nameRegExp, setNameRegExp] = useState(null);
const [blockedIds, setBlockedIds] = useState([]);
const [btnSize, setBtnSize] = useState(20);
@ -46,7 +41,6 @@ const Chat = ({
const ownName = useSelector((state) => state.user.name);
// eslint-disable-next-line max-len
const isDarkMode = useSelector((state) => state.gui.style.indexOf('dark') !== -1);
const fetching = useSelector((state) => state.fetching.fetchingChat);
const { channels, messages, blocked } = useSelector((state) => state.chat);
// eslint-disable-next-line max-len
@ -73,13 +67,6 @@ const Chat = ({
stayScrolled();
}, [channelMessages.length]);
useEffect(() => {
const regExp = (ownName)
? new RegExp(`(^|\\s)(@${escapeRegExp(ownName)})(\\s|$)`, 'g')
: null;
setNameRegExp(regExp);
}, [ownName]);