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

View File

@ -83,6 +83,14 @@ msgid ""
"one (Note: you can use those links just once)" "one (Note: you can use those links just once)"
msgstr "" 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 #: src/ssr-components/Globe.jsx:44
msgid "Double click on globe to go back." msgid "Double click on globe to go back."
msgstr "" msgstr ""
@ -99,14 +107,6 @@ msgstr ""
msgid "A 3D globe of our whole map" msgid "A 3D globe of our whole map"
msgstr "" 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 #: src/core/mail.js:65
#, javascript-format #, javascript-format
msgid "" msgid ""
@ -305,6 +305,21 @@ msgstr ""
msgid "Password must be shorter than 60 characters." msgid "Password must be shorter than 60 characters."
msgstr "" 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 #: src/routes/api/auth/register.js:33
msgid "No Captcha given" msgid "No Captcha given"
msgstr "" msgstr ""
@ -325,21 +340,6 @@ msgstr ""
msgid "Failed to establish session after register :(" msgid "Failed to establish session after register :("
msgstr "" 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 #: src/routes/api/auth/logout.js:13
msgid "You are not even logged in." msgid "You are not even logged in."
msgstr "" msgstr ""

View File

@ -3,48 +3,6 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Plural-Forms: nplurals=2; plural=(n!=1);\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 #: src/ui/placePixel.js:53
msgid "Error :(" msgid "Error :("
msgstr "" msgstr ""
@ -146,6 +104,48 @@ msgstr ""
msgid "Error ${ retCode }" msgid "Error ${ retCode }"
msgstr "" 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 #: src/ui/renderer.js:36
msgid "Canvas Error" msgid "Canvas Error"
msgstr "" msgstr ""
@ -170,11 +170,6 @@ msgstr ""
msgid "Look at past Canvases" msgid "Look at past Canvases"
msgstr "" msgstr ""
#: src/components/Converter.jsx:559
#: src/components/CoordinatesBox.jsx:32
msgid "Copy to Clipboard"
msgstr ""
#: src/components/OnlineBox.jsx:41 #: src/components/OnlineBox.jsx:41
msgid "Online Users on Canvas" msgid "Online Users on Canvas"
msgstr "" msgstr ""
@ -187,6 +182,11 @@ msgstr ""
msgid "Pixels placed" msgid "Pixels placed"
msgstr "" msgstr ""
#: src/components/Converter.jsx:559
#: src/components/CoordinatesBox.jsx:32
msgid "Copy to Clipboard"
msgstr ""
#: src/components/ModalRoot.jsx:69 #: src/components/ModalRoot.jsx:69
#: src/components/Modtools.jsx:224 #: src/components/Modtools.jsx:224
#: src/components/Window.jsx:138 #: src/components/Window.jsx:138
@ -198,6 +198,27 @@ msgstr ""
msgid "Restore" msgid "Restore"
msgstr "" 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 #: src/actions/fetch.js:39
msgid "You made too many requests" msgid "You made too many requests"
msgstr "" msgstr ""
@ -227,25 +248,8 @@ msgstr ""
msgid "Server answered with gibberish :(" msgid "Server answered with gibberish :("
msgstr "" msgstr ""
#: src/components/buttons/CanvasSwitchButton.jsx:22 #: src/components/buttons/GlobeButton.jsx:35
#: src/components/windows/index.js:22 msgid "Globe View"
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"
msgstr "" msgstr ""
#: src/components/HistorySelect.jsx:144 #: src/components/HistorySelect.jsx:144
@ -256,6 +260,33 @@ msgstr ""
msgid "Select Date above" msgid "Select Date above"
msgstr "" 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 #: src/components/Window.jsx:117
msgid "Clone" msgid "Clone"
msgstr "" msgstr ""
@ -272,37 +303,6 @@ msgstr ""
msgid "Resize" msgid "Resize"
msgstr "" 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 #: src/components/contextmenus/UserContextMenu.jsx:53
msgid "Ping" msgid "Ping"
msgstr "" msgstr ""
@ -319,19 +319,19 @@ msgstr ""
msgid "Mute" msgid "Mute"
msgstr "" msgstr ""
#: src/components/windows/index.js:19 #: src/components/windows/index.js:16
msgid "Registration" msgid "Registration"
msgstr "" msgstr ""
#: src/components/windows/index.js:20 #: src/components/windows/index.js:17
msgid "Forgot Password" msgid "Forgot Password"
msgstr "" msgstr ""
#: src/components/windows/index.js:21 #: src/components/windows/index.js:18
msgid "Chat" msgid "Chat"
msgstr "" msgstr ""
#: src/components/windows/index.js:23 #: src/components/windows/index.js:20
msgid "Canvas Archive" msgid "Canvas Archive"
msgstr "" msgstr ""
@ -538,30 +538,6 @@ msgstr ""
msgid "Credit for the Palette of the Top10 canvas goes to ${ vinikLink }." msgid "Credit for the Palette of the Top10 canvas goes to ${ vinikLink }."
msgstr "" 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 #: src/components/windows/Settings.jsx:133
msgid "Show Grid" msgid "Show Grid"
msgstr "" msgstr ""
@ -655,48 +631,6 @@ msgstr ""
msgid "Select Language" msgid "Select Language"
msgstr "" 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 #: src/components/windows/CanvasSelect.jsx:33
msgid "" msgid ""
"Select the canvas you want to use. Every canvas is unique and has " "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:" msgid "Enter your mail address and we will send you a new password:"
msgstr "" 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 #: src/components/windows/Chat.jsx:146
msgid "Channel settings" msgid "Channel settings"
msgstr "" msgstr ""
@ -765,6 +715,56 @@ msgstr ""
msgid "You must be logged in to chat" msgid "You must be logged in to chat"
msgstr "" 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:50
#: src/components/Captcha.jsx:105 #: src/components/Captcha.jsx:105
msgid "Could not load captcha" msgid "Could not load captcha"
@ -846,28 +846,56 @@ msgstr ""
msgid "Password must be shorter than 60 characters." msgid "Password must be shorter than 60 characters."
msgstr "" msgstr ""
#: src/components/LogInArea.jsx:21 #: src/components/ChangeMail.jsx:91
msgid "Login to access more features and stats." #: src/components/ChangeName.jsx:68
#: src/components/ChangePassword.jsx:110
#: src/components/LanguageSelect.jsx:73
msgid "Save"
msgstr "" msgstr ""
#: src/components/LogInArea.jsx:23 #: src/components/CanvasItem.jsx:30
msgid "Login with Name or Mail:" msgid "Online Users"
msgstr "" msgstr ""
#: src/components/LogInArea.jsx:30 #: src/components/CanvasItem.jsx:35
msgid "I forgot my Password." msgid "Cooldown"
msgstr "" msgstr ""
#: src/components/LogInArea.jsx:31 #: src/components/CanvasItem.jsx:41
msgid "or login with:" msgid "Stacking till"
msgstr "" msgstr ""
#: src/components/LogInArea.jsx:72 #: src/components/CanvasItem.jsx:43
msgid "or register here:" msgid "Ranked"
msgstr "" msgstr ""
#: src/components/LogInArea.jsx:79 #: src/components/CanvasItem.jsx:45
msgid "Register" 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 "" msgstr ""
#: src/components/UserAreaContent.jsx:63 #: src/components/UserAreaContent.jsx:63
@ -1083,64 +1111,28 @@ msgstr ""
msgid "Download Template" msgid "Download Template"
msgstr "" msgstr ""
#: src/components/ChangeMail.jsx:91 #: src/components/LogInArea.jsx:21
#: src/components/ChangeName.jsx:68 msgid "Login to access more features and stats."
#: src/components/ChangePassword.jsx:110
#: src/components/LanguageSelect.jsx:73
msgid "Save"
msgstr "" msgstr ""
#: src/components/CanvasItem.jsx:30 #: src/components/LogInArea.jsx:23
msgid "Online Users" msgid "Login with Name or Mail:"
msgstr "" msgstr ""
#: src/components/CanvasItem.jsx:35 #: src/components/LogInArea.jsx:30
msgid "Cooldown" msgid "I forgot my Password."
msgstr "" msgstr ""
#: src/components/CanvasItem.jsx:41 #: src/components/LogInArea.jsx:31
msgid "Stacking till" msgid "or login with:"
msgstr "" msgstr ""
#: src/components/CanvasItem.jsx:43 #: src/components/LogInArea.jsx:72
msgid "Ranked" msgid "or register here:"
msgstr "" msgstr ""
#: src/components/CanvasItem.jsx:45 #: src/components/LogInArea.jsx:79
msgid "Yes" msgid "Register"
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"
msgstr "" msgstr ""
#: src/components/UserMessages.jsx:28 #: src/components/UserMessages.jsx:28
@ -1177,6 +1169,10 @@ msgstr ""
msgid "Confirm New Password" msgid "Confirm New Password"
msgstr "" msgstr ""
#: src/components/ChangeName.jsx:64
msgid "New Username"
msgstr ""
#: src/components/ChangeMail.jsx:59 #: src/components/ChangeMail.jsx:59
msgid "" msgid ""
"Changed Mail successfully. We sent you a verification mail, " "Changed Mail successfully. We sent you a verification mail, "
@ -1187,14 +1183,6 @@ msgstr ""
msgid "New Mail" msgid "New Mail"
msgstr "" 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 #: src/components/SocialSettings.jsx:38
msgid "Block all Private Messages" msgid "Block all Private Messages"
msgstr "" msgstr ""
@ -1207,6 +1195,18 @@ msgstr ""
msgid "You have no users blocked" msgid "You have no users blocked"
msgstr "" 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/Help.jsx:14
#: src/components/windows/Settings.jsx:134 #: src/components/windows/Settings.jsx:134
msgctxt "keybinds" 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) { export function closeWindow(windowId) {
return { return {
type: 'CLOSE_WINDOW', type: 'CLOSE_WINDOW',

View File

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

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'; } from '../hooks/clickOutside';
import { import {
hideContextMenu, hideContextMenu,
addToChatInputMessage, addToChatInputMessageAndFocus,
startDm, startDm,
setUserBlock, setUserBlock,
setChatChannel, setChatChannel,
@ -45,7 +45,9 @@ const UserContextMenu = () => {
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => { onClick={() => {
dispatch(addToChatInputMessage(windowId, `@${name} `)); dispatch(
addToChatInputMessageAndFocus(windowId, `@[${name}](${uid}) `),
);
close(); close();
}} }}
style={{ borderTop: 'none' }} 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, setWindowTitle,
} from '../../actions'; } from '../../actions';
import SocketClient from '../../socket/SocketClient'; import SocketClient from '../../socket/SocketClient';
import splitChatMessage from '../../core/chatMessageFilter';
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
const Chat = ({ const Chat = ({
windowId, windowId,
@ -34,7 +30,6 @@ const Chat = ({
const listRef = useRef(); const listRef = useRef();
const targetRef = useRef(); const targetRef = useRef();
const [nameRegExp, setNameRegExp] = useState(null);
const [blockedIds, setBlockedIds] = useState([]); const [blockedIds, setBlockedIds] = useState([]);
const [btnSize, setBtnSize] = useState(20); const [btnSize, setBtnSize] = useState(20);
@ -46,7 +41,6 @@ const Chat = ({
const ownName = useSelector((state) => state.user.name); const ownName = useSelector((state) => state.user.name);
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
const isDarkMode = useSelector((state) => state.gui.style.indexOf('dark') !== -1);
const fetching = useSelector((state) => state.fetching.fetchingChat); const fetching = useSelector((state) => state.fetching.fetchingChat);
const { channels, messages, blocked } = useSelector((state) => state.chat); const { channels, messages, blocked } = useSelector((state) => state.chat);
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
@ -73,13 +67,6 @@ const Chat = ({
stayScrolled(); stayScrolled();
}, [channelMessages.length]); }, [channelMessages.length]);
useEffect(() => {
const regExp = (ownName)
? new RegExp(`(^|\\s)(@${escapeRegExp(ownName)})(\\s|$)`, 'g')
: null;
setNameRegExp(regExp);
}, [ownName]);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
const fontSize = Math.round(targetRef.current.offsetHeight / 10); const fontSize = Math.round(targetRef.current.offsetHeight / 10);
@ -97,10 +84,10 @@ const Chat = ({
function handleSubmit(e) { function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
const msg = inputMessage.trim(); const inptMsg = inputMessage.trim();
if (!msg) return; if (!inptMsg) return;
// send message via websocket // send message via websocket
SocketClient.sendChatMessage(msg, chatChannel); SocketClient.sendChatMessage(inptMsg, chatChannel);
dispatch(setChatInputMessage(windowId, '')); dispatch(setChatInputMessage(windowId, ''));
} }
@ -157,11 +144,10 @@ const Chat = ({
(!channelMessages.length) (!channelMessages.length)
&& ( && (
<ChatMessage <ChatMessage
name="info"
msgArray={splitChatMessage(t`Start chatting here`, nameRegExp)}
country="xx"
uid={0} uid={0}
dark={isDarkMode} name="info"
country="xx"
msg={t`Start chatting here`}
windowId={windowId} windowId={windowId}
/> />
) )
@ -170,11 +156,10 @@ const Chat = ({
channelMessages.map((message) => ((blockedIds.includes(message[3])) channelMessages.map((message) => ((blockedIds.includes(message[3]))
? null : ( ? null : (
<ChatMessage <ChatMessage
name={message[0]}
msgArray={splitChatMessage(message[1], nameRegExp)}
country={message[2]}
uid={message[3]} uid={message[3]}
dark={isDarkMode} name={message[0]}
country={message[2]}
msg={message[1]}
windowId={windowId} windowId={windowId}
/> />
))) )))
@ -188,6 +173,7 @@ const Chat = ({
> >
<input <input
style={{ flexGrow: 1, minWidth: 40 }} style={{ flexGrow: 1, minWidth: 40 }}
id={`chtipt-${windowId}`}
value={inputMessage} value={inputMessage}
onChange={(e) => dispatch( onChange={(e) => dispatch(
setChatInputMessage(windowId, e.target.value), setChatInputMessage(windowId, e.target.value),

View File

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

View File

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

View File

@ -212,4 +212,56 @@ export default class MString {
this.iter = cIter; this.iter = cIter;
return link; 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. * stuff like pixelplanet coords and usernames and bare links.
* This code is written in preparation for a possible imporementation in * This code is written in preparation for a possible imporementation in
* WebAssambly, so it's all in a big loop * WebAssambly, so it's all in a big loop
*
* @flow
*/ */
import MString from './MString'; import MString from './MString';
@ -41,6 +39,19 @@ function parseMParagraph(text, opts, breakChar) {
} }
pStart = text.iter + 1; pStart = text.iter + 1;
text.moveForward(); 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)) { } else if (paraElems.includes(chr)) {
/* /*
* bold, cursive, underline, etc. * bold, cursive, underline, etc.
@ -100,10 +111,14 @@ function parseMParagraph(text, opts, breakChar) {
* defaults to ordinary link * defaults to ordinary link
*/ */
let tag = 'l'; let tag = 'l';
const zIsLink = true; let zIsLink = true;
if (x === '!') { if (x === '!') {
tag = 'img'; tag = 'img';
oldPos -= 1; oldPos -= 1;
} else if (x === '@') {
zIsLink = false;
tag = '@';
oldPos -= 1;
} }
const encArr = text.checkIfEnclosure(zIsLink); const encArr = text.checkIfEnclosure(zIsLink);
@ -174,8 +189,8 @@ function parseQuote(text, opts) {
* or ident is smaller than given * or ident is smaller than given
*/ */
function parseMSection( function parseMSection(
text: string, text,
opts: Object, opts,
headingLevel, headingLevel,
indent, indent,
) { ) {
@ -351,13 +366,13 @@ function parseOpts(inOpts) {
return opts; return opts;
} }
export function parseParagraph(text: string, inOpts) { export function parseParagraph(text, inOpts) {
const opts = parseOpts(inOpts); const opts = parseOpts(inOpts);
const mText = new MString(text); const mText = new MString(text);
return parseMParagraph(mText, opts); return parseMParagraph(mText, opts);
} }
export function parse(text: string, inOpts) { export function parse(text, inOpts) {
const opts = parseOpts(inOpts); const opts = parseOpts(inOpts);
const mText = new MString(text); const mText = new MString(text);
return parseMText(mText, opts, 0); 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 * @param string input string
* @return escaped string * @return escaped string
*/ */
function escapeRegExp(string) { export function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched 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 * check if webGL2 is available
* @return boolean true if available * @return boolean true if available
@ -333,3 +323,54 @@ export function isWebGL2Available() {
return false; 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 { getUserData(): Object {
const {
id,
userlvl,
channels,
blocked,
} = this;
const data = {
id,
userlvl,
channels,
blocked,
};
if (this.regUser == null) { if (this.regUser == null) {
return { return {
...data,
name: null, name: null,
mailVerified: false, mailVerified: false,
blockDm: false, blockDm: false,
@ -229,15 +242,11 @@ class User {
ranking: null, ranking: null,
dailyRanking: null, dailyRanking: null,
mailreg: false, mailreg: false,
userlvl: 0,
channels: this.channels,
blocked: this.blocked,
}; };
} }
const { const { regUser } = this;
regUser, userlvl, channels, blocked,
} = this;
return { return {
...data,
name: regUser.name, name: regUser.name,
mailVerified: regUser.mailVerified, mailVerified: regUser.mailVerified,
blockDm: regUser.blockDm, blockDm: regUser.blockDm,
@ -246,9 +255,6 @@ class User {
ranking: regUser.ranking, ranking: regUser.ranking,
dailyRanking: regUser.dailyRanking, dailyRanking: regUser.dailyRanking,
mailreg: !!(regUser.password), mailreg: !!(regUser.password),
userlvl,
channels,
blocked,
}; };
} }
} }

View File

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

View File

@ -665,17 +665,20 @@ tr:nth-child(even) {
white-space: nowrap; white-space: nowrap;
} }
.chatname { .chatname {
color: #4B0000;
font-size: 13px; font-size: 13px;
white-space: nowrap;
display: inline-block;
} }
.chatmsg { .chatmsg {
color: #030303; color: #030303;
font-size: 13px; font-size: 13px;
user-select: text; user-select: text;
margin: 0; margin: 0;
display: flex;
flex-direction: row;
} }
.chatmsg > span { .msg {
display: inline-block; flex: 1;
} }
.msg.info { .msg.info {
color: #cc0000; color: #cc0000;
@ -686,6 +689,9 @@ tr:nth-child(even) {
.msg.greentext{ .msg.greentext{
color: green; color: green;
} }
.msg.redtext{
color: red;
}
.ping { .ping {
background-color: #ffff87; background-color: #ffff87;
border-style: solid; border-style: solid;
@ -905,3 +911,18 @@ tr:nth-child(even) {
.Modal.show, .Alert.show, .OverlayAlert.show, .OverlayModal.show, .window.show { .Modal.show, .Alert.show, .OverlayAlert.show, .OverlayModal.show, .window.show {
opacity: 1; 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'; 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)) { if (!Array.isArray(part)) {
return part; return part;
} }
@ -14,51 +17,42 @@ const MarkdownParagraph = ({ pArray }) => pArray.map((part) => {
case '*': case '*':
return ( return (
<strong> <strong>
<MarkdownParagraph pArray={part[1]} /> <MarkdownParagraph pArray={part[1]} />
</strong> </strong>
); );
case '~': case '~':
return ( return (
<s> <s>
<MarkdownParagraph pArray={part[1]} /> <MarkdownParagraph pArray={part[1]} />
</s> </s>
); );
case '+': case '+':
return ( return (
<em> <em>
<MarkdownParagraph pArray={part[1]} /> <MarkdownParagraph pArray={part[1]} />
</em> </em>
); );
case '_': case '_':
return ( return (
<u> <u>
<MarkdownParagraph pArray={part[1]} /> <MarkdownParagraph pArray={part[1]} />
</u> </u>
); );
case 'img':
case 'l': { case 'l': {
let title = getLinkDesc(part[2]);
if (part[1]) {
title += ` | ${part[1]}`;
}
return ( return (
<a href={part[2]}> <MdLink href={part[2]} title={part[1]} />
{title}
</a>
); );
} }
case 'img': { case '@': {
let title = getLinkDesc(part[2]);
if (part[1]) {
title += ` | ${part[1]}`;
}
return ( return (
<img src={part[2]} title={part[1] || title} alt={title} /> <MdMention uid={part[2]} name={part[1]} />
); );
} }
default: default:
return type; return type;
} }
}); }));
const Markdown = ({ mdArray }) => mdArray.map((part) => { const Markdown = ({ mdArray }) => mdArray.map((part) => {
const type = part[0]; const type = part[0];
@ -86,7 +80,7 @@ const Markdown = ({ mdArray }) => mdArray.map((part) => {
headingElem, headingElem,
<section> <section>
<Markdown mdArray={children} /> <Markdown mdArray={children} />
</section> </section>,
]; ];
} }
/* Paragraph */ /* 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 { .rt {
color: red; 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="description" content="Testing Markdown Parser">
<meta name="author" content="hf"> <meta name="author" content="hf">
<link rel="stylesheet" href="index.css"> <link rel="stylesheet" href="index.css">
<script>window.ssv={assetserver:'http://dev.pixelplanet.fun'}</script>
</head> </head>
<body> <body>

View File

@ -1,24 +1,6 @@
var path = require('path'); var path = require('path');
var webpack = require('webpack'); 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 = { module.exports = {
name: 'script', name: 'script',
target: 'web', target: 'web',
@ -41,16 +23,7 @@ module.exports = {
test: /\.(js|jsx)$/, test: /\.(js|jsx)$/,
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
presets: [ rootMode: 'upward-optional',
['@babel/preset-env', {
targets: {
browsers: [ 'defaults' ],
},
modules: false,
}],
'@babel/react',
],
plugins: babelPlugins,
}, },
}, },
], ],

View File

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

View File

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