forked from ppfun/pixelplanet
expand Markdown parsing, add embeds
This commit is contained in:
parent
3eeece6a54
commit
e80f9b4447
|
@ -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',
|
||||
];
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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
BIN
public/embico/direct.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 474 B |
BIN
public/embico/matrix.png
Normal file
BIN
public/embico/matrix.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 359 B |
BIN
public/embico/tiktok.png
Normal file
BIN
public/embico/tiktok.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 575 B |
BIN
public/embico/youtube.png
Normal file
BIN
public/embico/youtube.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 240 B |
|
@ -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',
|
||||
|
|
|
@ -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';
|
||||
}}
|
||||
/>
|
||||
|
||||
<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>
|
||||
:
|
||||
</span>
|
||||
<>
|
||||
<img
|
||||
alt=""
|
||||
title={country}
|
||||
src={`${window.ssv.assetserver}/cf/${country}.gif`}
|
||||
onError={(e) => {
|
||||
e.target.onerror = null;
|
||||
e.target.src = './cf/xx.gif';
|
||||
}}
|
||||
/>
|
||||
|
||||
<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>
|
||||
:
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
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
148
src/components/Markdown.jsx
Normal 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
94
src/components/MdLink.jsx
Normal 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) && (
|
||||
<>
|
||||
|
||||
{(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);
|
30
src/components/MdMention.jsx
Normal file
30
src/components/MdMention.jsx
Normal 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);
|
|
@ -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' }}
|
||||
|
|
52
src/components/embeds/DirectLinkMedia.jsx
Normal file
52
src/components/embeds/DirectLinkMedia.jsx
Normal 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`,
|
||||
];
|
33
src/components/embeds/Matrix.jsx
Normal file
33
src/components/embeds/Matrix.jsx
Normal 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`,
|
||||
];
|
59
src/components/embeds/TikTok.jsx
Normal file
59
src/components/embeds/TikTok.jsx
Normal 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`,
|
||||
];
|
49
src/components/embeds/YouTube.jsx
Normal file
49
src/components/embeds/YouTube.jsx
Normal 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`,
|
||||
];
|
28
src/components/embeds/index.js
Normal file
28
src/components/embeds/index.js
Normal 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,
|
||||
};
|
|
@ -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),
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
/*
|
||||
* @flow
|
||||
*/
|
||||
import { t } from 'ttag';
|
||||
|
||||
import Help from './Help';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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);
|
24
utils/markdown-test/MdMention.jsx
Normal file
24
utils/markdown-test/MdMention.jsx
Normal 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);
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue
Block a user