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]);
useEffect(() => {
setTimeout(() => {
const fontSize = Math.round(targetRef.current.offsetHeight / 10);
@ -97,10 +84,10 @@ const Chat = ({
function handleSubmit(e) {
e.preventDefault();
const msg = inputMessage.trim();
if (!msg) return;
const inptMsg = inputMessage.trim();
if (!inptMsg) return;
// send message via websocket
SocketClient.sendChatMessage(msg, chatChannel);
SocketClient.sendChatMessage(inptMsg, chatChannel);
dispatch(setChatInputMessage(windowId, ''));
}
@ -157,11 +144,10 @@ const Chat = ({
(!channelMessages.length)
&& (
<ChatMessage
name="info"
msgArray={splitChatMessage(t`Start chatting here`, nameRegExp)}
country="xx"
uid={0}
dark={isDarkMode}
name="info"
country="xx"
msg={t`Start chatting here`}
windowId={windowId}
/>
)
@ -170,11 +156,10 @@ const Chat = ({
channelMessages.map((message) => ((blockedIds.includes(message[3]))
? null : (
<ChatMessage
name={message[0]}
msgArray={splitChatMessage(message[1], nameRegExp)}
country={message[2]}
uid={message[3]}
dark={isDarkMode}
name={message[0]}
country={message[2]}
msg={message[1]}
windowId={windowId}
/>
)))
@ -188,6 +173,7 @@ const Chat = ({
>
<input
style={{ flexGrow: 1, minWidth: 40 }}
id={`chtipt-${windowId}`}
value={inputMessage}
onChange={(e) => dispatch(
setChatInputMessage(windowId, e.target.value),

View File

@ -1,6 +1,3 @@
/*
* @flow
*/
import { t } from 'ttag';
import Help from './Help';

View File

@ -20,6 +20,14 @@ import {
APISOCKET_USER_NAME,
} from './constants';
function getUserFromMd(mdUserLink) {
const mdUser = mdUserLink.trim();
if (mdUser[0] === '[' && mdUser[mdUser.length - 1] === ')') {
return mdUser.substring(1, mdUser.lastIndexOf(']')).trim();
}
return mdUser;
}
export class ChatProvider {
constructor() {
this.defaultChannels = {};
@ -262,21 +270,30 @@ export class ChatProvider {
case 'mute': {
const timeMin = Number(args.slice(-1));
if (Number.isNaN(timeMin)) {
return this.mute(args.join(' '), channelId);
return this.mute(
getUserFromMd(args.join(' ')),
channelId,
);
}
return this.mute(
args.slice(0, -1).join(' '),
getUserFromMd(args.slice(0, -1).join(' ')),
channelId,
timeMin,
);
}
case 'unmute':
return this.unmute(args.join(' '), channelId);
return this.unmute(
getUserFromMd(args.join(' ')),
channelId,
);
case 'mutec': {
if (args[0]) {
const cc = args[0].toLowerCase();
if (cc.length > 3) {
return 'No legit country defined';
}
this.mutedCountries.push(cc);
this.broadcastChatMessage(
'info',

View File

@ -212,4 +212,56 @@ export default class MString {
this.iter = cIter;
return link;
}
/*
* Check if current '#' is part of ppfun coordinates (example: #d,23,11,-10)
* @return null if not coords, otherwise the coords string
*/
checkIfCoords() {
let cIter = this.iter + 1;
while (cIter < this.txt.length) {
const chr = this.txt[cIter];
if (chr === ',') {
break;
}
if (MString.isWhiteSpace(chr)
|| !Number.isNaN(Number(chr))
) {
return null;
}
cIter += 1;
}
if (cIter >= this.txt.length
|| cIter - this.iter > 12
|| cIter === this.iter
) {
return null;
}
cIter += 1;
const curChr = this.txt[cIter];
if (curChr !== '-' && Number.isNaN(curChr)) {
return null;
}
cIter += 1;
let sectCount = 1;
while (cIter < this.txt.length && !MString.isWhiteSpace(this.txt[cIter])) {
const chr = this.txt[cIter];
if (chr === ',') {
sectCount += 1;
} else if (chr !== '-' && Number.isNaN(Number(chr))) {
return null;
}
cIter += 1;
}
if (sectCount < 2
|| sectCount > 3
|| this.txt[cIter - 1] === ','
) {
return null;
}
const coords = this.txt.slice(this.iter, cIter);
this.iter = cIter;
return coords;
}
}

View File

@ -5,8 +5,6 @@
* stuff like pixelplanet coords and usernames and bare links.
* This code is written in preparation for a possible imporementation in
* WebAssambly, so it's all in a big loop
*
* @flow
*/
import MString from './MString';
@ -41,6 +39,19 @@ function parseMParagraph(text, opts, breakChar) {
}
pStart = text.iter + 1;
text.moveForward();
} else if (chr === '#') {
/*
* ppfun coords #d,34,23,-10
*/
const oldPos = text.iter;
const coords = text.checkIfCoords();
if (coords) {
if (pStart !== oldPos) {
pArray.push(text.slice(pStart, oldPos));
}
pArray.push(['l', null, `${window.location.origin}/${coords}`]);
pStart = text.iter;
}
} else if (paraElems.includes(chr)) {
/*
* bold, cursive, underline, etc.
@ -100,10 +111,14 @@ function parseMParagraph(text, opts, breakChar) {
* defaults to ordinary link
*/
let tag = 'l';
const zIsLink = true;
let zIsLink = true;
if (x === '!') {
tag = 'img';
oldPos -= 1;
} else if (x === '@') {
zIsLink = false;
tag = '@';
oldPos -= 1;
}
const encArr = text.checkIfEnclosure(zIsLink);
@ -174,8 +189,8 @@ function parseQuote(text, opts) {
* or ident is smaller than given
*/
function parseMSection(
text: string,
opts: Object,
text,
opts,
headingLevel,
indent,
) {
@ -351,13 +366,13 @@ function parseOpts(inOpts) {
return opts;
}
export function parseParagraph(text: string, inOpts) {
export function parseParagraph(text, inOpts) {
const opts = parseOpts(inOpts);
const mText = new MString(text);
return parseMParagraph(mText, opts);
}
export function parse(text: string, inOpts) {
export function parse(text, inOpts) {
const opts = parseOpts(inOpts);
const mText = new MString(text);
return parseMText(mText, opts, 0);

View File

@ -1,64 +0,0 @@
/*
*
* @flow
*/
/*
* splits chat message into array of what it represents
* [[type, text],[type, text], ...]
* type:
* 'l': external link
* 't': text
* 'p': ping
* 'c': coordinates or ppfun link
* 'm': mention of somebody else
* nameRegExp has to be in the form of:
new RegExp(`(^|\\s+)(@${ownName})(\\s+|$)`, 'g');
*/
// eslint-disable-next-line
const linkRegExp = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi;
const ppLinkRegExp = /(#[a-z]*,-?[0-9]*,-?[0-9]*(,-?[0-9]+)?)/gi;
const linkRegExpFilter = (val, ind) => ((ind % 3) !== 2);
const mentionRegExp = /(^|\s)(@\S+)/g;
const spaceFilter = (val, ind) => (val !== ' ' && (ind !== 0 | val !== ''));
function splitChatMessageRegexp(
msgArray,
regExp,
ident,
filter = () => true,
) {
return msgArray.map((msgPart) => {
const [type, part] = msgPart;
if (type !== 't') {
return [msgPart];
}
return part
.split(regExp)
.filter(filter)
.map((stri, i) => {
if (i % 2 === 0) {
return ['t', stri];
}
return [ident, stri];
})
.filter((el) => !!el[1]);
}).flat(1);
}
function splitChatMessage(message, nameRegExp = null) {
if (!message) {
return null;
}
let arr = [['t', message.trim()]];
arr = splitChatMessageRegexp(arr, ppLinkRegExp, 'c', linkRegExpFilter);
if (nameRegExp) {
arr = splitChatMessageRegexp(arr, nameRegExp, 'p', spaceFilter);
}
arr = splitChatMessageRegexp(arr, linkRegExp, 'l', linkRegExpFilter);
arr = splitChatMessageRegexp(arr, mentionRegExp, 'm', spaceFilter);
return arr;
}
export default splitChatMessage;

View File

@ -307,20 +307,10 @@ export function setBrightness(hex, dark: boolean = false) {
* @param string input string
* @return escaped string
*/
function escapeRegExp(string) {
export function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/*
* create RegExp to search for ping in chat messages
* @param name name
* @return regular expression to search for name in message
*/
export function createNameRegExp(name) {
if (!name) return null;
return new RegExp(`(^|\\s+)(@${escapeRegExp(name)})(\\s+|$)`, 'g');
}
/*
* check if webGL2 is available
* @return boolean true if available
@ -333,3 +323,54 @@ export function isWebGL2Available() {
return false;
}
}
/*
* gets a descriptive text of the domain of the link
* Example:
* https://www.youtube.com/watch?v=G8APgeFfkAk returns 'youtube'
* http://www.google.at returns 'google.at'
* (www. and .com are split)
*/
export function getLinkDesc(link) {
let domainStart = link.indexOf('://') + 3;
if (domainStart < 3) {
domainStart = 0;
}
if (link.startsWith('www.', domainStart)) {
domainStart += 4;
}
let domainEnd = link.indexOf('/', domainStart);
if (domainEnd === -1) {
domainEnd = link.length;
}
if (link.endsWith('.com', domainEnd)) {
domainEnd -= 4;
}
if (domainEnd <= domainStart) {
return link;
}
return link.slice(domainStart, domainEnd);
}
/*
* try to get extension out of link
* @param link url
* @return extension or null if not available
*/
export function getExt(link) {
let paramStart = link.indexOf('&');
if (paramStart === -1) {
paramStart = link.length;
}
let posDot = paramStart - 1;
for (;posDot >= 0 && link[posDot] !== '.'; posDot -= 1) {
if (link[posDot] === '/') {
return null;
}
}
posDot += 1;
if (paramStart - posDot > 4) {
return null;
}
return link.slice(posDot, paramStart);
}

View File

@ -219,8 +219,21 @@ class User {
}
getUserData(): Object {
const {
id,
userlvl,
channels,
blocked,
} = this;
const data = {
id,
userlvl,
channels,
blocked,
};
if (this.regUser == null) {
return {
...data,
name: null,
mailVerified: false,
blockDm: false,
@ -229,15 +242,11 @@ class User {
ranking: null,
dailyRanking: null,
mailreg: false,
userlvl: 0,
channels: this.channels,
blocked: this.blocked,
};
}
const {
regUser, userlvl, channels, blocked,
} = this;
const { regUser } = this;
return {
...data,
name: regUser.name,
mailVerified: regUser.mailVerified,
blockDm: regUser.blockDm,
@ -246,9 +255,6 @@ class User {
ranking: regUser.ranking,
dailyRanking: regUser.dailyRanking,
mailreg: !!(regUser.password),
userlvl,
channels,
blocked,
};
}
}

View File

@ -1,8 +1,6 @@
import { createNameRegExp } from '../core/utils';
const initialState = {
id: null,
name: null,
center: [0, 0],
wait: null,
coolDown: null, // ms
lastCoolDownEnd: null,
@ -18,8 +16,6 @@ const initialState = {
notification: null,
// 1: Admin, 2: Mod, 0: ordinary user
userlvl: 0,
// regExp for detecting ping
nameRegExp: null,
};
export default function user(
@ -86,43 +82,41 @@ export default function user(
case 'RECEIVE_ME':
case 'LOGIN': {
const {
id,
name,
mailreg,
blockDm,
userlvl,
} = action;
const nameRegExp = createNameRegExp(name);
const messages = (action.messages) ? action.messages : [];
return {
...state,
id,
name,
messages,
mailreg,
blockDm,
userlvl,
nameRegExp,
};
}
case 'LOGOUT': {
return {
...state,
id: null,
name: null,
messages: [],
mailreg: false,
blockDm: false,
userlvl: 0,
nameRegExp: null,
};
}
case 'SET_NAME': {
const { name } = action;
const nameRegExp = createNameRegExp(name);
return {
...state,
name,
nameRegExp,
};
}

View File

@ -665,17 +665,20 @@ tr:nth-child(even) {
white-space: nowrap;
}
.chatname {
color: #4B0000;
font-size: 13px;
white-space: nowrap;
display: inline-block;
}
.chatmsg {
color: #030303;
font-size: 13px;
user-select: text;
margin: 0;
display: flex;
flex-direction: row;
}
.chatmsg > span {
display: inline-block;
.msg {
flex: 1;
}
.msg.info {
color: #cc0000;
@ -686,6 +689,9 @@ tr:nth-child(even) {
.msg.greentext{
color: green;
}
.msg.redtext{
color: red;
}
.ping {
background-color: #ffff87;
border-style: solid;
@ -905,3 +911,18 @@ tr:nth-child(even) {
.Modal.show, .Alert.show, .OverlayAlert.show, .OverlayModal.show, .window.show {
opacity: 1;
}
.vemb {
overflow: hidden;
position: relative;
height: 0;
padding-bottom: 66%;
}
.vembc {
left: 0;
top: 0;
height: 100%;
width: 100%;
position: absolute;
}

View File

@ -3,7 +3,10 @@
*/
import React from 'react';
const MarkdownParagraph = ({ pArray }) => pArray.map((part) => {
import MdLink from '../../src/components/MdLink';
import MdMention from './MdMention';
export const MarkdownParagraph = React.memo(({ pArray }) => pArray.map((part) => {
if (!Array.isArray(part)) {
return part;
}
@ -14,51 +17,42 @@ const MarkdownParagraph = ({ pArray }) => pArray.map((part) => {
case '*':
return (
<strong>
<MarkdownParagraph pArray={part[1]} />
<MarkdownParagraph pArray={part[1]} />
</strong>
);
case '~':
return (
<s>
<MarkdownParagraph pArray={part[1]} />
<MarkdownParagraph pArray={part[1]} />
</s>
);
case '+':
return (
<em>
<MarkdownParagraph pArray={part[1]} />
<MarkdownParagraph pArray={part[1]} />
</em>
);
case '_':
return (
<u>
<MarkdownParagraph pArray={part[1]} />
<MarkdownParagraph pArray={part[1]} />
</u>
);
case 'img':
case 'l': {
let title = getLinkDesc(part[2]);
if (part[1]) {
title += ` | ${part[1]}`;
}
return (
<a href={part[2]}>
{title}
</a>
<MdLink href={part[2]} title={part[1]} />
);
}
case 'img': {
let title = getLinkDesc(part[2]);
if (part[1]) {
title += ` | ${part[1]}`;
}
case '@': {
return (
<img src={part[2]} title={part[1] || title} alt={title} />
<MdMention uid={part[2]} name={part[1]} />
);
}
default:
return type;
}
});
}));
const Markdown = ({ mdArray }) => mdArray.map((part) => {
const type = part[0];
@ -86,7 +80,7 @@ const Markdown = ({ mdArray }) => mdArray.map((part) => {
headingElem,
<section>
<Markdown mdArray={children} />
</section>
</section>,
];
}
/* Paragraph */

View File

@ -1,61 +0,0 @@
/*
* Renders a markdown link
* Also provides previews
* Links are assumed to start with protocol (http:// etc.)
*/
import React from 'react';
/*
* gets a descriptive text of the domain of the link
* Example:
* https://www.youtube.com/watch?v=G8APgeFfkAk returns 'youtube'
* http://www.google.at returns 'google.at'
* (www. and .com are split)
*/
function getLinkDesc(link) {
let domainStart = link.indexOf('://') + 3;
if (domainStart < 3) {
domainStart = 0;
}
if (link.startsWith('www.', domainStart)) {
domainStart += 4;
}
let domainEnd = link.indexOf('/', domainStart);
if (domainEnd === -1) {
domainEnd = link.length;
}
if (link.endsWith('.com', domainEnd)) {
domainEnd -= 4;
}
if (domainEnd <= domainStart) {
return link;
}
return link.slice(domainStart, domainEnd);
}
/*
* try to get extension out of link
*/
function getExt(link) {
let paramStart = link.indexOf('&');
if (paramStart === -1) {
paramStart = link.length;
}
let posDot = paramStart - 1;
for (;posDot >= 0 && link[posDot] !== '.'; posDot -= 1) {
if (link[posDot] === '/') {
return null;
}
}
if (paramStart - posDot > 4) {
return null;
}
return link.slice(posDot, paramStart);
}
const MdLink = ({ href, title, type }) => {
const desc = getLinkDesc(href);
<div className="link">
};
export default Reace.memo(MdLink);

View File

@ -0,0 +1,24 @@
/*
* Parse Mention of Username
*/
import React from 'react';
import { colorFromText, setBrightness } from '../../src/core/utils';
const dark = false;
const ownid = 1;
const MdMention = ({ name, uid }) => {
const id = uid && uid.trim();
return (
<span
className={(id == ownid) ? "ping" : "mention"}
style={{
color: setBrightness(colorFromText(name), dark),
}}
>{name}</span>
);
}
export default React.memo(MdMention);

View File

@ -23,3 +23,18 @@ pre {
.rt {
color: red;
}
.vemb {
overflow: hidden;
position: relative;
height: 0;
padding-bottom: 66%;
}
.vembc {
left: 0;
top: 0;
height: 100%;
width: 100%;
position: absolute;
}

View File

@ -7,6 +7,7 @@
<meta name="description" content="Testing Markdown Parser">
<meta name="author" content="hf">
<link rel="stylesheet" href="index.css">
<script>window.ssv={assetserver:'http://dev.pixelplanet.fun'}</script>
</head>
<body>

View File

@ -1,24 +1,6 @@
var path = require('path');
var webpack = require('webpack');
var babelPlugins = [
'@babel/plugin-transform-flow-strip-types',
['@babel/plugin-proposal-decorators', { legacy: true }],
'@babel/plugin-proposal-function-sent',
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-proposal-numeric-separator',
'@babel/plugin-proposal-throw-expressions',
['@babel/plugin-proposal-class-properties', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
'@babel/proposal-object-rest-spread',
// react-optimize
'@babel/transform-react-constant-elements',
'@babel/transform-react-inline-elements',
'transform-react-remove-prop-types',
'transform-react-pure-class-to-function',
];
module.exports = {
name: 'script',
target: 'web',
@ -41,16 +23,7 @@ module.exports = {
test: /\.(js|jsx)$/,
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: {
browsers: [ 'defaults' ],
},
modules: false,
}],
'@babel/react',
],
plugins: babelPlugins,
rootMode: 'upward-optional',
},
},
],

View File

@ -7,8 +7,6 @@ import webpack from 'webpack';
import AssetsPlugin from 'assets-webpack-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import pkg from './package.json';
/*
* Emit a file with assets paths
*/
@ -120,13 +118,6 @@ export function buildWebpackClientConfig(
))
],
options: {
presets: [
['@babel/preset-env', {
targets: {
browsers: pkg.browserslist,
},
}],
],
plugins: babelPlugins,
},
},

View File

@ -75,14 +75,6 @@ export default ({
],
options: {
cacheDirectory: false,
presets: [
['@babel/preset-env', {
targets: {
node: pkg.engines.node.replace(/^\D+/g, ''),
},
modules: false,
}],
],
plugins: babelPlugins,
},
},