Compare commits
1 commit
master
...
message-li
Author | SHA1 | Date | |
---|---|---|---|
fa6ced4565 |
96 changed files with 4169 additions and 3474 deletions
.editorconfig.env.example.gitignoreLICENCEMakefileREADME.mdbuild.js
config
package-lock.jsonpackage.jsonsrc
assproc.js
init.js
mami.css
mami.js
animate.jsargs.js
audio
auth.jsavatar.jsawaitable.jschat
colpick
common.jsconcurrent.jsconman.jscontext.jscontrols
easings.jseeprom
emotes.jsemotes
events.jsflashii.jshtml.jsmain.jsmessages.jsmobile.jsmszauth.jsnotices
parsing.jsproto/sockchat
settings
sidebar
act-clear-backlog.jsxact-collapse-all.jsxact-toggle.jsxpan-settings.jsxpan-uploads.jsxpan-users.jsxsidebar.jsx
sockchat
sound
srle.jsthemes.jstitle.jsui
uniqstr.jsurl.jsusers.jsutility.jsweeb.jsworker.jsxhr.jsproto.js
utils.js
|
@ -1,11 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
22
.env.example
22
.env.example
|
@ -1,22 +0,0 @@
|
|||
# Values contained below are also the defaults, except MAMI_DEBUG.
|
||||
|
||||
# Enable debug mode
|
||||
# Primarily turns off minification of output styles, might also display more info in the console
|
||||
#MAMI_DEBUG=1
|
||||
|
||||
# Application title
|
||||
# Sets the title of the application, might be overridden for the window title by the common settings below once they're loaded
|
||||
#MAMI_TITLE="Flashii Chat"
|
||||
|
||||
# Flashii base URL
|
||||
# Sets the base URL for Flashii
|
||||
#MAMI_FII_URL="//flashii.net"
|
||||
|
||||
# Common configuration location
|
||||
# Contains further information shared by the other chat client
|
||||
#FUTAMI_URL="//futami.flashii.net/common.json"
|
||||
|
||||
# Compatibility client location
|
||||
# Alternative chat client for older browser, redirected to when checks in init.js fail
|
||||
# Also has a handy little button in the settings.
|
||||
#AMI_URL="//sockchat.flashii.net"
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,4 +8,3 @@
|
|||
/public/assets
|
||||
/public/index.html
|
||||
/public/mami.webmanifest
|
||||
/.env
|
||||
|
|
2
LICENCE
2
LICENCE
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2016-2025, flashwave <me@flash.moe>
|
||||
Copyright (c) 2016-2024, flashwave <me@flash.moe>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
30
Makefile
30
Makefile
|
@ -1,30 +0,0 @@
|
|||
NODE_EXE := $(shell which node)
|
||||
NPM_EXE := $(shell which npm)
|
||||
|
||||
BUILD_EXE := build.js
|
||||
ENV_FILE := .env
|
||||
|
||||
all: install build
|
||||
|
||||
install:
|
||||
${NPM_EXE} ci
|
||||
|
||||
update:
|
||||
${NPM_EXE} update --save
|
||||
|
||||
build:
|
||||
${NODE_EXE} --env-file-if-exists ${ENV_FILE} ${BUILD_EXE}
|
||||
|
||||
rebuild-css:
|
||||
${NODE_EXE} --env-file-if-exists ${ENV_FILE} ${BUILD_EXE} css
|
||||
|
||||
rebuild-html:
|
||||
${NODE_EXE} --env-file-if-exists ${ENV_FILE} ${BUILD_EXE} html
|
||||
|
||||
rebuild-js:
|
||||
${NODE_EXE} --env-file-if-exists ${ENV_FILE} ${BUILD_EXE} js
|
||||
|
||||
rebuild-webmanifest:
|
||||
${NODE_EXE} --env-file-if-exists ${ENV_FILE} ${BUILD_EXE} webmanifest
|
||||
|
||||
.PHONY: all install update build rebuild-css rebuild-html rebuild-js rebuild-webmanifest
|
26
README.md
26
README.md
|
@ -4,29 +4,17 @@ The Flashii Chat client.
|
|||
|
||||
## Configuration
|
||||
|
||||
Configuration consists of a `.env` file that contains the various URLs and a default chat title (the unified one is taken from whatever the remote `common.json` supplies).
|
||||
The defaults supplied by `build.js` are what are used for the production instance. Make sure you modify them as needed!
|
||||
Configuration consists of a `config.json` file that contains the various URLs and a default chat title (the unified one is taken from whatever the remote `common.json` supplies).
|
||||
Make sure you create the file before building. An example is also supplied.
|
||||
|
||||
## Building
|
||||
|
||||
Mami uses a build script written in Node.js and a Makefile to make life easier.
|
||||
I recommend using something like `nvm` with a Node version of at least `v22.14.0 (LTS)`.
|
||||
|
||||
The following make recipes are available:
|
||||
Mami uses a build script written in Node.js; make sure you have NPM available and then run the following commands.
|
||||
|
||||
```sh
|
||||
# Runs the package install and build recipes
|
||||
make [all]
|
||||
# Installs packages without updating package.json or package-lock.json
|
||||
npm ci
|
||||
|
||||
# Installs package versions expected by package-lock.json
|
||||
make install
|
||||
|
||||
# Runs the build process
|
||||
make build
|
||||
|
||||
# Updates latest versions of packages expected by package.json
|
||||
make update
|
||||
|
||||
# Rebuild a specific type of asset for speedier development
|
||||
make rebuild-css / rebuild-html / rebuild-js / rebuild-webmanifest
|
||||
# Runs the build process, make sure to run this after updates or changes as well
|
||||
node build.js
|
||||
```
|
||||
|
|
44
build.js
44
build.js
|
@ -4,24 +4,26 @@ const fs = require('fs');
|
|||
const exec = require('util').promisify(require('child_process').exec);
|
||||
|
||||
(async () => {
|
||||
const title = process.env.MAMI_TITLE ?? 'Flashii Chat';
|
||||
const isDebug = !!process.env.MAMI_DEBUG;
|
||||
const config = JSON.parse(fs.readFileSync(pathJoin(__dirname, 'config/config.json')));
|
||||
const isDebug = fs.existsSync(pathJoin(__dirname, '.debug'));
|
||||
|
||||
const env = {
|
||||
root: __dirname,
|
||||
source: pathJoin(__dirname, 'src'),
|
||||
public: pathJoin(__dirname, 'public'),
|
||||
debug: isDebug,
|
||||
swc: { es: 'es2021' },
|
||||
housekeep: [ pathJoin(__dirname, 'public', 'assets') ],
|
||||
swc: {
|
||||
es: 'es2020',
|
||||
},
|
||||
vars: {
|
||||
html: { title },
|
||||
html: {
|
||||
title: config.title,
|
||||
},
|
||||
build: {
|
||||
DEBUG: isDebug,
|
||||
TITLE: title,
|
||||
FII_URL: process.env.MAMI_FII_URL ?? '//flashii.net',
|
||||
FUTAMI_URL: process.env.FUTAMI_URL ?? '//futami.flashii.net/common.json',
|
||||
AMI_URL: process.env.AMI_URL ?? '//sockchat.flashii.net',
|
||||
FUTAMI_DEBUG: isDebug,
|
||||
FUTAMI_URL: config.common_url,
|
||||
MAMI_URL: config.modern_url,
|
||||
AMI_URL: config.compat_url,
|
||||
GIT_HASH: (await exec('git log --pretty="%H" -n1 HEAD')).stdout,
|
||||
},
|
||||
},
|
||||
|
@ -29,36 +31,20 @@ const exec = require('util').promisify(require('child_process').exec);
|
|||
|
||||
const tasks = {
|
||||
js: [
|
||||
{ source: 'mami.js', target: '/assets', name: 'mami.{hash}.js', vars: { build: 'MAMI_JS', html: ':source' } },
|
||||
{ source: 'proto.js', target: '/assets', name: 'proto.{hash}.js', vars: { build: 'MAMI_PROTO_JS', html: ':source' } },
|
||||
{ source: 'mami.js', target: '/assets', name: 'mami.{hash}.js', vars: { build: 'MAMI_MAIN_JS', html: ':source' } },
|
||||
{ source: 'init.js', target: '/assets', name: 'init.{hash}.js', es: 'es5', vars: { html: ':source' } },
|
||||
],
|
||||
css: [
|
||||
{ source: 'mami.css', target: '/assets', name: 'mami.{hash}.css', vars: { html: ':source' } },
|
||||
],
|
||||
webmanifest: [
|
||||
{
|
||||
source: 'mami.webmanifest',
|
||||
target: '/',
|
||||
name: 'mami.webmanifest',
|
||||
icons: '/icons',
|
||||
vars: { html: ':source' },
|
||||
body: {
|
||||
name: title,
|
||||
short_name: title,
|
||||
},
|
||||
},
|
||||
{ source: 'mami.webmanifest', target: '/', name: 'mami.webmanifest', icons: '/icons', vars: { html: ':source' }, body: { name: config.title, short_name: config.title } }
|
||||
],
|
||||
html: [
|
||||
{ source: 'mami.html', target: '/', name: 'index.html', template: 'html' },
|
||||
],
|
||||
};
|
||||
|
||||
if(process.argv.length > 2) {
|
||||
const filter = process.argv.slice(2);
|
||||
for(const name in tasks)
|
||||
if(!filter.includes(name))
|
||||
delete tasks[name];
|
||||
}
|
||||
|
||||
await assproc.process(env, tasks);
|
||||
})();
|
||||
|
|
6
config/config.example.json
Normal file
6
config/config.example.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"title": "Flashii Chat",
|
||||
"common_url": "//futami.flashii.net/common.json",
|
||||
"modern_url": "//chat.flashii.net",
|
||||
"compat_url": "//sockchat.flashii.net"
|
||||
}
|
596
package-lock.json
generated
596
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@railcomm/assproc": "^1.2.1"
|
||||
"@railcomm/assproc": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
108
src/assproc.js
Normal file
108
src/assproc.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
const utils = require('./utils.js');
|
||||
|
||||
exports.process = async function(root, options) {
|
||||
const macroPrefix = options.prefix || '#';
|
||||
const entryPoint = options.entry || '';
|
||||
|
||||
root = fs.realpathSync(root);
|
||||
|
||||
const included = [];
|
||||
|
||||
const processFile = async function(fileName) {
|
||||
const fullPath = path.join(root, fileName);
|
||||
if(included.includes(fullPath))
|
||||
return '';
|
||||
included.push(fullPath);
|
||||
|
||||
if(!fullPath.startsWith(root))
|
||||
return '/* *** INVALID PATH: ' + fullPath + ' */';
|
||||
if(!fs.existsSync(fullPath))
|
||||
return '/* *** FILE NOT FOUND: ' + fullPath + ' */';
|
||||
|
||||
const lines = readline.createInterface({
|
||||
input: fs.createReadStream(fullPath),
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let lastWasEmpty = false;
|
||||
|
||||
if(options.showPath)
|
||||
output += "/* *** PATH: " + fullPath + " */\n";
|
||||
|
||||
for await(const line of lines) {
|
||||
const lineTrimmed = utils.trim(line);
|
||||
if(lineTrimmed === '')
|
||||
continue;
|
||||
|
||||
if(line.startsWith(macroPrefix)) {
|
||||
const args = lineTrimmed.split(' ');
|
||||
const macro = utils.trim(utils.trimStart(args.shift(), macroPrefix));
|
||||
|
||||
switch(macro) {
|
||||
case 'comment':
|
||||
break;
|
||||
|
||||
case 'include': {
|
||||
const includePath = utils.trimEnd(args.join(' '), ';');
|
||||
output += utils.trim(await processFile(includePath));
|
||||
output += "\n";
|
||||
break;
|
||||
}
|
||||
|
||||
case 'buildvars':
|
||||
if(typeof options.buildVars === 'object') {
|
||||
const bvTarget = options.buildVarsTarget || 'window';
|
||||
const bvProps = [];
|
||||
|
||||
for(const bvName in options.buildVars)
|
||||
bvProps.push(`${bvName}: { value: ${JSON.stringify(options.buildVars[bvName])} }`);
|
||||
|
||||
if(Object.keys(bvProps).length > 0)
|
||||
output += `Object.defineProperties(${bvTarget}, { ${bvProps.join(', ')} });\n`;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
output += line;
|
||||
output += "\n";
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
output += line;
|
||||
output += "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
return await processFile(entryPoint);
|
||||
};
|
||||
|
||||
exports.housekeep = function(assetsPath) {
|
||||
const files = fs.readdirSync(assetsPath).map(fileName => {
|
||||
const stats = fs.statSync(path.join(assetsPath, fileName));
|
||||
return {
|
||||
name: fileName,
|
||||
lastMod: stats.mtimeMs,
|
||||
};
|
||||
}).sort((a, b) => b.lastMod - a.lastMod).map(info => info.name);
|
||||
|
||||
const regex = /^(.+)[\-\.]([a-f0-9]+)\.(.+)$/i;
|
||||
const counts = {};
|
||||
|
||||
for(const fileName of files) {
|
||||
const match = fileName.match(regex);
|
||||
if(match) {
|
||||
const name = match[1] + '-' + match[3];
|
||||
counts[name] = (counts[name] || 0) + 1;
|
||||
|
||||
if(counts[name] > 5)
|
||||
fs.unlinkSync(path.join(assetsPath, fileName));
|
||||
} else console.log(`Encountered file name in assets folder with unexpected format: ${fileName}`);
|
||||
}
|
||||
};
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
if(isCompatible) {
|
||||
(function(script) {
|
||||
script.src = MAMI_JS;
|
||||
script.src = MAMI_MAIN_JS;
|
||||
script.type = 'text/javascript';
|
||||
script.charset = 'utf-8';
|
||||
document.body.appendChild(script);
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
.throbber {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
min-width: var(--throbber-container-width, calc(var(--throbber-size, 1) * 100px));
|
||||
min-height: var(--throbber-container-height, calc(var(--throbber-size, 1) * 100px));
|
||||
}
|
||||
.throbber-inline {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
width: var(--throbber-container-width, auto);
|
||||
height: var(--throbber-container-height, auto);
|
||||
}
|
||||
|
||||
.throbber-frame {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.throbber-icon {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: var(--throbber-gap, calc(var(--throbber-size, 1) * 1px));
|
||||
margin: var(--throbber-margin, calc(var(--throbber-size, 1) * 10px));
|
||||
}
|
||||
|
||||
.throbber-icon-block {
|
||||
background: var(--throbber-colour, currentColor);
|
||||
width: var(--throbber-width, calc(var(--throbber-size, 1) * 10px));
|
||||
height: var(--throbber-height, calc(var(--throbber-size, 1) * 10px));
|
||||
}
|
||||
|
||||
.throbber-icon-block-hidden {
|
||||
opacity: 0;
|
||||
}
|
|
@ -12,6 +12,7 @@
|
|||
height: 40px;
|
||||
max-height: 140px;
|
||||
resize: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.input__text,
|
||||
|
@ -32,9 +33,6 @@
|
|||
cursor: pointer
|
||||
}
|
||||
|
||||
.input__menus {
|
||||
}
|
||||
|
||||
.input__menu {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
@ -40,6 +40,10 @@ a:hover {
|
|||
visibility: none !important;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
visibility: none !important;
|
||||
}
|
||||
|
||||
.sjis {
|
||||
font-family: IPAMonaPGothic, 'IPA モナー Pゴシック', Monapo, Mona, 'MS PGothic', 'MS Pゴシック', monospace;
|
||||
font-size: 16px;
|
||||
|
@ -71,7 +75,6 @@ a:hover {
|
|||
@include noscript.css;
|
||||
|
||||
@include controls/msgbox.css;
|
||||
@include controls/throbber.css;
|
||||
@include controls/views.css;
|
||||
|
||||
@include sound/sndtest.css;
|
||||
|
|
|
@ -1,25 +1,22 @@
|
|||
.overlay {
|
||||
font-family: Verdana, Tahoma, Geneva, Arial, Helvetica, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 25px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
color: #fff;
|
||||
}
|
||||
.overlay-filter {
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.overlay-wrapper {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
background: #111;
|
||||
background-color: #222;
|
||||
color: #ddd;
|
||||
text-shadow: 0 0 5px #000;
|
||||
text-align: center;
|
||||
box-shadow: inset 0 0 1em #000;
|
||||
}
|
||||
|
||||
.overlay-message {
|
||||
font-size: 1.5em;
|
||||
line-height: 1.3em;
|
||||
color: #fff;
|
||||
.overlay__message {
|
||||
margin-top: 10px
|
||||
}
|
||||
|
||||
.overlay__status {
|
||||
font-size: .8em;
|
||||
color: #888
|
||||
}
|
||||
|
|
|
@ -1,41 +1,138 @@
|
|||
#include easings.js
|
||||
|
||||
const MamiAnimate = ({ update, duration, easing='linear', signal=null }) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(typeof update !== 'function')
|
||||
throw new Error('update argument must be a function');
|
||||
if(typeof duration !== 'number' || duration < 0)
|
||||
throw new Error('duration argument must be a positive number');
|
||||
if(typeof easing === 'string' && easing in MamiEasings)
|
||||
// This probably need a bit of a rewrite at some point to allow starting for arbitrary time points
|
||||
const MamiAnimate = info => {
|
||||
if(typeof info !== 'object')
|
||||
throw 'info must be an object';
|
||||
|
||||
let onUpdate;
|
||||
if('update' in info && typeof info.update === 'function')
|
||||
onUpdate = info.update;
|
||||
if(onUpdate === undefined)
|
||||
throw 'update is a required parameter for info';
|
||||
|
||||
let duration;
|
||||
if('duration' in info)
|
||||
duration = parseFloat(info.duration);
|
||||
if(duration <= 0)
|
||||
throw 'duration is a required parameter for info and must be greater than 0';
|
||||
|
||||
let onStart;
|
||||
if('start' in info && typeof info.start === 'function')
|
||||
onStart = info.start;
|
||||
|
||||
let onEnd;
|
||||
if('end' in info && typeof info.end === 'function')
|
||||
onEnd = info.end;
|
||||
|
||||
let onCancel;
|
||||
if('cancel' in info && typeof info.cancel === 'function')
|
||||
onCancel = info.cancel;
|
||||
|
||||
let easing;
|
||||
if('easing' in info)
|
||||
easing = info.easing;
|
||||
|
||||
let delayed;
|
||||
if('delayed' in info)
|
||||
delayed = !!info.delayed;
|
||||
|
||||
let async;
|
||||
if('async' in info)
|
||||
async = !!info.async;
|
||||
|
||||
const easingType = typeof easing;
|
||||
if(easingType !== 'function') {
|
||||
if(easingType === 'string' && easing in MamiEasings)
|
||||
easing = MamiEasings[easing];
|
||||
else if(typeof easing !== 'function')
|
||||
throw new Error('easing argument was must be an acceptable easing name or an easing function');
|
||||
if(signal !== null && !(signal instanceof AbortSignal))
|
||||
throw new Error('signal must be null or an instance of AbortSignal');
|
||||
else
|
||||
easing = MamiEasings.linear;
|
||||
}
|
||||
|
||||
let start = null;
|
||||
if(typeof window.mami?.settings?.get === 'function')
|
||||
duration *= mami.settings.get('dbgAnimDurationMulti');
|
||||
|
||||
const frame = time => {
|
||||
if(signal?.aborted) {
|
||||
reject(signal.reason);
|
||||
let tStart, tLast,
|
||||
cancel = false,
|
||||
tRawCompletion = 0,
|
||||
tCompletion = 0,
|
||||
started = !delayed;
|
||||
|
||||
const update = tCurrent => {
|
||||
if(tStart === undefined) {
|
||||
tStart = tCurrent;
|
||||
if(onStart !== undefined)
|
||||
onStart();
|
||||
}
|
||||
|
||||
const tElapsed = tCurrent - tStart;
|
||||
|
||||
tRawCompletion = Math.min(1, Math.max(0, tElapsed / duration));
|
||||
tCompletion = easing(tRawCompletion);
|
||||
|
||||
onUpdate(tCompletion, tRawCompletion);
|
||||
|
||||
if(tElapsed < duration) {
|
||||
if(cancel) {
|
||||
if(onCancel !== undefined)
|
||||
onCancel(tCompletion, tRawCompletion);
|
||||
return;
|
||||
}
|
||||
|
||||
start ??= time;
|
||||
tLast = tCurrent;
|
||||
requestAnimationFrame(update);
|
||||
} else if(onEnd !== undefined)
|
||||
onEnd();
|
||||
};
|
||||
|
||||
const elapsed = time - start;
|
||||
const completion = Math.min(1, Math.max(0, elapsed / duration));
|
||||
|
||||
update(easing(completion), completion);
|
||||
|
||||
if(elapsed < duration) {
|
||||
requestAnimationFrame(frame);
|
||||
return;
|
||||
let promise;
|
||||
if(async)
|
||||
promise = new Promise((resolve, reject) => {
|
||||
if(onCancel === undefined) {
|
||||
onCancel = reject;
|
||||
} else {
|
||||
const realOnCancel = onCancel;
|
||||
onCancel = (...args) => {
|
||||
realOnCancel(...args);
|
||||
reject(...args);
|
||||
};
|
||||
}
|
||||
|
||||
resolve();
|
||||
};
|
||||
if(onEnd === undefined) {
|
||||
onEnd = resolve;
|
||||
} else {
|
||||
const realOnEnd = onEnd;
|
||||
onEnd = (...args) => {
|
||||
realOnEnd(...args);
|
||||
resolve(...args);
|
||||
};
|
||||
}
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
});
|
||||
if(!delayed)
|
||||
requestAnimationFrame(update);
|
||||
});
|
||||
|
||||
if(!delayed) {
|
||||
if(promise !== undefined)
|
||||
return promise;
|
||||
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
return {
|
||||
get completion() { return tCompletion; },
|
||||
get rawCompletion() { return tRawCompletion; },
|
||||
start: () => {
|
||||
if(!started) {
|
||||
started = true;
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
if(promise !== undefined)
|
||||
return promise;
|
||||
},
|
||||
cancel: () => {
|
||||
cancel = true;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
184
src/mami.js/args.js
Normal file
184
src/mami.js/args.js
Normal file
|
@ -0,0 +1,184 @@
|
|||
const MamiArgs = (argName, input, builder) => {
|
||||
if(input !== undefined && typeof input !== 'object')
|
||||
throw `${argName} must be an object or undefined`;
|
||||
if(typeof builder !== 'function')
|
||||
throw 'builder must be a function';
|
||||
|
||||
const args = new Map;
|
||||
|
||||
builder(name => {
|
||||
if(typeof name !== 'string')
|
||||
throw 'name must be a string';
|
||||
|
||||
const checkDefined = () => {
|
||||
if(args.has(name))
|
||||
throw `${name} has already been defined`;
|
||||
};
|
||||
checkDefined();
|
||||
|
||||
let created = false;
|
||||
const checkCreated = () => {
|
||||
if(created)
|
||||
throw 'argument has already been defined';
|
||||
};
|
||||
|
||||
const info = {
|
||||
name: name,
|
||||
type: undefined,
|
||||
filter: undefined,
|
||||
constraint: undefined,
|
||||
fallback: undefined,
|
||||
throw: undefined,
|
||||
required: undefined,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
};
|
||||
|
||||
const blueprint = {
|
||||
type: value => {
|
||||
if(typeof value !== 'string' && !Array.isArray(value))
|
||||
throw 'type must be a javascript type or array of valid string values';
|
||||
|
||||
checkCreated();
|
||||
|
||||
info.type = value;
|
||||
return blueprint;
|
||||
},
|
||||
default: value => {
|
||||
checkCreated();
|
||||
info.fallback = value;
|
||||
|
||||
if(info.type === undefined && info.constraint === undefined)
|
||||
info.type = typeof info.fallback;
|
||||
|
||||
return blueprint;
|
||||
},
|
||||
filter: value => {
|
||||
if(typeof value !== 'function')
|
||||
throw 'filter must be a function';
|
||||
|
||||
checkCreated();
|
||||
|
||||
info.filter = value;
|
||||
return blueprint;
|
||||
},
|
||||
constraint: value => {
|
||||
if(typeof value !== 'function')
|
||||
throw 'constraint must be a function';
|
||||
|
||||
checkCreated();
|
||||
|
||||
info.constraint = value;
|
||||
return blueprint;
|
||||
},
|
||||
throw: value => {
|
||||
checkCreated();
|
||||
info.throw = value === undefined || value === true;
|
||||
return blueprint;
|
||||
},
|
||||
required: value => {
|
||||
checkCreated();
|
||||
info.required = value === undefined || value === true;
|
||||
|
||||
if(info.required && info.throw === undefined)
|
||||
info.throw = true;
|
||||
|
||||
return blueprint;
|
||||
},
|
||||
min: value => {
|
||||
checkCreated();
|
||||
if(typeof value !== 'number')
|
||||
throw 'value must be a number';
|
||||
|
||||
info.min = value;
|
||||
return blueprint;
|
||||
},
|
||||
max: value => {
|
||||
checkCreated();
|
||||
if(typeof value !== 'number')
|
||||
throw 'value must be a number';
|
||||
|
||||
info.max = value;
|
||||
return blueprint;
|
||||
},
|
||||
done: () => {
|
||||
checkCreated();
|
||||
checkDefined();
|
||||
|
||||
args.set(name, info);
|
||||
},
|
||||
};
|
||||
|
||||
return blueprint;
|
||||
});
|
||||
|
||||
const inputIsNull = input === undefined || input === null;
|
||||
if(inputIsNull)
|
||||
input = {};
|
||||
|
||||
const output = {};
|
||||
|
||||
for(const [name, info] of args) {
|
||||
let value = info.fallback;
|
||||
|
||||
if(info.name in input) {
|
||||
const defaultOrThrow = ex => {
|
||||
if(info.throw)
|
||||
throw ex;
|
||||
value = info.fallback;
|
||||
};
|
||||
|
||||
value = input[info.name];
|
||||
|
||||
if(info.type !== undefined) {
|
||||
if(Array.isArray(info.type)) {
|
||||
if(!info.type.includes(value))
|
||||
defaultOrThrow(`${info.name} must match an enum value`);
|
||||
} else {
|
||||
const type = typeof value;
|
||||
let resolved = false;
|
||||
|
||||
if(type !== info.type) {
|
||||
if(type === 'string') {
|
||||
if(info.type === 'number') {
|
||||
value = parseFloat(value);
|
||||
resolved = true;
|
||||
|
||||
if(info.min !== undefined && value < info.min)
|
||||
value = info.min;
|
||||
else if(info.max !== undefined && value > info.max)
|
||||
value = info.max;
|
||||
} else if(info.type === 'boolean') {
|
||||
value = !!value;
|
||||
resolved = true;
|
||||
}
|
||||
} else if(info.type === 'string') {
|
||||
value = value.toString();
|
||||
resolved = true;
|
||||
}
|
||||
} else resolved = true;
|
||||
|
||||
if(!resolved)
|
||||
defaultOrThrow(`${info.name} must be of type ${info.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
if(info.constraint !== undefined && !info.constraint(value))
|
||||
defaultOrThrow(`${info.name} did not fit within constraints`);
|
||||
|
||||
// you could have a devious streak with this one cuz the checks aren't rerun
|
||||
// but surely you wouldn't fuck yourself over like that
|
||||
if(info.filter !== undefined)
|
||||
value = info.filter(value);
|
||||
} else if(info.required) {
|
||||
if(inputIsNull)
|
||||
throw `${argName} must be a non-null object`;
|
||||
|
||||
throw `${argName} is missing required key ${info.name}`;
|
||||
}
|
||||
|
||||
output[info.name] = value;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
|
@ -1,7 +1,11 @@
|
|||
const MamiDetectAutoPlaySource = 'data:audio/mpeg;base64,/+NIxAAAAAAAAAAAAFhpbmcAAAAPAAAAAwAAAfgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq////////////////////////////////////////////AAAAOUxBTUUzLjEwMAIeAAAAAAAAAAAUCCQC8CIAAAgAAAH49wpKWgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAAAANIAAAAAExBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxDsAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxHYAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV';
|
||||
#include srle.js
|
||||
#include utility.js
|
||||
|
||||
const MamiDetectAutoPlaySource = '/+NIxA!!FhpbmcA#PA$wA@fgAV$#q$#/$#A#OUxBTUUzLjEwMAIeA!)UCCQC8CIA@gA@H49wpKWgA!%&/+MYxA$NIA$ExBTUUzLjEwMFV^%/+MYxDsA@NIA$FV&&/+MYxHYA@NIA$FV&&';
|
||||
|
||||
const MamiDetectAutoPlay = async () => {
|
||||
try {
|
||||
await $element('audio', { src: MamiDetectAutoPlaySource }).play();
|
||||
await $e('audio', { src: 'data:audio/mpeg;base64,' + MamiSRLE.decode(MamiDetectAutoPlaySource) }).play();
|
||||
} catch(ex) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#include utility.js
|
||||
#include audio/source.js
|
||||
|
||||
const MamiAudioContext = function() {
|
||||
|
@ -61,8 +62,8 @@ const MamiAudioContext = function() {
|
|||
if(ctx === undefined)
|
||||
return undefined;
|
||||
|
||||
const { body } = await $xhr.get(url, { type: 'arraybuffer' });
|
||||
return await ctx.decodeAudioData(body);
|
||||
const result = await $x.get(url, { type: 'arraybuffer' });
|
||||
return await ctx.decodeAudioData(result.body());
|
||||
},
|
||||
|
||||
createSource: (buffer, reverse) => {
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
const MamiIsAuth = value => {
|
||||
return typeof value === 'object'
|
||||
&& value !== null
|
||||
&& 'type' in value
|
||||
&& (value.type === null || typeof value.type === 'string')
|
||||
&& 'token' in value
|
||||
&& (value.token === null || typeof value.token === 'string')
|
||||
&& 'header' in value
|
||||
&& (value.header === null || typeof value.header === 'string')
|
||||
&& 'refresh' in value
|
||||
&& typeof value.refresh === 'function';
|
||||
};
|
||||
|
||||
const MamiFlashiiAuth = function(flashii) {
|
||||
let type = null;
|
||||
let token = null;
|
||||
|
||||
return {
|
||||
get type() { return type; },
|
||||
get token() { return token; },
|
||||
get header() { return type ? `${type} ${token}` : null; },
|
||||
|
||||
async refresh() {
|
||||
try {
|
||||
({ token_type: type=null, access_token: token=null } = await flashii.v1.chat.token());
|
||||
} catch(ex) {
|
||||
if(ex instanceof Error && ex.cause === 'flashii.v1.unauthorized')
|
||||
await flashii.v1.chat.login();
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
11
src/mami.js/avatar.js
Normal file
11
src/mami.js/avatar.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const MamiFormatUserAvatarUrl = (userId, res = 0, changeTime = null) => {
|
||||
const template = futami.get('avatar') ?? '';
|
||||
if(template.length < 1)
|
||||
return '';
|
||||
|
||||
changeTime ??= Date.now();
|
||||
|
||||
return template.replace('{user:id}', userId)
|
||||
.replace('{resolution}', res)
|
||||
.replace('{user:avatar_change}', changeTime);
|
||||
};
|
|
@ -1,2 +1,19 @@
|
|||
const MamiSleep = durationMs => new Promise(resolve => { setTimeout(resolve, durationMs); });
|
||||
const MamiTimeout = (promise, time) => Promise.race([promise, new Promise((resolve, reject) => setTimeout(reject, time))]);
|
||||
|
||||
const MamiWaitVisible = () => {
|
||||
return new Promise(resolve => {
|
||||
if(document.visibilityState === 'visible') {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = () => {
|
||||
if(document.visibilityState === 'visible') {
|
||||
window.removeEventListener('visibilitychange', handler);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('visibilitychange', handler);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
#include chatform/input.jsx
|
||||
#include chatform/markup.jsx
|
||||
#include chat/input.jsx
|
||||
#include chat/markup.jsx
|
||||
|
||||
const MamiChatForm = function(eventTarget) {
|
||||
const input = new MamiChatFormInput(eventTarget);
|
||||
const MamiChatForm = function(findUserInfosByName, findEmoteByName, eventTarget) {
|
||||
const input = new MamiChatFormInput(findUserInfosByName, findEmoteByName, eventTarget);
|
||||
const markup = new MamiChatFormMarkup;
|
||||
|
||||
const html = <form class="input" onsubmit={ev => {
|
37
src/mami.js/chat/garbparse.jsx
Normal file
37
src/mami.js/chat/garbparse.jsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
#include parsing.js
|
||||
#include url.js
|
||||
#include weeb.js
|
||||
#include ui/emotes.js
|
||||
|
||||
// replace this with better parsing, read the fucking book already
|
||||
const MamiMolestMessageContents = (info, textElem, userNameElem) => {
|
||||
const hasTextElem = textElem instanceof Element;
|
||||
|
||||
if(hasTextElem) {
|
||||
Umi.UI.Emoticons.Parse(textElem, info.sender);
|
||||
Umi.Parsing.Parse(textElem, info);
|
||||
|
||||
const textSplit = textElem.innerText.split(' ');
|
||||
for(const textPart of textSplit) {
|
||||
const uri = Umi.URI.Parse(textPart);
|
||||
|
||||
if(uri !== null && uri.Slashes !== null) {
|
||||
const anchorElem = <a class="markup__link" href={textPart} target="_blank" rel="nofollow noreferrer noopener">{textPart}</a>;
|
||||
textElem.innerHTML = textElem.innerHTML.replace(textPart.replace(/&/g, '&'), anchorElem.outerHTML);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(mami.settings.get('weeaboo')) {
|
||||
if(userNameElem instanceof Element)
|
||||
userNameElem.appendChild($t(Weeaboo.getNameSuffix(info.sender)));
|
||||
|
||||
if(hasTextElem) {
|
||||
textElem.appendChild($t(Weeaboo.getTextSuffix(info.sender)));
|
||||
|
||||
const kaomoji = Weeaboo.getRandomKaomoji(true, info);
|
||||
if(kaomoji)
|
||||
textElem.append(` ${kaomoji}`);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,7 +1,4 @@
|
|||
#include emotes.js
|
||||
#include users.js
|
||||
|
||||
const MamiChatFormInput = function(eventTarget) {
|
||||
const MamiChatFormInput = function(findUserInfosByName, findEmoteByName, eventTarget) {
|
||||
const textElem = <textarea class="input__text" name="text" autofocus="autofocus"/>;
|
||||
let submitButton;
|
||||
|
||||
|
@ -74,25 +71,24 @@ const MamiChatFormInput = function(eventTarget) {
|
|||
snippet = text.charAt(position) + snippet;
|
||||
}
|
||||
|
||||
let insertText;
|
||||
const insertText = portion => {
|
||||
const nextPos = start - snippet.length + portion.length;
|
||||
textElem.value = text.slice(0, start - snippet.length) + portion + text.slice(start);
|
||||
textElem.selectionEnd = textElem.selectionStart = nextPos;
|
||||
};
|
||||
|
||||
if(snippet.indexOf(':') === 0) {
|
||||
let emoteRank = 0;
|
||||
if(Umi.User.hasCurrentUser())
|
||||
emoteRank = Umi.User.getCurrentUser().perms.rank;
|
||||
const emotes = MamiEmotes.findByName(emoteRank, snippet.substring(1), true);
|
||||
if(emotes.length > 0)
|
||||
insertText = `:${emotes[0]}:`;
|
||||
findEmoteByName(snippet.substring(1))
|
||||
.then(emotes => {
|
||||
if(emotes.length > 0)
|
||||
insertText(`:${emotes[0]}:`);
|
||||
});
|
||||
} else {
|
||||
const users = Umi.Users.Find(snippet);
|
||||
if(users.length === 1)
|
||||
insertText = users[0].name;
|
||||
}
|
||||
|
||||
if(insertText !== undefined) {
|
||||
const nextPos = start - snippet.length + insertText.length;
|
||||
textElem.value = text.slice(0, start - snippet.length) + insertText + text.slice(start);
|
||||
textElem.selectionEnd = textElem.selectionStart = nextPos;
|
||||
findUserInfosByName(snippet)
|
||||
.then(userInfos => {
|
||||
if(userInfos.length === 1)
|
||||
insertText(userInfos[0].name);
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
117
src/mami.js/chat/messages.jsx
Normal file
117
src/mami.js/chat/messages.jsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
#include uniqstr.js
|
||||
#include chat/msg-act.jsx
|
||||
#include chat/msg-text.jsx
|
||||
|
||||
const MamiChatMessages = function(globalEvents) {
|
||||
const html = <div class="chat"/>;
|
||||
const broadcasts = [];
|
||||
const channels = new Map;
|
||||
|
||||
let msgs = [];
|
||||
let autoScroll = false;
|
||||
let autoEmbed = false;
|
||||
|
||||
// so like how the fuck am i going to do this wtf
|
||||
// some msgs may not have an id, just gen a rando for those then?
|
||||
|
||||
const doAutoScroll = () => {
|
||||
if(autoScroll)
|
||||
html.lastElementChild?.scrollIntoView({ block: 'start', inline: 'end' });
|
||||
};
|
||||
|
||||
return {
|
||||
get element() { return html; },
|
||||
|
||||
get autoScroll() { return autoScroll; },
|
||||
set autoScroll(value) {
|
||||
autoScroll = !!value;
|
||||
},
|
||||
|
||||
get autoEmbed() { return autoEmbed; },
|
||||
set autoEmbed(value) {
|
||||
autoEmbed = !!value;
|
||||
},
|
||||
|
||||
addMessage: info => {
|
||||
console.info('addMessage()', info);
|
||||
|
||||
const msgId = info.id ?? `internal-${MamiUniqueStr(8)}`;
|
||||
|
||||
let msg;
|
||||
if('element' in info) {
|
||||
msg = info;
|
||||
} else {
|
||||
if(info.type === 'msg:text' || info.type.startsWith('legacy:'))
|
||||
msg = new MamiChatMessageText(info);
|
||||
else
|
||||
msg = new MamiChatMessageAction(info);
|
||||
}
|
||||
|
||||
if('first' in msg) {
|
||||
const prev = msgs[msgs.length - 1];
|
||||
msg.first = prev === undefined || !('first' in prev) || !('info' in prev) || info.sender.id !== prev.info.sender.id;
|
||||
}
|
||||
|
||||
msgs.push(msg);
|
||||
|
||||
html.appendChild(msg.element);
|
||||
|
||||
// only run when not hidden/current channel
|
||||
if(autoEmbed) {
|
||||
const callEmbedOn = msg.element.querySelectorAll('a[onclick^="Umi.Parser.SockChatBBcode.Embed"]');
|
||||
for(const embedElem of callEmbedOn)
|
||||
if(embedElem.dataset.embed !== '1')
|
||||
embedElem.click();
|
||||
}
|
||||
|
||||
// ^^ if(!eBase.classList.contains('hidden'))
|
||||
doAutoScroll();
|
||||
|
||||
if(globalEvents)
|
||||
globalEvents.dispatch('umi:ui:message_add', { element: msg.element });
|
||||
},
|
||||
removeMessage: msgId => {
|
||||
console.info('removeMessage()', msgId);
|
||||
|
||||
for(const i in msgs) {
|
||||
const msg = msgs[i];
|
||||
if(msg.matches(msgId)) {
|
||||
// TODO: i want login, logout, etc. actions to be collapsed as a single messages
|
||||
// some kinda flow to allow for that should exists (probably just check if the msg obj has certain functions and then handle deletion differently)
|
||||
if(html.contains(msg.element))
|
||||
html.removeChild(msg.element);
|
||||
msgs.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
doAutoScroll();
|
||||
},
|
||||
clearMessages: keepAmount => {
|
||||
console.info('clearMessages()', keepAmount);
|
||||
|
||||
msgs = [];
|
||||
html.replaceChildren();
|
||||
},
|
||||
|
||||
scrollIfNeeded: offsetOrForce => {
|
||||
console.info('scrollIfNeeded()', offsetOrForce);
|
||||
|
||||
if(typeof offsetOrForce === 'boolean' && offsetOrForce !== true)
|
||||
return;
|
||||
|
||||
if(typeof offsetOrForce === 'number' && html.scrollTop < (html.scrollHeight - html.offsetHeight - offsetOrForce))
|
||||
return;
|
||||
|
||||
doAutoScroll();
|
||||
},
|
||||
|
||||
// probably not the way i want this to work going forward
|
||||
switchChannel: channelName => {
|
||||
console.info('switchChannel()', channelName);
|
||||
//
|
||||
|
||||
doAutoScroll();
|
||||
},
|
||||
};
|
||||
};
|
82
src/mami.js/chat/msg-act.jsx
Normal file
82
src/mami.js/chat/msg-act.jsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
#include avatar.js
|
||||
#include chat/garbparse.jsx
|
||||
|
||||
const MamiChatMessageAction = function(info) {
|
||||
const avatarClasses = ['message__avatar'];
|
||||
|
||||
// TODO: make this not hardcoded probably (should be in its own object for the thing mentioned in messages.jsx)
|
||||
let text;
|
||||
let senderId = info.sender?.id ?? '-1';
|
||||
let senderName = info.sender?.name ?? '';
|
||||
let senderColour = info.sender?.colour ?? 'inherit';
|
||||
if(info.type === 'msg:action') {
|
||||
text = info.text;
|
||||
} else if(info.type === 'user:add') {
|
||||
text = 'has joined';
|
||||
} else if(info.type === 'user:remove') {
|
||||
if(info.detail.reason === 'kick') {
|
||||
avatarClasses.push('avatar-filter-invert');
|
||||
text = 'got bludgeoned to death';
|
||||
} else if(info.detail.reason === 'flood') {
|
||||
avatarClasses.push('avatar-filter-invert');
|
||||
text = 'got kicked for flood protection';
|
||||
} else if(info.detail.reason === 'timeout') {
|
||||
avatarClasses.push('avatar-filter-greyscale');
|
||||
text = 'exploded';
|
||||
} else {
|
||||
avatarClasses.push('avatar-filter-greyscale');
|
||||
text = 'has disconnected';
|
||||
}
|
||||
} else if(info.type === 'chan:join') {
|
||||
text = 'has joined the channel';
|
||||
} else if(info.type === 'chan:leave') {
|
||||
avatarClasses.push('avatar-filter-greyscale');
|
||||
text = 'has left the channel';
|
||||
} else if(info.type === 'user:nick') {
|
||||
if(senderId === '-1') {
|
||||
senderId = '0';
|
||||
senderName = info.detail.previousName;
|
||||
senderColour = 'inherit';
|
||||
}
|
||||
|
||||
text = `changed their name to ${info.detail.name}`;
|
||||
} else {
|
||||
text = `unknown action type: ${info.type}!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`;
|
||||
}
|
||||
|
||||
if(text.indexOf("'") !== 0 || (text.match(/\'/g).length % 2) === 0)
|
||||
text = "\xA0" + text;
|
||||
|
||||
let textElem, userNameElem;
|
||||
const html = <div class={`message message-tiny message--user-${senderId} message--first`}>
|
||||
<div class={avatarClasses} style={`background-image: url('${MamiFormatUserAvatarUrl(senderId, 80)}');`} />
|
||||
<div class="message__container">
|
||||
<div class="message__meta">
|
||||
{userNameElem = <div class="message__user" style={`color: ${senderColour}`}>
|
||||
{senderName}
|
||||
</div>}
|
||||
{textElem = <div class="message-tiny-text">
|
||||
{text}
|
||||
</div>}
|
||||
<div class="message__time">
|
||||
{info.time.getHours().toString().padStart(2, '0')}:{info.time.getMinutes().toString().padStart(2, '0')}:{info.time.getSeconds().toString().padStart(2, '0')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
MamiMolestMessageContents(info, info.type === 'msg:action' ? textElem : undefined, userNameElem);
|
||||
|
||||
return {
|
||||
get element() { return html; },
|
||||
|
||||
matches: other => {
|
||||
if(typeof other === 'object' && other !== null && 'id' in other)
|
||||
other = other.id;
|
||||
if(typeof other !== 'string')
|
||||
return false;
|
||||
|
||||
return other === info.id;
|
||||
},
|
||||
};
|
||||
};
|
89
src/mami.js/chat/msg-text.jsx
Normal file
89
src/mami.js/chat/msg-text.jsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
#include avatar.js
|
||||
#include chat/garbparse.jsx
|
||||
|
||||
const MamiChatMessageText = function(info) {
|
||||
let text;
|
||||
if(info.type === 'msg:text') {
|
||||
text = info.text;
|
||||
} else if(info.type.startsWith('legacy:')) {
|
||||
const bi = info.botInfo;
|
||||
if(bi.type === 'banlist') {
|
||||
const bans = bi.args[0].split(', ');
|
||||
for(const i in bans)
|
||||
bans[i] = bans[i].slice(92, -4);
|
||||
|
||||
text = `Banned: ${bans.join(', ')}`;
|
||||
} else if(bi.type === 'who') {
|
||||
const users = bi.args[0].split(', ');
|
||||
for(const i in users) {
|
||||
const isSelf = users[i].includes(' style="font-weight: bold;"');
|
||||
users[i] = users[i].slice(isSelf ? 102 : 75, -4);
|
||||
if(isSelf) users[i] += ' (You)';
|
||||
}
|
||||
|
||||
text = `Online: ${users.join(', ')}`;
|
||||
} else if(bi.type === 'whochan') {
|
||||
const users = bi.args[1].split(', ');
|
||||
for(const i in users) {
|
||||
const isSelf = users[i].includes(' style="font-weight: bold;"');
|
||||
users[i] = users[i].slice(isSelf ? 102 : 75, -4);
|
||||
if(isSelf) users[i] += ' (You)';
|
||||
}
|
||||
|
||||
text = `Online in ${bi.args[0]}: ${users.join(', ')}`;
|
||||
} else if(bi.type === 'say') {
|
||||
text = bi.args[0];
|
||||
} else if(bi.type === 'ipaddr') {
|
||||
text = `IP address of ${bi.args[0]}: ${bi.args[1]}.`;
|
||||
} else {
|
||||
text = `unknown legacy text type: ${bi.type}!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`;
|
||||
}
|
||||
} else {
|
||||
text = `unknown text type: ${info.type}!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`;
|
||||
}
|
||||
|
||||
let textElem, userNameElem;
|
||||
const html = <div class={`message message--user-${info.sender.id}`}>
|
||||
<div class="message__avatar" style={`background-image: url('${MamiFormatUserAvatarUrl(info.sender.id, 80)}');`} />
|
||||
<div class="message__container">
|
||||
<div class="message__meta">
|
||||
{userNameElem = <div class="message__user" style={`color: ${info.sender.colour}`}>
|
||||
{info.sender.name}
|
||||
</div>}
|
||||
<div class="message__time">
|
||||
{info.time.getHours().toString().padStart(2, '0')}:{info.time.getMinutes().toString().padStart(2, '0')}:{info.time.getSeconds().toString().padStart(2, '0')}
|
||||
</div>
|
||||
</div>
|
||||
{textElem = <div class="message__text">
|
||||
{text}
|
||||
</div>}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
if(info.type === 'msg:text') {
|
||||
if(info.sender.id === '136')
|
||||
html.style.transform = 'scaleY(' + (0.76 + (0.01 * Math.max(0, Math.ceil(Date.now() / (7 * 24 * 60 * 60000)) - 2813))).toString() + ')';
|
||||
|
||||
MamiMolestMessageContents(info, textElem, userNameElem);
|
||||
}
|
||||
|
||||
return {
|
||||
get element() { return html; },
|
||||
|
||||
get info() { return info; },
|
||||
|
||||
get first() { return html.classList.contains('message--first'); },
|
||||
set first(value) {
|
||||
html.classList.toggle('message--first', value);
|
||||
},
|
||||
|
||||
matches: other => {
|
||||
if(typeof other === 'object' && other !== null && 'id' in other)
|
||||
other = other.id;
|
||||
if(typeof other !== 'string')
|
||||
return false;
|
||||
|
||||
return other === info.id;
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,4 +1,6 @@
|
|||
#include args.js
|
||||
#include colour.js
|
||||
#include utility.js
|
||||
#include controls/tabs.js
|
||||
#include colpick/tgrid.jsx
|
||||
#include colpick/tpresets.jsx
|
||||
|
@ -6,32 +8,21 @@
|
|||
#include colpick/vhex.jsx
|
||||
#include colpick/vraw.jsx
|
||||
|
||||
const MamiColourPicker = function({
|
||||
colour=0,
|
||||
posX=-1,
|
||||
posY=-1,
|
||||
flashii=null,
|
||||
presets=[],
|
||||
showPresetsTab=true,
|
||||
showGridTab=true,
|
||||
showSlidersTab=true,
|
||||
showHexValue=true,
|
||||
showRawValue=true,
|
||||
showDialogButtons=true,
|
||||
}={}) {
|
||||
if(typeof colour !== 'number')
|
||||
throw new Error('colour argument must be a number');
|
||||
if(typeof posX !== 'number')
|
||||
throw new Error('posX argument must be a number');
|
||||
if(typeof posY !== 'number')
|
||||
throw new Error('posY argument must be a number');
|
||||
if(typeof flashii !== 'object')
|
||||
throw new Error('flashii argument must be an object or null');
|
||||
if(!Array.isArray(presets))
|
||||
throw new Error('presets argument must be an array');
|
||||
|
||||
colour = Math.min(0xFFFFFF, Math.max(0, colour | 0));
|
||||
const MamiColourPicker = function(options) {
|
||||
options = MamiArgs('options', options, define => {
|
||||
define('colour').default(0).filter(value => Math.min(0xFFFFFF, Math.max(0, value))).done();
|
||||
define('posX').default(-1).done();
|
||||
define('posY').default(-1).done();
|
||||
define('presets').default([]).constraint(value => Array.isArray(value)).done();
|
||||
define('showPresetsTab').default(true).done();
|
||||
define('showGridTab').default(true).done();
|
||||
define('showSlidersTab').default(true).done();
|
||||
define('showHexValue').default(true).done();
|
||||
define('showRawValue').default(true).done();
|
||||
define('showDialogButtons').default(true).done();
|
||||
});
|
||||
|
||||
let colour;
|
||||
const needsColour = [];
|
||||
|
||||
let promiseResolve;
|
||||
|
@ -110,8 +101,8 @@ const MamiColourPicker = function({
|
|||
},
|
||||
onRemove: ctx => {
|
||||
const name = ctx.info.name;
|
||||
$query(`.colpick-tab-${name}-button`)?.remove();
|
||||
$query(`.colpick-tab-${name}-container`)?.remove();
|
||||
$rq(`.colpick-tab-${name}-button`);
|
||||
$rq(`.colpick-tab-${name}-container`);
|
||||
},
|
||||
onSwitch: ctx => {
|
||||
if(ctx.from !== undefined) {
|
||||
|
@ -124,11 +115,11 @@ const MamiColourPicker = function({
|
|||
},
|
||||
});
|
||||
|
||||
if(showPresetsTab && (flashii !== null || presets.length > 0))
|
||||
tabs.add(new MamiColourPickerPresetsTab(flashii ?? presets));
|
||||
if(showGridTab)
|
||||
if(options.showPresetsTab && options.presets.length > 0)
|
||||
tabs.add(new MamiColourPickerPresetsTab(options.presets));
|
||||
if(options.showGridTab)
|
||||
tabs.add(new MamiColourPickerGridTab);
|
||||
if(showSlidersTab)
|
||||
if(options.showSlidersTab)
|
||||
tabs.add(new MamiColourPickerSlidersTab);
|
||||
|
||||
if(tabs.count < 1)
|
||||
|
@ -143,9 +134,9 @@ const MamiColourPicker = function({
|
|||
</label>);
|
||||
};
|
||||
|
||||
if(showHexValue)
|
||||
if(options.showHexValue)
|
||||
addValue('hex', 'Hex', new MamiColourPickerValueHex);
|
||||
if(showRawValue)
|
||||
if(options.showRawValue)
|
||||
addValue('raw', 'Raw', new MamiColourPickerValueRaw);
|
||||
|
||||
const addButton = (id, name, action) => {
|
||||
|
@ -162,13 +153,13 @@ const MamiColourPicker = function({
|
|||
buttons.appendChild(button);
|
||||
};
|
||||
|
||||
if(showDialogButtons) {
|
||||
if(options.showDialogButtons) {
|
||||
addButton('cancel', 'Cancel', runReject);
|
||||
addButton('apply', 'Apply');
|
||||
}
|
||||
|
||||
setPosition({ x: posX, y: posY });
|
||||
setColour(colour);
|
||||
setPosition({ x: options.posX, y: options.posY });
|
||||
setColour(options.colour);
|
||||
|
||||
return {
|
||||
get element() { return html; },
|
||||
|
@ -177,6 +168,7 @@ const MamiColourPicker = function({
|
|||
setPosition: setPosition,
|
||||
close: runReject,
|
||||
dialog: pos => {
|
||||
html.classList.add('invisible');
|
||||
html.classList.remove('hidden');
|
||||
|
||||
if(pos instanceof MouseEvent)
|
||||
|
@ -200,6 +192,8 @@ const MamiColourPicker = function({
|
|||
if(pos !== undefined)
|
||||
setPosition(pos);
|
||||
|
||||
html.classList.remove('invisible');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
promiseResolve = resolve;
|
||||
promiseReject = reject;
|
||||
|
|
|
@ -1,35 +1,14 @@
|
|||
#include colour.js
|
||||
#include controls/throbber.jsx
|
||||
|
||||
const MamiColourPickerPresetsTab = function(presets) {
|
||||
let onChange = () => {};
|
||||
|
||||
const html = <div/>;
|
||||
|
||||
const appendColours = (presets, titleParam, rgbParam) => {
|
||||
for(const preset of presets)
|
||||
html.appendChild(<button class={['colpick-presets-option', `colpick-presets-option-${preset[rgbParam]}`]}
|
||||
style={{ background: MamiColour.hex(preset[rgbParam]) }} title={preset[titleParam]}
|
||||
type="button" onclick={() => onChange(preset[rgbParam])}/>);
|
||||
};
|
||||
|
||||
// todo: make this not bad
|
||||
if(Array.isArray(presets)) {
|
||||
appendColours(presets, 'n', 'c');
|
||||
} else {
|
||||
const throbber = <Throbber containerWidth="100%" containerHeight="230px" />;
|
||||
$appendChild(html, throbber);
|
||||
|
||||
presets.v1.colours.presets({ fields: ['title', 'rgb'] })
|
||||
.then(presets => {
|
||||
$removeChild(html, throbber);
|
||||
appendColours(presets, 'title', 'rgb');
|
||||
})
|
||||
.catch(ex => {
|
||||
console.error(ex);
|
||||
throbber.icon.stop().then(() => { throbber.icon.batsu(); });
|
||||
});
|
||||
}
|
||||
for(const preset of presets)
|
||||
html.appendChild(<button class={['colpick-presets-option', `colpick-presets-option-${preset.c}`]}
|
||||
style={{ background: MamiColour.hex(preset.c) }} title={preset.n}
|
||||
type="button" onclick={() => onChange(preset.c)}/>);
|
||||
|
||||
return {
|
||||
get name() { return 'presets'; },
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#include utility.js
|
||||
|
||||
const FutamiCommon = function(vars) {
|
||||
vars = vars || {};
|
||||
|
||||
|
@ -13,8 +15,8 @@ const FutamiCommon = function(vars) {
|
|||
if(noCache)
|
||||
options.headers = { 'Cache-Control': 'no-cache' };
|
||||
|
||||
const { body } = await $xhr.get(get(name), options);
|
||||
return body;
|
||||
const resp = await $x.get(get(name), options);
|
||||
return resp.body();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -23,12 +25,12 @@ FutamiCommon.load = async url => {
|
|||
if(typeof url !== 'string' && 'FUTAMI_URL' in window)
|
||||
url = window.FUTAMI_URL + '?t=' + Date.now().toString();
|
||||
|
||||
const { body } = await $xhr.get(url, {
|
||||
const resp = await $x.get(url, {
|
||||
type: 'json',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
return new FutamiCommon(body);
|
||||
return new FutamiCommon(resp.body());
|
||||
};
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
const MamiRunConcurrent = (tasks, concurrency=4) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let index = 0;
|
||||
let active = 0;
|
||||
const results = [];
|
||||
let rejected = false;
|
||||
|
||||
const next = () => {
|
||||
if(rejected)
|
||||
return;
|
||||
|
||||
if(index >= tasks.length && active < 1) {
|
||||
resolve(results);
|
||||
return;
|
||||
}
|
||||
|
||||
while(active < concurrency && index < tasks.length) {
|
||||
const current = index++;
|
||||
++active;
|
||||
|
||||
Promise.resolve(tasks[current]())
|
||||
.then(result => {
|
||||
results[current] = result;
|
||||
--active;
|
||||
next();
|
||||
})
|
||||
.catch((...args) => {
|
||||
rejected = true;
|
||||
reject(...args);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
#include awaitable.js
|
||||
#include utility.js
|
||||
|
||||
const MamiConnectionManager = function(client, settings, flashii, eventTarget) {
|
||||
const MamiConnectionManager = function(client, settings, urls, eventTarget) {
|
||||
const validateClient = value => {
|
||||
if(typeof value !== 'object' || value === null)
|
||||
throw 'client must be a non-null object';
|
||||
|
@ -10,6 +11,8 @@ const MamiConnectionManager = function(client, settings, flashii, eventTarget) {
|
|||
|
||||
if(typeof settings !== 'object' || settings === null)
|
||||
throw 'settings must be a non-null object';
|
||||
if(!Array.isArray(urls))
|
||||
throw 'urls must be an array';
|
||||
if(typeof eventTarget !== 'object' || eventTarget === null)
|
||||
throw 'eventTarget must be a non-null object';
|
||||
|
||||
|
@ -39,14 +42,23 @@ const MamiConnectionManager = function(client, settings, flashii, eventTarget) {
|
|||
url = undefined;
|
||||
};
|
||||
|
||||
$as(urls);
|
||||
|
||||
const attempt = () => {
|
||||
started = Date.now();
|
||||
url = urls[attempts % urls.length];
|
||||
if(url.startsWith('//'))
|
||||
url = location.protocol.replace('http', 'ws') + url;
|
||||
|
||||
const attemptNo = attempts + 1;
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
resetTimeout();
|
||||
|
||||
(async () => {
|
||||
if(settings.get('onlyConnectWhenVisible2'))
|
||||
await MamiWaitVisible();
|
||||
|
||||
eventTarget.dispatch('attempt', {
|
||||
url: url,
|
||||
started: started,
|
||||
|
@ -54,16 +66,6 @@ const MamiConnectionManager = function(client, settings, flashii, eventTarget) {
|
|||
});
|
||||
|
||||
try {
|
||||
({ uri: url } = await flashii.v1.chat.servers.recommended({
|
||||
proto: 'sockchat',
|
||||
fresh: attempts > 3,
|
||||
}));
|
||||
} catch(ex) {}
|
||||
|
||||
try {
|
||||
if(typeof url !== 'string')
|
||||
throw {};
|
||||
|
||||
const result = await client.connect(url);
|
||||
if(typeof result === 'boolean' && !result)
|
||||
throw {};
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
#include events.js
|
||||
|
||||
const MamiContext = function(globalEventTarget, eventTarget, auth, flashii) {
|
||||
const MamiContext = function(globalEventTarget, eventTarget) {
|
||||
if(typeof globalEventTarget !== 'object' && globalEventTarget === null)
|
||||
throw 'globalEventTarget must be undefined or a non-null object';
|
||||
|
||||
if(eventTarget === undefined || eventTarget === null)
|
||||
if(eventTarget === undefined)
|
||||
eventTarget = 'mami';
|
||||
else if(typeof eventTarget !== 'object' || eventTarget === null)
|
||||
throw 'eventTarget must be a string or a non-null object';
|
||||
|
||||
if(typeof eventTarget === 'string')
|
||||
eventTarget = globalEventTarget.scopeTo(eventTarget);
|
||||
else if(typeof eventTarget !== 'object')
|
||||
throw 'eventTarget must be a string or a non-null object';
|
||||
|
||||
let isUnloading = false;
|
||||
|
||||
|
@ -20,14 +21,12 @@ const MamiContext = function(globalEventTarget, eventTarget, auth, flashii) {
|
|||
let textTriggers;
|
||||
let eeprom;
|
||||
let conMan;
|
||||
let protoWorker;
|
||||
|
||||
return {
|
||||
get globalEvents() { return globalEventTarget; },
|
||||
get events() { return eventTarget; },
|
||||
|
||||
get auth() { return auth; },
|
||||
get flashii() { return flashii; },
|
||||
|
||||
get isUnloading() { return isUnloading; },
|
||||
set isUnloading(state) {
|
||||
isUnloading = !!state;
|
||||
|
@ -89,5 +88,12 @@ const MamiContext = function(globalEventTarget, eventTarget, auth, flashii) {
|
|||
throw 'conMan must be a non-null object';
|
||||
conMan = value;
|
||||
},
|
||||
|
||||
get protoWorker() { return protoWorker; },
|
||||
set protoWorker(value) {
|
||||
if(typeof value !== 'object' || value === null)
|
||||
throw 'protoWorker must be a non-null object';
|
||||
protoWorker = value;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
#include animate.js
|
||||
#include args.js
|
||||
#include utility.js
|
||||
|
||||
const MamiMessageBoxContainer = function() {
|
||||
const container = <div class="msgbox-container"/>;
|
||||
let raised = false;
|
||||
let animeAbort;
|
||||
let currAnim;
|
||||
|
||||
return {
|
||||
async raise(parent) {
|
||||
raise: async parent => {
|
||||
if(raised) return;
|
||||
raised = true;
|
||||
container.style.pointerEvents = null;
|
||||
|
@ -14,46 +16,41 @@ const MamiMessageBoxContainer = function() {
|
|||
if(!parent.contains(container))
|
||||
parent.appendChild(container);
|
||||
|
||||
animeAbort?.abort();
|
||||
animeAbort = new AbortController;
|
||||
const signal = animeAbort.signal;
|
||||
|
||||
container.style.opacity = '0';
|
||||
currAnim?.cancel();
|
||||
|
||||
currAnim = MamiAnimate({
|
||||
async: true,
|
||||
delayed: true,
|
||||
duration: 300,
|
||||
easing: 'outExpo',
|
||||
start: () => { container.style.opacity = '0'; },
|
||||
update: t => { container.style.opacity = t; },
|
||||
end: () => { container.style.opacity = null; },
|
||||
});
|
||||
try {
|
||||
await MamiAnimate({
|
||||
duration: 300,
|
||||
easing: 'outExpo',
|
||||
signal,
|
||||
update(t) {
|
||||
container.style.opacity = t;
|
||||
},
|
||||
});
|
||||
container.style.opacity = null;
|
||||
await currAnim.start();
|
||||
} catch(ex) {}
|
||||
},
|
||||
async dismiss(parent) {
|
||||
dismiss: async parent => {
|
||||
if(!raised) return;
|
||||
raised = false;
|
||||
container.style.pointerEvents = 'none';
|
||||
|
||||
animeAbort?.abort();
|
||||
animeAbort = new AbortController;
|
||||
const signal = animeAbort.signal;
|
||||
currAnim?.cancel();
|
||||
|
||||
currAnim = MamiAnimate({
|
||||
async: true,
|
||||
delayed: true,
|
||||
duration: 300,
|
||||
easing: 'outExpo',
|
||||
update: t => { container.style.opacity = 1 - t; },
|
||||
end: t => { parent.removeChild(container); },
|
||||
});
|
||||
try {
|
||||
await MamiAnimate({
|
||||
duration: 300,
|
||||
easing: 'outExpo',
|
||||
signal,
|
||||
update(t) {
|
||||
container.style.opacity = 1 - t;
|
||||
},
|
||||
});
|
||||
parent.removeChild(container);
|
||||
await currAnim.start();
|
||||
} catch(ex) {}
|
||||
},
|
||||
async show(dialog) {
|
||||
show: async dialog => {
|
||||
if(typeof dialog !== 'object' || dialog === null)
|
||||
throw 'dialog must be a non-null object';
|
||||
|
||||
|
@ -171,8 +168,6 @@ const MamiMessageBoxDialog = function(info) {
|
|||
if(buttons.childElementCount > 2)
|
||||
buttons.classList.add('msgbox-dialog-buttons-many');
|
||||
|
||||
const showAnimAbort = new AbortController;
|
||||
|
||||
return {
|
||||
get buttonCount() {
|
||||
return buttons.childElementCount;
|
||||
|
@ -210,36 +205,31 @@ const MamiMessageBoxDialog = function(info) {
|
|||
if(primaryButton instanceof HTMLButtonElement)
|
||||
primaryButton.focus();
|
||||
|
||||
dialog.style.pointerEvents = null;
|
||||
|
||||
MamiAnimate({
|
||||
async: true,
|
||||
duration: 500,
|
||||
easing: 'outElasticHalf',
|
||||
signal: showAnimAbort.signal,
|
||||
update: t => {
|
||||
dialog.style.transform = `scale(${t})`;
|
||||
},
|
||||
}).then(() => {
|
||||
dialog.style.transform = null;
|
||||
}).catch(ex => { });
|
||||
end: () => {
|
||||
dialog.style.transform = null;
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
dismiss: () => {
|
||||
showAnimAbort.abort();
|
||||
|
||||
dismiss: async () => {
|
||||
dialog.style.pointerEvents = 'none';
|
||||
|
||||
MamiAnimate({
|
||||
await MamiAnimate({
|
||||
async: true,
|
||||
duration: 600,
|
||||
easing: 'outExpo',
|
||||
update(t) {
|
||||
dialog.style.opacity = 1 - t;
|
||||
},
|
||||
}).catch(ex => { }).then(() => {
|
||||
dialog.style.opacity = '0';
|
||||
}).finally(() => {
|
||||
dialog.remove();
|
||||
update: t => { dialog.style.opacity = 1 - t; },
|
||||
end: () => { dialog.style.opacity = '0'; },
|
||||
});
|
||||
|
||||
$r(dialog);
|
||||
},
|
||||
cancel: () => {
|
||||
doReject();
|
||||
|
@ -247,10 +237,12 @@ const MamiMessageBoxDialog = function(info) {
|
|||
};
|
||||
};
|
||||
|
||||
const MamiMessageBoxControl = function({ parent }) {
|
||||
if(!(parent instanceof HTMLElement))
|
||||
throw new Error('parent argument must be an instance of HTMLElement');
|
||||
const MamiMessageBoxControl = function(options) {
|
||||
options = MamiArgs('options', options, define => {
|
||||
define('parent').required().constraint(value => value instanceof Element).done();
|
||||
});
|
||||
|
||||
const parent = options.parent;
|
||||
const container = new MamiMessageBoxContainer;
|
||||
const queue = [];
|
||||
let currentDialog;
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
const MamiTabsControl = function({ onAdd, onRemove, onSwitch }) {
|
||||
if(typeof onAdd !== 'function')
|
||||
throw new Error('onAdd argument must be a function');
|
||||
if(typeof onRemove !== 'function')
|
||||
throw new Error('onRemove argument must be a function');
|
||||
if(typeof onSwitch !== 'function')
|
||||
throw new Error('onSwitch argument must be a function');
|
||||
#include args.js
|
||||
#include uniqstr.js
|
||||
|
||||
const MamiTabsControl = function(options) {
|
||||
options = MamiArgs('options', options, define => {
|
||||
define('onAdd').required().type('function').done();
|
||||
define('onRemove').required().type('function').done();
|
||||
define('onSwitch').required().type('function').done();
|
||||
});
|
||||
|
||||
const onAdd = options.onAdd;
|
||||
const onRemove = options.onRemove;
|
||||
const onSwitch = options.onSwitch;
|
||||
|
||||
const tabs = new Map;
|
||||
let tabIdCounter = 0;
|
||||
let currentTab;
|
||||
|
||||
const extractElement = elementInfo => {
|
||||
|
@ -16,6 +21,8 @@ const MamiTabsControl = function({ onAdd, onRemove, onSwitch }) {
|
|||
element = elementInfo;
|
||||
} else if('element' in elementInfo) {
|
||||
element = elementInfo.element;
|
||||
} else if('getElement' in elementInfo) {
|
||||
element = elementInfo.getElement();
|
||||
} else throw 'elementInfo is not a valid type';
|
||||
|
||||
if(!(element instanceof Element))
|
||||
|
@ -119,7 +126,7 @@ const MamiTabsControl = function({ onAdd, onRemove, onSwitch }) {
|
|||
if(tabId !== undefined)
|
||||
throw 'tabInfo has already been added';
|
||||
|
||||
tabId = 't' + (++tabIdCounter).toString(16);
|
||||
tabId = MamiUniqueStr(8);
|
||||
tabs.set(tabId, tabInfo);
|
||||
|
||||
if(title !== 'string' && 'title' in tabInfo)
|
||||
|
|
|
@ -1,183 +0,0 @@
|
|||
const ThrobberIcon = function() {
|
||||
const element = <div class="throbber-icon"/>;
|
||||
for(let i = 0; i < 9; ++i)
|
||||
element.appendChild(<div class="throbber-icon-block"/>);
|
||||
|
||||
// this is moderately cursed but it'll do
|
||||
const blocks = [
|
||||
element.children[3],
|
||||
element.children[0],
|
||||
element.children[1],
|
||||
element.children[2],
|
||||
element.children[5],
|
||||
element.children[8],
|
||||
element.children[7],
|
||||
element.children[6],
|
||||
];
|
||||
|
||||
let tsLastUpdate;
|
||||
let counter = 0;
|
||||
let playing = false;
|
||||
let delay = 50;
|
||||
let playResolve;
|
||||
let pauseResolve;
|
||||
|
||||
const update = tsCurrent => {
|
||||
try {
|
||||
if(tsLastUpdate !== undefined && (tsCurrent - tsLastUpdate) < delay)
|
||||
return;
|
||||
tsLastUpdate = tsCurrent;
|
||||
|
||||
for(let i = 0; i < blocks.length; ++i)
|
||||
blocks[(counter + i) % blocks.length].classList.toggle('throbber-icon-block-hidden', i < 3);
|
||||
|
||||
++counter;
|
||||
} finally {
|
||||
if(playResolve)
|
||||
try {
|
||||
playResolve();
|
||||
} finally {
|
||||
playResolve = undefined;
|
||||
playing = true;
|
||||
}
|
||||
|
||||
if(pauseResolve)
|
||||
try {
|
||||
pauseResolve();
|
||||
} finally {
|
||||
pauseResolve = undefined;
|
||||
playing = false;
|
||||
}
|
||||
|
||||
if(playing)
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
};
|
||||
|
||||
const play = () => {
|
||||
return new Promise(resolve => {
|
||||
if(playing || playResolve) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
playResolve = resolve;
|
||||
requestAnimationFrame(update);
|
||||
});
|
||||
};
|
||||
const pause = () => {
|
||||
return new Promise(resolve => {
|
||||
if(!playing || pauseResolve) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
pauseResolve = resolve;
|
||||
});
|
||||
};
|
||||
const stop = async () => {
|
||||
await pause();
|
||||
counter = 0;
|
||||
};
|
||||
const restart = async () => {
|
||||
await stop();
|
||||
await play();
|
||||
};
|
||||
const reverse = () => {
|
||||
blocks.reverse();
|
||||
};
|
||||
const setBlock = (num, state=null) => {
|
||||
element.children[num].classList.toggle('throbber-icon-block-hidden', !state);
|
||||
};
|
||||
const batsu = () => {
|
||||
setBlock(0, true);setBlock(1, false);setBlock(2, true);
|
||||
setBlock(3, false);setBlock(4, true);setBlock(5, false);
|
||||
setBlock(6, true);setBlock(7, false);setBlock(8, true);
|
||||
};
|
||||
const maru = () => {
|
||||
setBlock(0, true);setBlock(1, true);setBlock(2, true);
|
||||
setBlock(3, true);setBlock(4, false);setBlock(5, true);
|
||||
setBlock(6, true);setBlock(7, true);setBlock(8, true);
|
||||
};
|
||||
|
||||
return {
|
||||
get element() { return element; },
|
||||
get playing() { return playing; },
|
||||
get delay() { return delay; },
|
||||
set delay(value) {
|
||||
if(typeof value !== 'number')
|
||||
value = parseFloat(value);
|
||||
if(isNaN(value) || !isFinite(value))
|
||||
return;
|
||||
if(value < 0)
|
||||
value = Math.abs(value);
|
||||
delay = value;
|
||||
},
|
||||
|
||||
play,
|
||||
pause,
|
||||
stop,
|
||||
restart,
|
||||
reverse,
|
||||
batsu,
|
||||
maru,
|
||||
};
|
||||
};
|
||||
|
||||
const Throbber = function(options=null) {
|
||||
if(typeof options !== 'object')
|
||||
throw 'options must be an object';
|
||||
|
||||
let {
|
||||
element, size, colour,
|
||||
width, height, inline,
|
||||
containerWidth, containerHeight,
|
||||
gap, margin, hidden, paused,
|
||||
} = options ?? {};
|
||||
|
||||
if(typeof element === 'string')
|
||||
element = document.querySelector(element);
|
||||
if(!(element instanceof HTMLElement))
|
||||
element = <div class="throbber"/>;
|
||||
|
||||
if(!element.classList.contains('throbber'))
|
||||
element.classList.add('throbber');
|
||||
if(inline)
|
||||
element.classList.add('throbber-inline');
|
||||
if(hidden)
|
||||
element.classList.add('hidden');
|
||||
|
||||
if(typeof size === 'number' && size > 0)
|
||||
element.style.setProperty('--throbber-size', size);
|
||||
if(typeof containerWidth === 'string')
|
||||
element.style.setProperty('--throbber-container-width', containerWidth);
|
||||
if(typeof containerHeight === 'string')
|
||||
element.style.setProperty('--throbber-container-height', containerHeight);
|
||||
if(typeof gap === 'string')
|
||||
element.style.setProperty('--throbber-gap', gap);
|
||||
if(typeof margin === 'string')
|
||||
element.style.setProperty('--throbber-margin', margin);
|
||||
if(typeof width === 'string')
|
||||
element.style.setProperty('--throbber-width', width);
|
||||
if(typeof height === 'string')
|
||||
element.style.setProperty('--throbber-height', height);
|
||||
if(typeof colour === 'string')
|
||||
element.style.setProperty('--throbber-colour', colour);
|
||||
|
||||
let icon;
|
||||
if(element.childElementCount < 1) {
|
||||
icon = new ThrobberIcon;
|
||||
if(!paused) icon.play();
|
||||
element.appendChild(<div class="throbber-frame">{icon}</div>);
|
||||
}
|
||||
|
||||
return {
|
||||
get element() { return element; },
|
||||
|
||||
get hasIcon() { return icon !== undefined; },
|
||||
get icon() { return icon; },
|
||||
|
||||
get visible() { return !element.classList.contains('hidden'); },
|
||||
set visible(state) { element.classList.toggle('hidden', !state); },
|
||||
};
|
||||
};
|
|
@ -1,8 +1,13 @@
|
|||
const MamiViewsControl = function({ body }) {
|
||||
if(!(body instanceof HTMLElement))
|
||||
throw new Error('body argument must be an instance of HTMLElement');
|
||||
#include args.js
|
||||
#include utility.js
|
||||
|
||||
body.classList.toggle('views', true);
|
||||
const MamiViewsControl = function(options) {
|
||||
options = MamiArgs('options', options, define => {
|
||||
define('body').required().constraint(value => value instanceof Element).done();
|
||||
});
|
||||
|
||||
const targetBody = options.body;
|
||||
targetBody.classList.toggle('views', true);
|
||||
|
||||
const views = [];
|
||||
|
||||
|
@ -12,12 +17,14 @@ const MamiViewsControl = function({ body }) {
|
|||
element = elementInfo;
|
||||
} else if('element' in elementInfo) {
|
||||
element = elementInfo.element;
|
||||
} else if('getElement' in elementInfo) {
|
||||
element = elementInfo.getElement();
|
||||
} else throw 'elementInfo is not a valid type';
|
||||
|
||||
if(!(element instanceof Element))
|
||||
throw 'element is not an instance of Element';
|
||||
if(element === body)
|
||||
throw 'element may not be the same as the body argument';
|
||||
if(element === targetBody)
|
||||
throw 'element may not be the same as targetBody';
|
||||
|
||||
return element;
|
||||
};
|
||||
|
@ -42,8 +49,8 @@ const MamiViewsControl = function({ body }) {
|
|||
element.classList.toggle('views-background', false);
|
||||
element.classList.toggle('hidden', false);
|
||||
|
||||
if(!body.contains(element))
|
||||
body.appendChild(element);
|
||||
if(!targetBody.contains(element))
|
||||
targetBody.appendChild(element);
|
||||
|
||||
if(typeof elementInfo.onViewForeground === 'function')
|
||||
await elementInfo.onViewForeground();
|
||||
|
@ -114,8 +121,8 @@ const MamiViewsControl = function({ body }) {
|
|||
if(typeof elementInfo.onViewBackground === 'function')
|
||||
await elementInfo.onViewBackground();
|
||||
|
||||
if(body.contains(element))
|
||||
body.removeChild(element);
|
||||
if(targetBody.contains(element))
|
||||
targetBody.removeChild(element);
|
||||
|
||||
if(typeof elementInfo.onViewPop === 'function')
|
||||
await elementInfo.onViewPop();
|
||||
|
@ -177,8 +184,8 @@ const MamiViewsControl = function({ body }) {
|
|||
element.classList.toggle('views-background', true);
|
||||
element.classList.toggle('hidden', true);
|
||||
|
||||
if(!body.contains(element))
|
||||
body.appendChild(element);
|
||||
if(!targetBody.contains(element))
|
||||
targetBody.appendChild(element);
|
||||
|
||||
updateZIncides();
|
||||
},
|
||||
|
@ -189,8 +196,8 @@ const MamiViewsControl = function({ body }) {
|
|||
const elementInfo = views.shift();
|
||||
const element = extractElement(elementInfo);
|
||||
|
||||
if(body.contains(element))
|
||||
body.removeChild(element);
|
||||
if(targetBody.contains(element))
|
||||
targetBody.removeChild(element);
|
||||
|
||||
if(typeof elementInfo.onViewPop === 'function')
|
||||
await elementInfo.onViewPop();
|
||||
|
|
|
@ -14,124 +14,82 @@ const MamiEasings = (() => {
|
|||
const elasticOffsetQuarter = Math.pow(2, -10) * Math.sin((.25 - elasticConst2) * elasticConst);
|
||||
const inOutElasticOffset = Math.pow(2, -10) * Math.sin((1 - elasticConst2 * 1.5) * elasticConst / 1.5);
|
||||
|
||||
return {
|
||||
linear(t) {
|
||||
return t;
|
||||
},
|
||||
let outBounce;
|
||||
|
||||
inQuad(t) {
|
||||
return t * t;
|
||||
},
|
||||
outQuad(t) {
|
||||
return t * (2 - t);
|
||||
},
|
||||
inOutQuad(t) {
|
||||
return {
|
||||
linear: t => t,
|
||||
|
||||
inQuad: t => t * t,
|
||||
outQuad: t => t * (2 - t),
|
||||
inOutQuad: t => {
|
||||
if(t < .5)
|
||||
return t * t * 2;
|
||||
return --t * t * -2 + 1;
|
||||
},
|
||||
|
||||
inCubic(t) {
|
||||
return t * t * t;
|
||||
},
|
||||
outCubic(t) {
|
||||
return --t * t * t + 1;
|
||||
},
|
||||
inOutCubic(t) {
|
||||
inCubic: t => t * t * t,
|
||||
outCubic: t => --t * t * t + 1,
|
||||
inOutCubic: t => {
|
||||
if(t < .5)
|
||||
return t * t * t * 4;
|
||||
return --t * t * t * 4 + 1;
|
||||
},
|
||||
|
||||
inQuart(t) {
|
||||
return t * t * t * t;
|
||||
},
|
||||
outQuart(t) {
|
||||
return 1 - --t * t * t * t;
|
||||
},
|
||||
inOutQuart(t) {
|
||||
inQuart: t => t * t * t * t,
|
||||
outQuart: t => 1 - --t * t * t * t,
|
||||
inOutQuart: t => {
|
||||
if(t < .5)
|
||||
return t * t * t * t * 8;
|
||||
return --t * t * t * t * -8 + 1;
|
||||
},
|
||||
|
||||
inQuint(t) {
|
||||
return t * t * t * t * t;
|
||||
},
|
||||
outQuint(t) {
|
||||
return --t * t * t * t * t + 1;
|
||||
},
|
||||
inOutQuint(t) {
|
||||
inQuint: t => t * t * t * t * t,
|
||||
outQuint: t => --t * t * t * t * t + 1,
|
||||
inOutQuint: t => {
|
||||
if(t < .5)
|
||||
return t * t * t * t * t * 16;
|
||||
return --t * t * t * t * t * 16 + 1;
|
||||
},
|
||||
|
||||
inSine(t) {
|
||||
return 1 - Math.cos(t * Math.PI * .5);
|
||||
},
|
||||
outSine(t) {
|
||||
return Math.sin(t * Math.PI * .5);
|
||||
},
|
||||
inOutSine(t) {
|
||||
return .5 - .5 * Math.cos(Math.PI * t);
|
||||
},
|
||||
inSine: t => 1 - Math.cos(t * Math.PI * .5),
|
||||
outSine: t => Math.sin(t * Math.PI * .5),
|
||||
inOutSine: t => .5 - .5 * Math.cos(Math.PI * t),
|
||||
|
||||
inCirc(t) {
|
||||
return 1 - Math.sqrt(1 - t * t);
|
||||
},
|
||||
outCirc(t) {
|
||||
return Math.sqrt(1 - --t * t);
|
||||
},
|
||||
inOutCirc(t) {
|
||||
inCirc: t => 1 - Math.sqrt(1 - t * t),
|
||||
outCirc: t => Math.sqrt(1 - --t * t),
|
||||
inOutCirc: t => {
|
||||
if((t *= 2) < 1)
|
||||
return .5 - .5 * Math.sqrt(1 - t * t);
|
||||
return .5 * Math.sqrt(1 - (t -= 2) * t) + .5;
|
||||
},
|
||||
|
||||
inElastic(t) {
|
||||
return -Math.pow(2, -10 + 10 * t) * Math.sin((1 - elasticConst2 - t) * elasticConst) + elasticOffsetFull * (1 - t);
|
||||
},
|
||||
outElastic(t) {
|
||||
return Math.pow(2, -10 * t) * Math.sin((t - elasticConst2) * elasticConst) + 1 - elasticOffsetFull * t;
|
||||
},
|
||||
outElasticHalf(t) {
|
||||
return Math.pow(2, -10 * t) * Math.sin((.5 * t - elasticConst2) * elasticConst) + 1 - elasticOffsetHalf * t;
|
||||
},
|
||||
outElasticQuarter(t) {
|
||||
return Math.pow(2, -10 * t) * Math.sin((.25 * t - elasticConst2) * elasticConst) + 1 - elasticOffsetQuarter * t;
|
||||
},
|
||||
inOutElastic(t) {
|
||||
inElastic: t => -Math.pow(2, -10 + 10 * t) * Math.sin((1 - elasticConst2 - t) * elasticConst) + elasticOffsetFull * (1 - t),
|
||||
outElastic: t => Math.pow(2, -10 * t) * Math.sin((t - elasticConst2) * elasticConst) + 1 - elasticOffsetFull * t,
|
||||
outElasticHalf: t => Math.pow(2, -10 * t) * Math.sin((.5 * t - elasticConst2) * elasticConst) + 1 - elasticOffsetHalf * t,
|
||||
outElasticQuarter: t => Math.pow(2, -10 * t) * Math.sin((.25 * t - elasticConst2) * elasticConst) + 1 - elasticOffsetQuarter * t,
|
||||
inOutElastic: t => {
|
||||
if((t *= 2) < 1)
|
||||
return -.5 * (Math.pow(2, -10 + 10 * t) * Math.sin((1 - elasticConst2 * 1.5 - t) * elasticConst / 1.5) - inOutElasticOffset * (1 - t));
|
||||
return .5 * (Math.pow(2, -10 * --t) * Math.sin((t - elasticConst2 * 1.5) * elasticConst / 1.5) - inOutElasticOffset * t) + 1;
|
||||
},
|
||||
|
||||
inExpo(t) {
|
||||
return Math.pow(2, 10 * (t - 1)) + expoOffset * (t - 1);
|
||||
},
|
||||
outExpo(t) {
|
||||
return -Math.pow(2, -10 * t) + 1 + expoOffset * t;
|
||||
},
|
||||
inOutExpo(t) {
|
||||
inExpo: t => Math.pow(2, 10 * (t - 1)) + expoOffset * (t - 1),
|
||||
outExpo: t => -Math.pow(2, -10 * t) + 1 + expoOffset * t,
|
||||
inOutExpo: t => {
|
||||
if(t < .5)
|
||||
return .5 * (Math.pow(2, 20 * t - 10)) + expoOffset * (2 * t - 1);
|
||||
return 1 - .5 * (Math.pow(2, -20 * t + 10)) + expoOffset * (-2 * t + 1);
|
||||
},
|
||||
|
||||
inBack(t) {
|
||||
return t * t * ((backConst + 1) * t - backConst);
|
||||
},
|
||||
outBack(t) {
|
||||
return --t * t * ((backConst + 1) * t + backConst) + 1;
|
||||
},
|
||||
inOutBack(t) {
|
||||
inBack: t => t * t * ((backConst + 1) * t - backConst),
|
||||
outBack: t => --t * t * ((backConst + 1) * t + backConst) + 1,
|
||||
inOutBack: t => {
|
||||
if((t *= 2) < 1)
|
||||
return .5 * t * t * ((backConst2 + 1) * t - backConst2);
|
||||
return .5 * ((t -= 2) * t * ((backConst2 + 1) * t + backConst2) + 2);
|
||||
},
|
||||
|
||||
inBounce(t) {
|
||||
inBounce: t => {
|
||||
t = 1 - t;
|
||||
if(t < bounceConst)
|
||||
return 1 - 7.5625 * t * t;
|
||||
|
@ -141,23 +99,23 @@ const MamiEasings = (() => {
|
|||
return 1 - (7.5625 * (t -= 2.25 * bounceConst) * t + .9375);
|
||||
return 1 - (7.5625 * (t -= 2.625 * bounceConst) * t + .984375);
|
||||
},
|
||||
outBounce(t) {
|
||||
if(t < bounceConst)
|
||||
return 7.5625 * t * t;
|
||||
if(t < 2 * bounceConst)
|
||||
return 7.5625 * (t -= 1.5 * bounceConst) * t + .75;
|
||||
if(t < 2.5 * bounceConst)
|
||||
return 7.5625 * (t -= 2.25 * bounceConst) * t + .9375;
|
||||
return 7.5625 * (t -= 2.625 * bounceConst) * t + .984375;
|
||||
},
|
||||
inOutBounce(t) {
|
||||
outBounce: (() => {
|
||||
return outBounce = t => {
|
||||
if(t < bounceConst)
|
||||
return 7.5625 * t * t;
|
||||
if(t < 2 * bounceConst)
|
||||
return 7.5625 * (t -= 1.5 * bounceConst) * t + .75;
|
||||
if(t < 2.5 * bounceConst)
|
||||
return 7.5625 * (t -= 2.25 * bounceConst) * t + .9375;
|
||||
return 7.5625 * (t -= 2.625 * bounceConst) * t + .984375;
|
||||
};
|
||||
})(),
|
||||
inOutBounce: t => {
|
||||
if(t < .5)
|
||||
return .5 - .5 * outBounce(1 - t * 2);
|
||||
return outBounce((t - .5) * 2) * .5 + .5;
|
||||
},
|
||||
|
||||
outPow10(t) {
|
||||
return --t * Math.pow(t, 10) + 1;
|
||||
},
|
||||
outPow10: t => --t * Math.pow(t, 10) + 1,
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -1,82 +1,37 @@
|
|||
const MamiEEPROM = function(endPoint) {
|
||||
if(typeof endPoint !== 'string')
|
||||
throw 'endPoint must be a string';
|
||||
#include eeprom/script.jsx
|
||||
|
||||
const MamiEEPROM = function(baseUrl, getAuthLine) {
|
||||
if(typeof baseUrl !== 'string')
|
||||
throw 'baseUrl must be a string';
|
||||
if(typeof getAuthLine !== 'function')
|
||||
throw 'getAuthLine must be a function';
|
||||
|
||||
// when the pools rewrite happen, retrieve this from futami common
|
||||
const appId = '1';
|
||||
let client;
|
||||
|
||||
return {
|
||||
create(fileInput) {
|
||||
if(!(fileInput instanceof File))
|
||||
throw 'fileInput must be an instance of window.File';
|
||||
|
||||
const abort = new AbortController;
|
||||
|
||||
return {
|
||||
abort() { abort.abort(); },
|
||||
async start(progress) {
|
||||
if(abort.signal.aborted)
|
||||
throw 'File upload was cancelled by the user, it cannot be restarted.';
|
||||
|
||||
const reportProgress = typeof progress !== 'function' ? undefined : ev => {
|
||||
progress({
|
||||
loaded: ev.loaded,
|
||||
total: ev.total,
|
||||
progress: ev.total <= 0 ? 0 : ev.loaded / ev.total,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append('src', appId);
|
||||
formData.append('file', fileInput);
|
||||
|
||||
const { status, body } = await $xhr.post(`${endPoint}/uploads`, {
|
||||
type: 'json',
|
||||
authed: true,
|
||||
upload: reportProgress,
|
||||
signal: abort.signal,
|
||||
}, formData);
|
||||
if(body === null)
|
||||
throw "The upload server didn't return the metadata for some reason.";
|
||||
|
||||
if(status !== 201)
|
||||
throw body.english ?? body.error ?? `Upload failed with status code ${status}`;
|
||||
|
||||
body.isImage = () => body.type.startsWith('image/');
|
||||
body.isVideo = () => body.type.startsWith('video/');
|
||||
body.isAudio = () => body.type.startsWith('audio/');
|
||||
body.isMedia = () => body.isImage() || body.isAudio() || body.isVideo();
|
||||
|
||||
return Object.freeze(body);
|
||||
} catch(ex) {
|
||||
if(ex.aborted || abort.signal.aborted)
|
||||
throw '';
|
||||
|
||||
console.error(ex);
|
||||
throw ex;
|
||||
}
|
||||
},
|
||||
};
|
||||
get client() {
|
||||
return client;
|
||||
},
|
||||
|
||||
async delete(fileInfo) {
|
||||
if(typeof fileInfo !== 'object')
|
||||
throw 'fileInfo must be an object';
|
||||
if(typeof fileInfo.urlf !== 'string')
|
||||
throw 'fileInfo.urlf must be a string';
|
||||
init: async () => {
|
||||
await MamiEEPROMLoadScript(baseUrl);
|
||||
client = new EEPROM(appId, baseUrl, getAuthLine);
|
||||
},
|
||||
|
||||
const { status, body } = await $xhr.delete(fileInfo.urlf, {
|
||||
type: 'json',
|
||||
authed: true,
|
||||
});
|
||||
create: fileInput => {
|
||||
if(client === undefined)
|
||||
throw 'eeprom client is uninitialised';
|
||||
|
||||
if(status !== 204) {
|
||||
if(body === null)
|
||||
throw `Delete failed with status code ${status}`;
|
||||
return client.create(fileInput);
|
||||
},
|
||||
|
||||
throw body.english ?? body.error ?? `Delete failed with status code ${status}`;
|
||||
}
|
||||
delete: async fileInfo => {
|
||||
if(client === undefined)
|
||||
throw 'eeprom client is uninitialised';
|
||||
|
||||
await client.delete(fileInfo);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
19
src/mami.js/eeprom/script.jsx
Normal file
19
src/mami.js/eeprom/script.jsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
#include utility.js
|
||||
|
||||
const MamiEEPROMLoadScript = baseUrl => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(typeof baseUrl !== 'string')
|
||||
throw 'baseUrl must be a string';
|
||||
|
||||
const src = `${baseUrl}/scripts/eepromv1a.js`;
|
||||
|
||||
let script = $q(`script[src="${src}"]`);
|
||||
if(script instanceof Element) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
script = <script src={src} onload={() => { resolve(); }} onerror={() => { $r(script); reject(); }} />;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
};
|
|
@ -9,37 +9,46 @@ const MamiEmotes = (function() {
|
|||
emotes.push(emote);
|
||||
};
|
||||
|
||||
const load = function(batch) {
|
||||
for(const emote of batch)
|
||||
add(emote);
|
||||
const addLegacy = function(emoteOld) {
|
||||
const emote = {
|
||||
url: emoteOld.Image,
|
||||
minRank: emoteOld.Hierarchy,
|
||||
strings: [],
|
||||
};
|
||||
for(let i = 0; i < emoteOld.Text.length; ++i)
|
||||
emote.strings.push(emoteOld.Text[i].slice(1, -1));
|
||||
add(emote);
|
||||
};
|
||||
|
||||
return {
|
||||
clear,
|
||||
add,
|
||||
load,
|
||||
async loadApi(flashii, fresh=false) {
|
||||
const emotes = await flashii.v1.emotes({ fields: ['url', 'strings', 'min_rank'], fresh });
|
||||
clear();
|
||||
load(emotes);
|
||||
clear: clear,
|
||||
add: add,
|
||||
addLegacy: addLegacy,
|
||||
load: function(batch) {
|
||||
for(const emote of batch)
|
||||
add(emote);
|
||||
},
|
||||
forEach(minRank, callback) {
|
||||
loadLegacy: function(batch) {
|
||||
for(const emote of batch)
|
||||
addLegacy(emote);
|
||||
},
|
||||
forEach: function(minRank, callback) {
|
||||
for(const emote of emotes)
|
||||
if(!emote.min_rank || emote.min_rank <= minRank)
|
||||
if(emote.minRank <= minRank)
|
||||
callback(emote);
|
||||
},
|
||||
all(minRank) {
|
||||
all: minRank => {
|
||||
const items = [];
|
||||
for(const emote of emotes)
|
||||
if(!emote.min_rank || emote.min_rank <= minRank)
|
||||
if(emote.minRank <= minRank)
|
||||
items.push(emote);
|
||||
|
||||
return items;
|
||||
},
|
||||
findByName(minRank, name, returnString) {
|
||||
findByName: function(minRank, name, returnString) {
|
||||
const found = [];
|
||||
for(const emote of emotes)
|
||||
if(!emote.min_rank || emote.min_rank <= minRank) {
|
||||
if(emote.minRank <= minRank) {
|
||||
for(const string of emote.strings)
|
||||
if(string.indexOf(name) === 0) {
|
||||
found.push(returnString ? string : emote);
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
const MamiEmotePicker = function({ getEmotes, onPick, onClose=null, setKeepOpenOnPick=null }) {
|
||||
if(typeof getEmotes !== 'function')
|
||||
throw new Error('getEmotes must be a function');
|
||||
if(typeof onPick !== 'function')
|
||||
throw new Error('onPick must be a function');
|
||||
#include args.js
|
||||
#include utility.js
|
||||
|
||||
const MamiEmotePicker = function(args) {
|
||||
args = MamiArgs('args', args, define => {
|
||||
define('getEmotes').type('function').done();
|
||||
define('setKeepOpenOnPick').type('function').done();
|
||||
define('onPick').type('function').done();
|
||||
define('onClose').type('function').done();
|
||||
});
|
||||
|
||||
let emotes;
|
||||
|
||||
let listElem, searchElem, keepOpenToggleElem;
|
||||
const html = <div class="emopick" style="z-index: 9001;" onfocusout={ev => {
|
||||
if(!keepOpenToggleElem.checked && !html.contains(ev.relatedTarget))
|
||||
|
@ -19,8 +25,8 @@ const MamiEmotePicker = function({ getEmotes, onPick, onClose=null, setKeepOpenO
|
|||
<div class="emopick-actions" tabindex="0">
|
||||
<label class="emopick-action-toggle" tabindex="0">
|
||||
{keepOpenToggleElem = <input type="checkbox" class="emopick-action-toggle-box" onchange={() => {
|
||||
if(typeof setKeepOpenOnPick === 'function')
|
||||
setKeepOpenOnPick(keepOpenToggleElem.checked);
|
||||
if(args.setKeepOpenOnPick !== undefined)
|
||||
args.setKeepOpenOnPick(keepOpenToggleElem.checked);
|
||||
}} />}
|
||||
<span class="emopick-action-toggle-label">Keep picker open</span>
|
||||
</label>
|
||||
|
@ -29,11 +35,11 @@ const MamiEmotePicker = function({ getEmotes, onPick, onClose=null, setKeepOpenO
|
|||
</div>;
|
||||
|
||||
const buildList = () => {
|
||||
$removeChildren(listElem);
|
||||
$rc(listElem);
|
||||
|
||||
for(const emote of emotes)
|
||||
listElem.appendChild(<button class="emopick-emote" type="button" title={`:${emote.strings[0]}:`} data-strings={emote.strings.join(' ')} onclick={() => {
|
||||
onPick(emote);
|
||||
args.onPick(emote);
|
||||
if(!keepOpenToggleElem.checked)
|
||||
close();
|
||||
}}>
|
||||
|
@ -84,8 +90,8 @@ const MamiEmotePicker = function({ getEmotes, onPick, onClose=null, setKeepOpenO
|
|||
promiseResolve = undefined;
|
||||
}
|
||||
|
||||
if(typeof onClose === 'function')
|
||||
onClose();
|
||||
if(args.onClose !== undefined)
|
||||
args.onClose();
|
||||
|
||||
html.classList.add('hidden');
|
||||
};
|
||||
|
@ -105,38 +111,55 @@ const MamiEmotePicker = function({ getEmotes, onPick, onClose=null, setKeepOpenO
|
|||
setPosition: setPosition,
|
||||
close: close,
|
||||
dialog: pos => {
|
||||
emotes = getEmotes();
|
||||
buildList();
|
||||
const dialogBody = () => {
|
||||
buildList();
|
||||
|
||||
html.classList.remove('hidden');
|
||||
html.classList.add('invisible');
|
||||
html.classList.remove('hidden');
|
||||
|
||||
if(pos instanceof MouseEvent)
|
||||
pos = pos.target;
|
||||
if(pos instanceof Element)
|
||||
pos = pos.getBoundingClientRect();
|
||||
if(pos instanceof DOMRect) {
|
||||
const bbb = pos;
|
||||
pos = {};
|
||||
if(pos instanceof MouseEvent)
|
||||
pos = pos.target;
|
||||
if(pos instanceof Element)
|
||||
pos = pos.getBoundingClientRect();
|
||||
if(pos instanceof DOMRect) {
|
||||
const bbb = pos;
|
||||
pos = {};
|
||||
|
||||
const mbb = html.getBoundingClientRect();
|
||||
const pbb = html.parentNode.getBoundingClientRect();
|
||||
const mbb = html.getBoundingClientRect();
|
||||
const pbb = html.parentNode.getBoundingClientRect();
|
||||
|
||||
pos.right = pbb.width - bbb.left;
|
||||
pos.bottom = pbb.height - bbb.top;
|
||||
pos.right = pbb.width - bbb.left;
|
||||
pos.bottom = pbb.height - bbb.top;
|
||||
|
||||
if(pos.right + mbb.width > pbb.width)
|
||||
pos.right = 0;
|
||||
else if(pos.right > mbb.width)
|
||||
pos.right -= mbb.width;
|
||||
else
|
||||
pos.right -= bbb.width;
|
||||
if(pos.right + mbb.width > pbb.width)
|
||||
pos.right = 0;
|
||||
else if(pos.right > mbb.width)
|
||||
pos.right -= mbb.width;
|
||||
else
|
||||
pos.right -= bbb.width;
|
||||
}
|
||||
|
||||
if(pos !== undefined)
|
||||
setPosition(pos);
|
||||
|
||||
html.classList.remove('invisible');
|
||||
searchElem.focus();
|
||||
};
|
||||
|
||||
const result = args.getEmotes();
|
||||
if(result instanceof Promise)
|
||||
result.then(list => {
|
||||
emotes = list;
|
||||
}).catch(() => {
|
||||
// uhhh just use the last version of the list LOL
|
||||
}).finally(() => {
|
||||
dialogBody();
|
||||
});
|
||||
else {
|
||||
emotes = result;
|
||||
dialogBody();
|
||||
}
|
||||
|
||||
if(pos !== undefined)
|
||||
setPosition(pos);
|
||||
|
||||
searchElem.focus();
|
||||
|
||||
return new Promise(resolve => { promiseResolve = resolve; });
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
const MamiIsEventTarget = value => {
|
||||
return value !== null
|
||||
&& typeof value === 'object'
|
||||
&& typeof value.scopeTo === 'function'
|
||||
&& typeof value.create === 'function'
|
||||
&& typeof value.watch === 'function'
|
||||
&& typeof value.unwatch === 'function'
|
||||
&& typeof value.dispatch === 'function';
|
||||
if(typeof value !== 'object' || value === null)
|
||||
return false;
|
||||
|
||||
if(typeof value.scopeTo !== 'function'
|
||||
|| typeof value.create !== 'function'
|
||||
|| typeof value.watch !== 'function'
|
||||
|| typeof value.unwatch !== 'function'
|
||||
|| typeof value.dispatch !== 'function')
|
||||
return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const MamiEventTargetScoped = function(eventTarget, prefix) {
|
||||
|
|
|
@ -1,270 +0,0 @@
|
|||
#include xhr.js
|
||||
|
||||
const Flashii = function(baseUrl) {
|
||||
if(typeof baseUrl !== 'string')
|
||||
throw new Error('baseUrl must be a string');
|
||||
if(baseUrl.startsWith('//'))
|
||||
baseUrl = window.location.protocol + baseUrl;
|
||||
|
||||
const sortArray = typeof Array.prototype.toSorted === 'function'
|
||||
? array => array.toSorted()
|
||||
: array => {
|
||||
array = array.slice();
|
||||
array.sort();
|
||||
return array;
|
||||
};
|
||||
|
||||
const convertFields = fields => {
|
||||
if(fields === true)
|
||||
return '*';
|
||||
|
||||
if(Array.isArray(fields))
|
||||
return sortArray(fields).join(',');
|
||||
|
||||
if(typeof fields === 'string')
|
||||
return fields;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const createUrl = (path, fields=null) => {
|
||||
const url = new URL(baseUrl + path);
|
||||
|
||||
if(fields)
|
||||
url.searchParams.set('fields', convertFields(fields));
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
const send = async ({
|
||||
method,
|
||||
path,
|
||||
fresh=false,
|
||||
params=null,
|
||||
fields=null,
|
||||
body=null,
|
||||
headers=null,
|
||||
type='json',
|
||||
authed=false,
|
||||
}) => {
|
||||
const url = createUrl(path, fields);
|
||||
if(params)
|
||||
for(const name in params) {
|
||||
if(name === 'fields' || params[name] === null || params[name] === undefined)
|
||||
continue;
|
||||
|
||||
url.searchParams.set(name, params[name]);
|
||||
}
|
||||
|
||||
headers ??= {};
|
||||
if(fresh) headers['Cache-Control'] = 'no-cache';
|
||||
|
||||
return await $xhr.send(method, url, { type, headers, authed }, body);
|
||||
};
|
||||
|
||||
const fii = {};
|
||||
|
||||
fii.v1 = {};
|
||||
|
||||
const verifyChatServerId = id => {
|
||||
if(typeof id === 'number')
|
||||
id = id.toString();
|
||||
if(/^([^0-9]+)$/gu.test(id))
|
||||
throw new Error('id argument is not an acceptable chat server id.', { cause: 'flashii.v1.chat.servers.id' });
|
||||
return id;
|
||||
};
|
||||
|
||||
fii.v1.chat = {};
|
||||
fii.v1.chat.login = function({ redirect=null, assign=true }={}) {
|
||||
return new Promise(resolve => {
|
||||
redirect ??= `${location.protocol}//${location.host}`;
|
||||
if(typeof redirect !== 'string')
|
||||
throw new Error('redirect must a string.', { cause: 'flashii.v1.chat.login.redirect' });
|
||||
|
||||
const url = createUrl('/v1/chat/login');
|
||||
url.searchParams.set('redirect', redirect);
|
||||
|
||||
// intentionally does not resolve
|
||||
if(assign)
|
||||
location.assign(url);
|
||||
else
|
||||
resolve(url);
|
||||
});
|
||||
};
|
||||
fii.v1.chat.servers = async function({ proto=null, secure=null, fields=null, fresh=false }={}) {
|
||||
const params = {};
|
||||
|
||||
if(proto !== null && typeof proto !== 'string')
|
||||
throw new Error('proto must be a string or null', { cause: 'flashii.v1.chat.servers.proto' });
|
||||
params['proto'] = proto;
|
||||
|
||||
if(secure !== null)
|
||||
params['secure'] = secure ? '1' : '0';
|
||||
|
||||
const { status, body } = await send({ method: 'GET', path: '/v1/chat/servers', fields, fresh, params });
|
||||
|
||||
if(status === 400)
|
||||
throw new Error('An argument contains an unsupported value.', { cause: 'flashii.v1.value' });
|
||||
if(status > 299)
|
||||
throw new Error(`Failed to fetch chat server list with error code ${status}.`, { cause: `flashii.http${status}` });
|
||||
|
||||
return body;
|
||||
};
|
||||
fii.v1.chat.servers.server = async function({ id, fields=null, fresh=false }) {
|
||||
id = verifyChatServerId(id);
|
||||
|
||||
const { status, body } = await send({ method: 'GET', path: `/v1/chat/servers/${id}`, fields, fresh });
|
||||
|
||||
if(status === 400)
|
||||
throw new Error('Fields argument contains unsupported value.', { cause: 'flashii.v1.value' });
|
||||
if(status === 404)
|
||||
throw new Error('No chat server with that id could be found.', { cause: 'flashii.v1.notfound' });
|
||||
if(status > 299)
|
||||
throw new Error(`Failed to fetch chat server with error code ${status}.`, { cause: `flashii.http${status}` });
|
||||
|
||||
return body;
|
||||
};
|
||||
fii.v1.chat.servers.recommended = async function({ proto, secure=null, fields=null, fresh=false }) {
|
||||
if(typeof proto !== 'string')
|
||||
throw new Error('proto must be a string');
|
||||
|
||||
const params = { proto };
|
||||
if(secure !== null)
|
||||
params['secure'] = secure ? '1' : '0';
|
||||
|
||||
const { status, body } = await send({ method: 'GET', path: '/v1/chat/servers/recommended', fields, fresh, params });
|
||||
|
||||
if(status === 400)
|
||||
throw new Error('An argument contains an unsupported value.', { cause: 'flashii.v1.value' });
|
||||
if(status === 404)
|
||||
throw new Error('Was unable to determine a server based on the requested parameters.', { cause: 'flashii.v1.notfound' });
|
||||
if(status > 299)
|
||||
throw new Error(`Failed to fetch recommended chat server with error code ${status}.`, { cause: `flashii.http${status}` });
|
||||
|
||||
return body;
|
||||
};
|
||||
fii.v1.chat.token = async function() {
|
||||
const { status, body } = await send({ method: 'GET', path: '/v1/chat/token', authed: true, fresh: true });
|
||||
|
||||
if(status === 403)
|
||||
throw new Error('You must be logged in to use chat.', { cause: 'flashii.v1.unauthorized' });
|
||||
if(status > 299)
|
||||
throw new Error(`Failed to fetch authorization token with error code ${status}.`, { cause: `flashii.http${status}` });
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const verifyColourPresetName = name => {
|
||||
if(/^([^A-Za-z0-9\-_]+)$/gu.test(name))
|
||||
throw new Error('name argument is not an acceptable colour preset name.');
|
||||
return name;
|
||||
};
|
||||
|
||||
fii.v1.colours = {};
|
||||
fii.v1.colours.presets = async function({ fields=null, fresh=false }={}) {
|
||||
const { status, body } = await send({ method: 'GET', path: '/v1/colours/presets', fields, fresh });
|
||||
|
||||
if(status === 400)
|
||||
throw new Error('Fields argument contains unsupported value.', { cause: 'flashii.v1.value' });
|
||||
if(status > 299)
|
||||
throw new Error(`Failed to fetch colour presets with error code ${status}.`, { cause: `flashii.http${status}` });
|
||||
|
||||
return body;
|
||||
};
|
||||
fii.v1.colours.presets.preset = async function({ name, fields=null, fresh=false }) {
|
||||
name = verifyColourPresetName(name);
|
||||
|
||||
const { status, body } = await send({ method: 'GET', path: `/v1/colours/presets/${name}`, fields, fresh });
|
||||
|
||||
if(status === 400)
|
||||
throw new Error('Fields argument contains unsupported value.', { cause: 'flashii.v1.value' });
|
||||
if(status === 404)
|
||||
throw new Error('Requested colour preset does not exist.', { cause: 'flashii.v1.notfound' });
|
||||
if(status > 299)
|
||||
throw new Error(`Failed to fetch colour preset "${name}" with error code ${status}.`, { cause: `flashii.http${status}` });
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const verifyEmoticonId = id => {
|
||||
if(typeof id === 'number')
|
||||
id = id.toString();
|
||||
if(/^([^0-9]+)$/gu.test(id))
|
||||
throw new Error('id argument is not an acceptable emoticon id.', { cause: 'flashii.v1.emotes.id' });
|
||||
return id;
|
||||
};
|
||||
|
||||
fii.v1.emotes = async function({ fields=null, fresh=false }={}) {
|
||||
const { status, body } = await send({ method: 'GET', path: '/v1/emotes', fields, fresh });
|
||||
|
||||
if(status === 400)
|
||||
throw new Error('Fields argument contains unsupported value.', { cause: 'flashii.v1.value' });
|
||||
if(status > 299)
|
||||
throw new Error(`Failed to fetch emoticons with error code ${status}.`, { cause: `flashii.http${status}` });
|
||||
|
||||
return body;
|
||||
};
|
||||
fii.v1.emotes.emote = async function({ id, fields=null, fresh=false }) {
|
||||
id = verifyEmoticonId(id);
|
||||
|
||||
const { status, body } = await send({ method: 'GET', path: `/v1/emotes/${id}`, fields, fresh });
|
||||
|
||||
if(status === 400)
|
||||
throw new Error('Fields argument contains unsupported value.', { cause: 'flashii.v1.value' });
|
||||
if(status === 404)
|
||||
throw new Error('Requested emoticon does not exist.', { cause: 'flashii.v1.notfound' });
|
||||
if(status > 299)
|
||||
throw new Error(`Failed to fetch emoticon "${id}" with error code ${status}.`, { cause: `flashii.http${status}` });
|
||||
|
||||
return body;
|
||||
};
|
||||
fii.v1.emotes.emote.strings = async function({ id, fields=null, fresh=false }) {
|
||||
id = verifyEmoticonId(id);
|
||||
|
||||
const { status, body } = await send({ method: 'GET', path: `/v1/emotes/${id}/strings`, fields, fresh });
|
||||
|
||||
if(status === 400)
|
||||
throw new Error('Fields argument contains unsupported value.', { cause: 'flashii.v1.value' });
|
||||
if(status === 404)
|
||||
throw new Error('Requested emoticon does not exist.', { cause: 'flashii.v1.notfound' });
|
||||
if(status > 299)
|
||||
throw new Error(`Failed to fetch emoticon "${id}" with error code ${status}.`, { cause: `flashii.http${status}` });
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const verifyKaomojiId = id => {
|
||||
if(typeof id === 'number')
|
||||
id = id.toString();
|
||||
if(/^([^0-9]+)$/gu.test(id))
|
||||
throw new Error('id argument is not an acceptable kaomoji id.', { cause: 'flashii.v1.kaomoji.id' });
|
||||
return id;
|
||||
};
|
||||
|
||||
fii.v1.kaomoji = async function({ as=null, fields=null, fresh=false }={}) {
|
||||
const { status, body } = await send({ method: 'GET', path: '/v1/kaomoji', params: { as }, fields, fresh });
|
||||
|
||||
if(status === 400)
|
||||
throw new Error('As or fields argument contains unsupported value.', { cause: 'flashii.v1.value' });
|
||||
if(status > 299)
|
||||
throw new Error(`Failed to fetch kaomoji with error code ${status}.`, { cause: `flashii.http${status}` });
|
||||
|
||||
return body;
|
||||
};
|
||||
fii.v1.kaomoji.kaomoji = async function({ id, fields=null, fresh=false }) {
|
||||
id = verifyKaomojiId(id);
|
||||
|
||||
const { status, body } = await send({ method: 'GET', path: `/v1/kaomoji/${id}`, fields, fresh });
|
||||
|
||||
if(status === 400)
|
||||
throw new Error('Fields argument contains unsupported value.', { cause: 'flashii.v1.value' });
|
||||
if(status === 404)
|
||||
throw new Error('Requested kaomoji does not exist.', { cause: 'flashii.v1.notfound' });
|
||||
if(status > 299)
|
||||
throw new Error(`Failed to fetch kaomoji "${id}" with error code ${status}.`, { cause: `flashii.http${status}` });
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
return fii;
|
||||
};
|
|
@ -1,157 +0,0 @@
|
|||
const $id = document.getElementById.bind(document);
|
||||
const $query = document.querySelector.bind(document);
|
||||
const $queryAll = document.querySelectorAll.bind(document);
|
||||
const $text = document.createTextNode.bind(document);
|
||||
|
||||
const $insertBefore = function(target, element) {
|
||||
target.parentNode.insertBefore(element, target);
|
||||
};
|
||||
|
||||
const $appendChild = function(element, child) {
|
||||
switch(typeof child) {
|
||||
case 'undefined':
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
element.appendChild($text(child));
|
||||
break;
|
||||
|
||||
case 'function':
|
||||
$appendChild(element, child());
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
if(child === null)
|
||||
break;
|
||||
|
||||
if(child instanceof Node)
|
||||
element.appendChild(child);
|
||||
else if(child?.element instanceof Node)
|
||||
element.appendChild(child.element);
|
||||
else if(typeof child?.toString === 'function')
|
||||
element.appendChild($text(child.toString()));
|
||||
break;
|
||||
|
||||
default:
|
||||
element.appendChild($text(child.toString()));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const $appendChildren = function(element, ...children) {
|
||||
for(const child of children)
|
||||
$appendChild(element, child);
|
||||
};
|
||||
|
||||
const $removeChild = function(element, child) {
|
||||
switch(typeof child) {
|
||||
case 'function':
|
||||
$removeChild(element, child());
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
if(child === null)
|
||||
break;
|
||||
|
||||
if(child instanceof Node)
|
||||
element.removeChild(child);
|
||||
else if(child?.element instanceof Node)
|
||||
element.removeChild(child.element);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const $removeChildren = function(element) {
|
||||
while(element.lastChild)
|
||||
element.removeChild(element.lastChild);
|
||||
};
|
||||
|
||||
const $fragment = function(props, ...children) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
$appendChildren(fragment, ...children);
|
||||
return fragment;
|
||||
};
|
||||
|
||||
const $element = function(type, props, ...children) {
|
||||
if(typeof type === 'function')
|
||||
return new type(props ?? {}, ...children);
|
||||
|
||||
const element = document.createElement(type ?? 'div');
|
||||
|
||||
if(props)
|
||||
for(let key in props) {
|
||||
const prop = props[key];
|
||||
if(prop === undefined || prop === null)
|
||||
continue;
|
||||
|
||||
switch(typeof prop) {
|
||||
case 'function':
|
||||
if(key.substring(0, 2) === 'on')
|
||||
key = key.substring(2).toLowerCase();
|
||||
element.addEventListener(key, prop);
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
if(prop instanceof Array) {
|
||||
if(key === 'class')
|
||||
key = 'classList';
|
||||
|
||||
const attr = element[key];
|
||||
let addFunc = null;
|
||||
|
||||
if(attr instanceof Array)
|
||||
addFunc = attr.push.bind(attr);
|
||||
else if(attr instanceof DOMTokenList)
|
||||
addFunc = attr.add.bind(attr);
|
||||
|
||||
if(addFunc !== null) {
|
||||
for(let j = 0; j < prop.length; ++j)
|
||||
addFunc(prop[j]);
|
||||
} else {
|
||||
if(key === 'classList')
|
||||
key = 'class';
|
||||
element.setAttribute(key, prop.toString());
|
||||
}
|
||||
} else {
|
||||
if(key === 'class' || key === 'className')
|
||||
key = 'classList';
|
||||
|
||||
let setFunc = null;
|
||||
if(element[key] instanceof DOMTokenList)
|
||||
setFunc = (ak, av) => { if(av) element[key].add(ak); };
|
||||
else if(element[key] instanceof CSSStyleDeclaration)
|
||||
setFunc = (ak, av) => {
|
||||
if(ak.includes('-'))
|
||||
element[key].setProperty(ak, av);
|
||||
else
|
||||
element[key][ak] = av;
|
||||
};
|
||||
else
|
||||
setFunc = (ak, av) => { element[key][ak] = av; };
|
||||
|
||||
for(const attrKey in prop) {
|
||||
const attrValue = prop[attrKey];
|
||||
if(attrValue)
|
||||
setFunc(attrKey, attrValue);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
if(prop)
|
||||
element.setAttribute(key, '');
|
||||
break;
|
||||
|
||||
default:
|
||||
if(key === 'className')
|
||||
key = 'class';
|
||||
|
||||
element.setAttribute(key, prop.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$appendChildren(element, ...children);
|
||||
|
||||
return element;
|
||||
};
|
|
@ -1,10 +1,7 @@
|
|||
window.Umi = { UI: {} };
|
||||
|
||||
#include html.js
|
||||
#include xhr.js
|
||||
|
||||
#include animate.js
|
||||
#include auth.js
|
||||
#include args.js
|
||||
#include awaitable.js
|
||||
#include common.js
|
||||
#include compat.js
|
||||
|
@ -12,14 +9,18 @@ window.Umi = { UI: {} };
|
|||
#include context.js
|
||||
#include emotes.js
|
||||
#include events.js
|
||||
#include flashii.js
|
||||
#include mobile.js
|
||||
#include mszauth.js
|
||||
#include parsing.js
|
||||
#include themes.js
|
||||
#include txtrigs.js
|
||||
#include users.js
|
||||
#include uniqstr.js
|
||||
#include utility.js
|
||||
#include weeb.js
|
||||
#include worker.js
|
||||
#include audio/autoplay.js
|
||||
#include chatform/form.jsx
|
||||
#include chat/form.jsx
|
||||
#include chat/messages.jsx
|
||||
#include colpick/picker.jsx
|
||||
#include controls/msgbox.jsx
|
||||
#include controls/ping.jsx
|
||||
|
@ -35,6 +36,7 @@ window.Umi = { UI: {} };
|
|||
#include sidebar/act-ping.jsx
|
||||
#include sidebar/act-scroll.jsx
|
||||
#include sidebar/act-sound.jsx
|
||||
#include sidebar/act-toggle.jsx
|
||||
#include sidebar/pan-channels.jsx
|
||||
#include sidebar/pan-settings.jsx
|
||||
#include sidebar/pan-uploads.jsx
|
||||
|
@ -49,54 +51,63 @@ window.Umi = { UI: {} };
|
|||
#include ui/emotes.js
|
||||
#include ui/loading-overlay.jsx
|
||||
|
||||
const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='umi-' }={}) => {
|
||||
if(auth !== null && !MamiIsAuth(auth))
|
||||
throw new Error('auth argument must be a compatible auth object');
|
||||
const MamiInit = async args => {
|
||||
args = MamiArgs('args', args, define => {
|
||||
define('parent').default(document.body).constraint(value => value instanceof Element).done();
|
||||
define('eventTarget').required().constraint(MamiIsEventTarget).done();
|
||||
define('settingsPrefix').default('umi-').done();
|
||||
});
|
||||
|
||||
if(!MamiIsEventTarget(eventTarget))
|
||||
throw new Error('eventTarget must be a compatible event target object');
|
||||
|
||||
if(parent === null)
|
||||
parent = document.body;
|
||||
else if(!(parent instanceof HTMLElement))
|
||||
throw new Error('parent argument must be an instance of HTMLElement');
|
||||
|
||||
if(typeof settingsPrefix !== 'string')
|
||||
throw new Error('settingsPrefix must be a string');
|
||||
|
||||
const flashii = new Flashii(`${window.FII_URL}/api`);
|
||||
|
||||
if(!auth) {
|
||||
auth = new MamiFlashiiAuth(flashii);
|
||||
await auth.refresh();
|
||||
setInterval(() => { auth.refresh(); }, 600000);
|
||||
}
|
||||
|
||||
const ctx = new MamiContext(eventTarget, null, auth, flashii);
|
||||
const ctx = new MamiContext(args.eventTarget);
|
||||
|
||||
// remove this later and replace with the one commented out way below
|
||||
if(!('mami' in window))
|
||||
Object.defineProperty(window, 'mami', { enumerable: true, value: ctx });
|
||||
|
||||
ctx.views = new MamiViewsControl({ body: parent });
|
||||
ctx.msgbox = new MamiMessageBoxControl({ parent });
|
||||
ctx.views = new MamiViewsControl({ body: args.parent });
|
||||
ctx.msgbox = new MamiMessageBoxControl({ parent: args.parent });
|
||||
|
||||
const loadingOverlay = new Umi.UI.LoadingOverlay('spinner', 'Loading...');
|
||||
await ctx.views.push(loadingOverlay);
|
||||
|
||||
if(!('futami' in window)) {
|
||||
loadingOverlay.message = 'Loading environment...';
|
||||
try {
|
||||
window.futami = await FutamiCommon.load();
|
||||
} catch(ex) {
|
||||
console.error('Failed to load common settings.', ex);
|
||||
loadingOverlay.icon = 'cross';
|
||||
loadingOverlay.message = 'Could not load common settings.';
|
||||
loadingOverlay.header = 'Failed!';
|
||||
loadingOverlay.message = 'Failed to load common settings.';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(!MamiMisuzuAuth.hasInfo()) {
|
||||
loadingOverlay.message = 'Fetching credentials...';
|
||||
try {
|
||||
const auth = await MamiMisuzuAuth.update();
|
||||
if(!auth.ok)
|
||||
throw 'Authentication failed.';
|
||||
} catch(ex) {
|
||||
console.error(ex);
|
||||
location.assign(futami.get('login'));
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = new MamiSettings(settingsPrefix, ctx.events.scopeTo('settings'));
|
||||
setInterval(() => {
|
||||
MamiMisuzuAuth.update()
|
||||
.then(auth => {
|
||||
if(!auth.ok)
|
||||
location.assign(futami.get('login'));
|
||||
})
|
||||
}, 600000);
|
||||
}
|
||||
|
||||
|
||||
loadingOverlay.message = 'Loading settings...';
|
||||
|
||||
const settings = new MamiSettings(args.settingsPrefix, ctx.events.scopeTo('settings'));
|
||||
ctx.settings = settings;
|
||||
|
||||
settings.define('style').default('dark').create();
|
||||
|
@ -125,6 +136,7 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
settings.define('windowsLiveMessenger').default(false).create();
|
||||
settings.define('seinfeld').default(false).create();
|
||||
settings.define('flashTitle').default(true).create();
|
||||
settings.define('onlyConnectWhenVisible2').default(MamiIsMobileDevice()).create();
|
||||
settings.define('playJokeSounds').default(true).create();
|
||||
settings.define('weeaboo').default(false).create();
|
||||
settings.define('motivationalImages').default(false).create();
|
||||
|
@ -132,9 +144,10 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
settings.define('osuKeys').default(false).create();
|
||||
settings.define('osuKeysV2').type(['no', 'yes', 'rng']).default('no').create();
|
||||
settings.define('explosionRadius').default(20).min(0).create();
|
||||
settings.define('dumpPackets').default(window.DEBUG).create();
|
||||
settings.define('dumpEvents').default(window.DEBUG).create();
|
||||
settings.define('dumpPackets').default(FUTAMI_DEBUG).create();
|
||||
settings.define('dumpEvents').default(FUTAMI_DEBUG).create();
|
||||
settings.define('marqueeAllNames').default(false).create();
|
||||
settings.define('dbgAnimDurationMulti').default(1).min(0).max(10).create();
|
||||
settings.define('newLineOnEnter').default(false).create();
|
||||
settings.define('keepEmotePickerOpen').default(true).create();
|
||||
settings.define('doNotMarkLinksAsVisited').default(false).create();
|
||||
|
@ -146,6 +159,7 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
settings.define('notificationTriggers').default('').immutable(noNotifSupport).create();
|
||||
|
||||
|
||||
loadingOverlay.message = 'Loading sounds...';
|
||||
const soundCtx = new MamiSoundContext;
|
||||
ctx.sound = soundCtx;
|
||||
|
||||
|
@ -211,8 +225,10 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
|
||||
// loading these asynchronously makes them not show up in the backlog
|
||||
// revisit when emote reparsing is implemented
|
||||
loadingOverlay.message = 'Loading emoticons...';
|
||||
try {
|
||||
await MamiEmotes.loadApi(flashii);
|
||||
const emotes = await futami.getJson('emotes');
|
||||
MamiEmotes.loadLegacy(emotes);
|
||||
} catch(ex) {
|
||||
console.error('Failed to load emoticons.', ex);
|
||||
}
|
||||
|
@ -233,10 +249,11 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
});
|
||||
|
||||
|
||||
loadingOverlay.message = 'Preparing UI...';
|
||||
|
||||
ctx.textTriggers = new MamiTextTriggers;
|
||||
|
||||
const sidebar = new MamiSidebar;
|
||||
sidebar.toggle(window.innerWidth > 800);
|
||||
|
||||
MamiCompat('Umi.UI.Menus.Add', {
|
||||
value: (baseId, title) => {
|
||||
|
@ -245,9 +262,13 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
text: title,
|
||||
createdButton: button => {
|
||||
button.element.id = `umi-menu-icons-${baseId}`;
|
||||
button.element.append($element('div', { className: `sidebar__selector-mode--${baseId}` }));
|
||||
button.element.append($e({
|
||||
attrs: { className: `sidebar__selector-mode--${baseId}` },
|
||||
}));
|
||||
},
|
||||
element: $element('div', { 'class': `sidebar__menu--${baseId}`, id: `umi-menus-${baseId}` }),
|
||||
element: $e({
|
||||
attrs: { 'class': `sidebar__menu--${baseId}`, id: `umi-menus-${baseId}` }
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -267,7 +288,15 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
});
|
||||
|
||||
|
||||
const chatForm = new MamiChatForm(ctx.events.scopeTo('form'));
|
||||
const messages = new MamiChatMessages(ctx.globalEvents);
|
||||
settings.watch('autoScroll', ev => { messages.autoScroll = ev.detail.value; });
|
||||
settings.watch('autoEmbedV1', ev => { messages.autoEmbed = ev.detail.value; });
|
||||
|
||||
const chatForm = new MamiChatForm(
|
||||
async snippet => await sockChatClient.getUserInfosByName(snippet),
|
||||
async snippet => MamiEmotes.findByName((await sockChatClient.getCurrentUserInfo()).perms.rank, snippet, true),
|
||||
ctx.events.scopeTo('form')
|
||||
);
|
||||
|
||||
window.addEventListener('keydown', ev => {
|
||||
if((ev.ctrlKey && ev.key !== 'v') || ev.altKey)
|
||||
|
@ -279,6 +308,7 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
|
||||
settings.watch('showMarkupSelector', ev => {
|
||||
chatForm.markup.visible = ev.detail.value !== 'never';
|
||||
messages.scrollIfNeeded(chatForm.markup.height);
|
||||
Umi.UI.Messages.ScrollIfNeeded(chatForm.markup.height);
|
||||
});
|
||||
|
||||
|
@ -291,8 +321,8 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
onclick: ev => {
|
||||
if(bbCode.tag === 'color') {
|
||||
if(colourPicker === undefined) {
|
||||
colourPicker = new MamiColourPicker({ flashii });
|
||||
layout.element.appendChild(colourPicker.element);
|
||||
colourPicker = new MamiColourPicker({ presets: futami.get('colours') });
|
||||
layout.getElement().appendChild(colourPicker.element);
|
||||
}
|
||||
|
||||
if(colourPickerVisible) {
|
||||
|
@ -310,27 +340,28 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
});
|
||||
|
||||
|
||||
const layout = new Umi.UI.ChatLayout(chatForm, sidebar);
|
||||
const layout = new Umi.UI.ChatLayout(messages, chatForm, sidebar);
|
||||
await ctx.views.unshift(layout);
|
||||
|
||||
ctx.events.watch('form:resize', ev => {
|
||||
messages.scrollIfNeeded(ev.detail.diffHeight);
|
||||
Umi.UI.Messages.ScrollIfNeeded(ev.detail.diffHeight);
|
||||
});
|
||||
|
||||
settings.watch('style', ev => {
|
||||
for(const className of layout.element.classList)
|
||||
for(const className of layout.getElement().classList)
|
||||
if(className.startsWith('umi--'))
|
||||
layout.element.classList.remove(className);
|
||||
layout.element.classList.add(`umi--${ev.detail.value}`);
|
||||
layout.getElement().classList.remove(className);
|
||||
layout.getElement().classList.add(`umi--${ev.detail.value}`);
|
||||
|
||||
UmiThemeApply(ev.detail.value);
|
||||
});
|
||||
settings.watch('compactView', ev => {
|
||||
layout.element.classList.toggle('chat--compact', ev.detail.value);
|
||||
layout.interface.messageList.element.classList.toggle('chat--compact', ev.detail.value);
|
||||
layout.getElement().classList.toggle('chat--compact', ev.detail.value);
|
||||
layout.getInterface().getMessageList().getElement().classList.toggle('chat--compact', ev.detail.value);
|
||||
});
|
||||
settings.watch('preventOverflow', ev => { parent.classList.toggle('prevent-overflow', ev.detail.value); });
|
||||
settings.watch('doNotMarkLinksAsVisited', ev => { layout.interface.messageList.element.classList.toggle('mami-do-not-mark-links-as-visited', ev.detail.value); });
|
||||
settings.watch('preventOverflow', ev => { args.parent.classList.toggle('prevent-overflow', ev.detail.value); });
|
||||
settings.watch('doNotMarkLinksAsVisited', ev => { layout.getInterface().getMessageList().getElement().classList.toggle('mami-do-not-mark-links-as-visited', ev.detail.value); });
|
||||
settings.watch('newLineOnEnter', ev => { chatForm.input.newLineOnEnter = ev.detail.value; });
|
||||
settings.watch('expandTextBox', ev => { chatForm.input.growInputField = ev.detail.value; });
|
||||
|
||||
|
@ -369,7 +400,7 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
});
|
||||
|
||||
settings.watch('weeaboo', ev => {
|
||||
if(ev.detail.value) Weeaboo.init(flashii);
|
||||
if(ev.detail.value) Weeaboo.init();
|
||||
});
|
||||
|
||||
settings.watch('osuKeysV2', ev => {
|
||||
|
@ -387,51 +418,90 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
});
|
||||
|
||||
|
||||
MamiCompat('Umi.Parser.SockChatBBcode.EmbedStub', { value: () => {} }); // intentionally a no-op
|
||||
loadingOverlay.message = 'Building menus...';
|
||||
|
||||
MamiCompat('Umi.Parser.SockChatBBcode.EmbedStub', { value: () => {} });
|
||||
MamiCompat('Umi.UI.View.SetText', { value: text => { chatForm.input.setText(text); } });
|
||||
|
||||
let sockChatClient;
|
||||
|
||||
const sbUsers = new MamiSidebarPanelUsers;
|
||||
sidebar.createPanel(sbUsers);
|
||||
|
||||
sbUsers.addOption({
|
||||
await sbUsers.addOption({
|
||||
name: 'profile',
|
||||
text: 'View profile',
|
||||
onclick: entry => window.open(`${window.FII_URL}/profile.php?u=${encodeURIComponent(entry.id)}`, '_blank'),
|
||||
onclick: entry => window.open(futami.get('profile').replace('{user:id}', entry.id), '_blank'),
|
||||
});
|
||||
sbUsers.addOption({
|
||||
await sbUsers.addOption({
|
||||
name: 'action',
|
||||
text: 'Describe action',
|
||||
condition: entry => Umi.User.getCurrentUser()?.id === entry.id,
|
||||
condition: async entry => await sockChatClient.getCurrentUserId() === entry.id,
|
||||
onclick: entry => { chatForm.input.setText('/me '); },
|
||||
});
|
||||
sbUsers.addOption({
|
||||
await sbUsers.addOption({
|
||||
name: 'nick',
|
||||
text: 'Set nickname',
|
||||
condition: entry => Umi.User.getCurrentUser()?.id === entry.id && Umi.User.getCurrentUser().perms.canSetNick,
|
||||
condition: async entry => {
|
||||
const userInfo = await sockChatClient.getCurrentUserInfo();
|
||||
return userInfo.id === entry.id && userInfo.perms.nick;
|
||||
},
|
||||
onclick: entry => { chatForm.input.setText('/nick '); },
|
||||
});
|
||||
sbUsers.addOption({
|
||||
await sbUsers.addOption({
|
||||
name: 'bans',
|
||||
text: 'View bans',
|
||||
condition: entry => Umi.User.getCurrentUser()?.id === entry.id && Umi.User.getCurrentUser().perms.canKick,
|
||||
condition: async entry => {
|
||||
const userInfo = await sockChatClient.getCurrentUserInfo();
|
||||
return userInfo.id === entry.id && userInfo.perms.kick;
|
||||
},
|
||||
onclick: entry => { Umi.Server.sendMessage('/bans'); },
|
||||
});
|
||||
sbUsers.addOption({
|
||||
await sbUsers.addOption({
|
||||
name: 'kfe',
|
||||
text: 'Kick Fucking Everyone',
|
||||
condition: async entry => {
|
||||
const userInfo = await sockChatClient.getCurrentUserInfo();
|
||||
return userInfo.id === entry.id && userInfo.perms.kick;
|
||||
},
|
||||
onclick: async entry => {
|
||||
try {
|
||||
await ctx.msgbox.show({
|
||||
body: ['You are about to detonate the fucking bomb.', 'Are you sure?'],
|
||||
yes: { text: 'FUCK IT, WE BALL' },
|
||||
no: { text: 'nah' },
|
||||
});
|
||||
|
||||
const myInfo = await sockChatClient.getCurrentUserInfo();
|
||||
const userInfos = await sockChatClient.getUserInfos();
|
||||
for(const userInfo of userInfos)
|
||||
if(myInfo.id !== userInfo.id)
|
||||
sockChatClient.sendMessage(`/kick ${userInfo.name}`);
|
||||
} catch(ex) {}
|
||||
},
|
||||
});
|
||||
await sbUsers.addOption({
|
||||
name: 'dm',
|
||||
text: 'Send direct message',
|
||||
condition: entry => Umi.User.getCurrentUser()?.id !== entry.id,
|
||||
condition: async entry => await sockChatClient.getCurrentUserId() !== entry.id,
|
||||
onclick: entry => { chatForm.input.setText(`/msg ${entry.name} `); },
|
||||
});
|
||||
sbUsers.addOption({
|
||||
await sbUsers.addOption({
|
||||
name: 'kick',
|
||||
text: 'Kick from chat',
|
||||
condition: entry => Umi.User.hasCurrentUser() && Umi.User.getCurrentUser().id !== entry.id && Umi.User.getCurrentUser().perms.canKick,
|
||||
condition: async entry => {
|
||||
const userInfo = await sockChatClient.getCurrentUserInfo();
|
||||
return userInfo.id !== entry.id && userInfo.perms.kick;
|
||||
},
|
||||
onclick: entry => { chatForm.input.setText(`/kick ${entry.name} `); },
|
||||
});
|
||||
sbUsers.addOption({
|
||||
await sbUsers.addOption({
|
||||
name: 'ipaddr',
|
||||
text: 'View IP address',
|
||||
condition: entry => Umi.User.hasCurrentUser() && Umi.User.getCurrentUser().id !== entry.id && Umi.User.getCurrentUser().perms.canKick,
|
||||
condition: async entry => {
|
||||
const userInfo = await sockChatClient.getCurrentUserInfo();
|
||||
return userInfo.id !== entry.id && userInfo.perms.kick;
|
||||
},
|
||||
onclick: entry => { Umi.Server.sendMessage(`/ip ${entry.name}`); },
|
||||
});
|
||||
|
||||
|
@ -509,6 +579,10 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
});
|
||||
sbSettings.category(category => {
|
||||
category.header('Misc');
|
||||
category.setting('onlyConnectWhenVisible2').title('Only connect when the tab is in the foreground').confirm([
|
||||
'Please only disable this setting if you are using a desktop or laptop computer, this should always remain on on a phone, tablet or other device of that sort.',
|
||||
'Are you sure you want to change this setting? Ignoring this warning may carry consequences.',
|
||||
]).done();
|
||||
category.setting('playJokeSounds').title('Run joke triggers').done();
|
||||
category.setting('weeaboo').title('Weeaboo').done();
|
||||
category.setting('motivationalImages').title('Make images motivational').done();
|
||||
|
@ -549,7 +623,9 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
button.disabled = true;
|
||||
button.textContent = 'Reloading emoticons...';
|
||||
try {
|
||||
await MamiEmotes.loadApi(flashii, true);
|
||||
const emotes = await futami.getJson('emotes', true);
|
||||
MamiEmotes.clear();
|
||||
MamiEmotes.loadLegacy(emotes);
|
||||
} finally {
|
||||
button.textContent = textOrig;
|
||||
button.disabled = false;
|
||||
|
@ -592,15 +668,15 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
sbSettings.category(category => {
|
||||
category.header('Settings');
|
||||
category.button('Import settings', () => {
|
||||
(new MamiSettingsBackup(settings)).importUpload(parent);
|
||||
(new MamiSettingsBackup(settings)).importUpload(args.parent);
|
||||
}, ['Your current settings will be replaced with the ones in the export.', 'Are you sure you want to continue?']);
|
||||
category.button('Export settings', () => {
|
||||
const user = Umi.User.getCurrentUser();
|
||||
category.button('Export settings', async () => {
|
||||
const userInfo = await sockChatClient.getCurrentUserInfo();
|
||||
let fileName;
|
||||
if(user !== null)
|
||||
fileName = `${user.name}'s settings.mami`;
|
||||
if(userInfo !== undefined)
|
||||
fileName = `${userInfo.name}'s settings.mami`;
|
||||
|
||||
(new MamiSettingsBackup(settings)).exportDownload(parent, fileName);
|
||||
(new MamiSettingsBackup(settings)).exportDownload(args.parent, fileName);
|
||||
});
|
||||
category.button('Reset settings', () => {
|
||||
settings.clear();
|
||||
|
@ -612,7 +688,8 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
category.warning("Only touch these settings if you're ABSOLUTELY sure you know what you're doing, you're on your own if you break something.");
|
||||
category.setting('dumpPackets').title('Dump packets to console').done();
|
||||
category.setting('dumpEvents').title('Dump events to console').done();
|
||||
category.setting('marqueeAllNames').title('Apply marquee on everyone (requires reload)').done();
|
||||
category.setting('marqueeAllNames').title('Apply marquee on everyone').done();
|
||||
category.setting('dbgAnimDurationMulti').title('Animation multiplier').type('range').done();
|
||||
category.button('Test kick/ban notice', async button => {
|
||||
button.disabled = true;
|
||||
await ctx.views.push(new MamiForceDisconnectNotice({ perma: true, type: 'ban' }));
|
||||
|
@ -645,10 +722,15 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
const sbUploads = new MamiSidebarPanelUploads;
|
||||
sidebar.createPanel(sbUploads);
|
||||
|
||||
const sbActToggle = new MamiSidebarActionToggle(sidebar);
|
||||
sidebar.createAction(sbActToggle);
|
||||
if(window.innerWidth < 800)
|
||||
sbActToggle.click();
|
||||
|
||||
sidebar.createAction(new MamiSidebarActionScroll(settings));
|
||||
sidebar.createAction(new MamiSidebarActionSound(settings));
|
||||
sidebar.createAction(new MamiSidebarActionCollapseAll);
|
||||
sidebar.createAction(new MamiSidebarActionClearBacklog(settings, soundCtx.library, ctx.msgbox));
|
||||
sidebar.createAction(new MamiSidebarActionClearBacklog(settings, soundCtx.library, ctx.msgbox, messages));
|
||||
|
||||
const pingIndicator = new MamiPingIndicator;
|
||||
const sbActPing = new MamiSidebarActionPing(pingIndicator, ctx.msgbox);
|
||||
|
@ -666,10 +748,10 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
onPick: emote => {
|
||||
chatForm.input.insertAtCursor(`:${emote.strings[0]}:`);
|
||||
},
|
||||
getEmotes: () => MamiEmotes.all(Umi.User.getCurrentUser().perms.rank),
|
||||
getEmotes: async () => MamiEmotes.all((await sockChatClient.getCurrentUserInfo()).perms.rank),
|
||||
setKeepOpenOnPick: value => { settings.set('keepEmotePickerOpen', value); },
|
||||
});
|
||||
layout.element.appendChild(emotePicker.element);
|
||||
layout.getElement().appendChild(emotePicker.element);
|
||||
settings.watch('keepEmotePickerOpen', ev => { emotePicker.keepOpenOnPick = ev.detail.value; });
|
||||
}
|
||||
|
||||
|
@ -683,112 +765,117 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
});
|
||||
|
||||
|
||||
ctx.eeprom = new MamiEEPROM(window.FII_URL);
|
||||
let doUpload;
|
||||
ctx.eeprom = new MamiEEPROM(futami.get('eeprom2'), MamiMisuzuAuth.getLine);
|
||||
ctx.eeprom.init()
|
||||
.catch(ex => { console.error('Failed to initialise EEPROM.', ex); })
|
||||
.then(async () => {
|
||||
await sbUploads.addOption({
|
||||
name: 'view',
|
||||
text: 'View upload',
|
||||
condition: entry => entry.uploadInfo !== undefined,
|
||||
onclick: entry => window.open(entry.uploadInfo.url),
|
||||
});
|
||||
await sbUploads.addOption({
|
||||
name: 'insert',
|
||||
text: 'Insert into message',
|
||||
condition: entry => entry.uploadInfo !== undefined,
|
||||
onclick: entry => {
|
||||
const upload = entry.uploadInfo;
|
||||
|
||||
sbUploads.addOption({
|
||||
name: 'view',
|
||||
text: 'View upload',
|
||||
condition: entry => entry.uploadInfo !== undefined,
|
||||
onclick: entry => window.open(entry.uploadInfo.url),
|
||||
});
|
||||
sbUploads.addOption({
|
||||
name: 'insert',
|
||||
text: 'Insert into message',
|
||||
condition: entry => entry.uploadInfo !== undefined,
|
||||
onclick: entry => {
|
||||
const upload = entry.uploadInfo;
|
||||
let text;
|
||||
if(upload.isImage()) {
|
||||
text = `[img]${upload.url}[/img]`;
|
||||
} else if(upload.isAudio()) {
|
||||
text = `[audio]${upload.url}[/audio]`;
|
||||
} else if(upload.isVideo()) {
|
||||
text = `[video]${upload.url}[/video]`;
|
||||
} else
|
||||
text = location.protocol + upload.url;
|
||||
|
||||
let text;
|
||||
let url = upload.url;
|
||||
if(upload.isImage()) {
|
||||
text = `[img]${url}[/img]`;
|
||||
} else if(upload.isAudio()) {
|
||||
text = `[audio]${url}[/audio]`;
|
||||
} else if(upload.isVideo()) {
|
||||
text = `[video]${url}[/video]`;
|
||||
} else {
|
||||
if(url.startsWith('//'))
|
||||
url = location.protocol + url;
|
||||
text = url;
|
||||
}
|
||||
chatForm.input.insertAtCursor(text);
|
||||
},
|
||||
});
|
||||
await sbUploads.addOption({
|
||||
name: 'delete',
|
||||
text: 'Delete upload',
|
||||
condition: entry => entry.uploadInfo !== undefined,
|
||||
onclick: async entry => {
|
||||
try {
|
||||
await ctx.eeprom.delete(entry.uploadInfo);
|
||||
sbUploads.deleteEntry(entry);
|
||||
} catch(ex) {
|
||||
console.error(ex);
|
||||
await ctx.msgbox.show({ body: ['An error occurred while trying to delete an uploaded file:', ex] });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
chatForm.input.insertAtCursor(text);
|
||||
},
|
||||
});
|
||||
sbUploads.addOption({
|
||||
name: 'delete',
|
||||
text: 'Delete upload',
|
||||
condition: entry => entry.uploadInfo !== undefined,
|
||||
onclick: async entry => {
|
||||
try {
|
||||
await ctx.eeprom.delete(entry.uploadInfo);
|
||||
sbUploads.deleteEntry(entry);
|
||||
} catch(ex) {
|
||||
console.error(ex);
|
||||
await ctx.msgbox.show({ body: ['An error occurred while trying to delete an uploaded file:', ex] });
|
||||
}
|
||||
},
|
||||
});
|
||||
doUpload = async file => {
|
||||
const entry = sbUploads.createEntry(file);
|
||||
|
||||
const doUpload = async file => {
|
||||
const entry = sbUploads.createEntry(file);
|
||||
const task = ctx.eeprom.create(file);
|
||||
task.onProgress(prog => {
|
||||
entry.progress = prog.progress;
|
||||
});
|
||||
await entry.addOption({
|
||||
name: 'cancel',
|
||||
text: 'Cancel upload',
|
||||
onclick: () => { task.abort(); },
|
||||
});
|
||||
await entry.setOptionsVisible(true);
|
||||
|
||||
const task = ctx.eeprom.create(file);
|
||||
entry.addOption({
|
||||
name: 'cancel',
|
||||
text: 'Cancel upload',
|
||||
onclick: () => { task.abort(); },
|
||||
try {
|
||||
const fileInfo = await task.start();
|
||||
|
||||
await entry.setOptionsVisible(false);
|
||||
entry.uploadInfo = fileInfo;
|
||||
await entry.removeOption('cancel');
|
||||
entry.nukeProgress();
|
||||
|
||||
if(settings.get('eepromAutoInsert'))
|
||||
entry.clickOption('insert');
|
||||
} catch(ex) {
|
||||
if(!ex.aborted) {
|
||||
console.error(ex);
|
||||
ctx.msgbox.show({ body: ['An error occurred while trying to upload a file:', ex] });
|
||||
}
|
||||
|
||||
sbUploads.deleteEntry(entry);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadForm = $e({
|
||||
tag: 'input',
|
||||
attrs: {
|
||||
type: 'file',
|
||||
multiple: true,
|
||||
style: { display: 'none' },
|
||||
onchange: ev => {
|
||||
for(const file of ev.target.files)
|
||||
doUpload(file);
|
||||
},
|
||||
},
|
||||
});
|
||||
args.parent.appendChild(uploadForm);
|
||||
|
||||
chatForm.input.createButton({
|
||||
title: 'Upload',
|
||||
icon: 'fas fa-file-upload',
|
||||
onclick: () => { uploadForm.click(); },
|
||||
});
|
||||
|
||||
ctx.events.watch('form:upload', ev => {
|
||||
for(const file of ev.detail.files)
|
||||
doUpload(file);
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const fileInfo = await task.start(prog => { entry.progress = prog.progress; });
|
||||
|
||||
entry.optionsVisible = false;
|
||||
entry.uploadInfo = fileInfo;
|
||||
entry.removeOption('cancel');
|
||||
entry.nukeProgress();
|
||||
sbUploads.reloadOptionsFor(entry);
|
||||
|
||||
if(settings.get('eepromAutoInsert'))
|
||||
entry.clickOption('insert');
|
||||
} catch(ex) {
|
||||
if(ex !== '') {
|
||||
console.error(ex);
|
||||
ctx.msgbox.show({ body: ['An error occurred while trying to upload a file:', ex] });
|
||||
}
|
||||
|
||||
sbUploads.deleteEntry(entry);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadForm = $element('input', {
|
||||
type: 'file',
|
||||
multiple: true,
|
||||
style: { display: 'none' },
|
||||
onchange: ev => {
|
||||
for(const file of ev.target.files)
|
||||
doUpload(file);
|
||||
},
|
||||
});
|
||||
parent.appendChild(uploadForm);
|
||||
|
||||
chatForm.input.createButton({
|
||||
title: 'Upload',
|
||||
icon: 'fas fa-file-upload',
|
||||
onclick: () => { uploadForm.click(); },
|
||||
});
|
||||
|
||||
ctx.events.watch('form:upload', ev => {
|
||||
console.info(ev);
|
||||
for(const file of ev.detail.files)
|
||||
doUpload(file);
|
||||
});
|
||||
|
||||
// figure out how to display a UI for this someday
|
||||
//parent.addEventListener('dragenter', ev => { console.info('dragenter', ev); });
|
||||
//parent.addEventListener('dragleave', ev => { console.info('dragleave', ev); });
|
||||
parent.addEventListener('dragover', ev => { ev.preventDefault(); });
|
||||
parent.addEventListener('drop', ev => {
|
||||
//args.parent.addEventListener('dragenter', ev => { console.info('dragenter', ev); });
|
||||
//args.parent.addEventListener('dragleave', ev => { console.info('dragleave', ev); });
|
||||
args.parent.addEventListener('dragover', ev => { ev.preventDefault(); });
|
||||
args.parent.addEventListener('drop', ev => {
|
||||
if(ev.dataTransfer === undefined || ev.dataTransfer === null || ev.dataTransfer.files.length < 1)
|
||||
return;
|
||||
|
||||
|
@ -828,23 +915,27 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
|
||||
loadingOverlay.message = 'Connecting...';
|
||||
|
||||
const setLoadingOverlay = async (icon, message, optional) => {
|
||||
const setLoadingOverlay = async (icon, header, message, optional) => {
|
||||
const currentView = ctx.views.current;
|
||||
|
||||
if('icon' in currentView) {
|
||||
currentView.icon = icon;
|
||||
currentView.header = header;
|
||||
currentView.message = message;
|
||||
return currentView;
|
||||
}
|
||||
|
||||
if(!optional) {
|
||||
const loading = new Umi.UI.LoadingOverlay(icon, message);
|
||||
const loading = new Umi.UI.LoadingOverlay(icon, header, message);
|
||||
await ctx.views.push(loading);
|
||||
}
|
||||
};
|
||||
|
||||
const sockChat = new MamiSockChat(ctx.events.scopeTo('sockchat'));
|
||||
const conMan = new MamiConnectionManager(sockChat, settings, flashii, ctx.events.scopeTo('conn'));
|
||||
const protoWorker = new MamiWorker(MAMI_PROTO_JS, ctx.events.scopeTo('worker'));
|
||||
ctx.protoWorker = protoWorker;
|
||||
|
||||
const sockChat = new MamiSockChat(protoWorker);
|
||||
const conMan = new MamiConnectionManager(sockChat, settings, futami.get('servers'), ctx.events.scopeTo('conn'));
|
||||
ctx.conMan = conMan;
|
||||
|
||||
ctx.events.watch('form:send', ev => {
|
||||
|
@ -857,6 +948,7 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
// hack for DM channels
|
||||
if(info.isUserChannel) {
|
||||
sbChannels.setActiveEntry(info.name);
|
||||
messages.switchChannel(info.name);
|
||||
Umi.UI.Messages.SwitchChannel(info);
|
||||
}
|
||||
};
|
||||
|
@ -872,12 +964,12 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
|
||||
const reconManAttempt = ev => {
|
||||
if(sockChatRestarting || ev.detail.delay > 2000)
|
||||
setLoadingOverlay('spinner', `Connecting${'.'.repeat(Math.max(3, ev.detail.attempt))}`);
|
||||
setLoadingOverlay('spinner', 'Connecting...', 'Connecting to server...');
|
||||
};
|
||||
const reconManFail = ev => {
|
||||
// this is absolutely disgusting but i really don't care right now sorry
|
||||
if(sockChatRestarting || ev.detail.delay > 2000)
|
||||
setLoadingOverlay('unlink', `Could not reconnect, retrying in ${(ev.detail.delay / 1000).toLocaleString()} seconds... [<a href="javascript:void(0)" onclick="mami.conMan.force()">Force?</a>]`);
|
||||
setLoadingOverlay('unlink', sockChatRestarting ? 'Restarting...' : 'Disconnected', `Attempting to reconnect in ${(ev.detail.delay / 1000).toLocaleString()} seconds...<br><a href="javascript:void(0)" onclick="mami.conMan.force()">Retry now</a>`);
|
||||
};
|
||||
const reconManSuccess = () => {
|
||||
conMan.unwatch('success', reconManSuccess);
|
||||
|
@ -894,17 +986,18 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
|
||||
const sockChatHandlers = new MamiSockChatHandlers(
|
||||
ctx, sockChat, setLoadingOverlay, sockChatReconnect, pingIndicator,
|
||||
sbActPing, sbChannels, sbUsers
|
||||
sbActPing, sbChannels, sbUsers, messages
|
||||
);
|
||||
settings.watch('dumpEvents', ev => { sockChatHandlers.setDumpEvents(ev.detail.value); });
|
||||
settings.watch('dumpPackets', ev => { sockChat.dumpPackets = ev.detail.value; });
|
||||
settings.watch('dumpEvents', ev => sockChatHandlers.setDumpEvents(ev.detail.value));
|
||||
settings.watch('dumpPackets', ev => sockChat.setDumpPackets(ev.detail.value));
|
||||
sockChatHandlers.register();
|
||||
|
||||
const conManAttempt = ev => {
|
||||
setLoadingOverlay('spinner', `Connecting${'.'.repeat(Math.max(3, ev.detail.attempt))}`);
|
||||
let message = ev.detail.attempt > 2 ? `Attempt ${ev.detail.attempt}...` : 'Connecting to server...';
|
||||
setLoadingOverlay('spinner', 'Connecting...', message);
|
||||
};
|
||||
const conManFail = ev => {
|
||||
setLoadingOverlay('cross', `Could not connect, retrying in ${(ev.detail.delay / 1000).toLocaleString()} seconds... [<a href="javascript:void(0)" onclick="mami.conMan.force()">Force?</a>]`);
|
||||
setLoadingOverlay('cross', 'Failed to connect', `Retrying in ${ev.detail.delay / 1000} seconds...`);
|
||||
};
|
||||
const conManSuccess = () => {
|
||||
conMan.unwatch('success', conManSuccess);
|
||||
|
@ -916,9 +1009,41 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
conMan.watch('attempt', conManAttempt);
|
||||
conMan.watch('fail', conManFail);
|
||||
|
||||
await sockChat.create();
|
||||
conMan.client = sockChat;
|
||||
await conMan.start();
|
||||
let workerStarting = false;
|
||||
const initWorker = async () => {
|
||||
if(workerStarting)
|
||||
return;
|
||||
workerStarting = true;
|
||||
|
||||
if(FUTAMI_DEBUG)
|
||||
console.info('[proto] initialising worker...');
|
||||
|
||||
try {
|
||||
await protoWorker.connect();
|
||||
await sockChat.create();
|
||||
conMan.client = sockChat;
|
||||
sockChatClient = sockChat.client;
|
||||
await conMan.start();
|
||||
} finally {
|
||||
workerStarting = false;
|
||||
}
|
||||
};
|
||||
|
||||
protoWorker.watch(':timeout', ev => {
|
||||
console.warn('worker timeout', ev.detail);
|
||||
initWorker();
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if(document.visibilityState === 'visible') {
|
||||
protoWorker.ping().catch(ex => {
|
||||
console.warn('worker died', ex);
|
||||
initWorker();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await initWorker();
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
@ -927,7 +1052,37 @@ const MamiInit = async ({ auth=null, eventTarget, parent=null, settingsPrefix='u
|
|||
const eventTarget = new MamiEventTargetWindow;
|
||||
Object.defineProperty(window, 'mamiEventTarget', { enumerable: true, value: eventTarget });
|
||||
|
||||
MamiInit({ eventTarget }).then(mami => {
|
||||
MamiInit({
|
||||
eventTarget: eventTarget,
|
||||
}).then(mami => {
|
||||
//Object.defineProperty(window, 'mami', { enumerable: true, value: mami });
|
||||
});
|
||||
})();
|
||||
|
||||
const MamiDbgCreateFloatingInstance = async () => {
|
||||
if(!FUTAMI_DEBUG)
|
||||
return;
|
||||
|
||||
const prefix = MamiUniqueStr(8);
|
||||
const parent = $e({
|
||||
attrs: {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
bottom: '100px',
|
||||
right: '100px',
|
||||
zIndex: '9001',
|
||||
width: '640px',
|
||||
height: '480px',
|
||||
background: '#0f0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
document.body.appendChild(parent);
|
||||
|
||||
return await MamiInit({
|
||||
parent: parent,
|
||||
settingsPrefix: `dbg:${prefix}:`,
|
||||
eventTarget: mamiEventTarget.scopeTo(prefix),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
#include users.js
|
||||
|
||||
const MamiMessageAuthorInfo = function(self = false, user = null, id = null, name = null, colour = null, rank = null, avatar = null) {
|
||||
if(typeof self !== 'boolean')
|
||||
throw 'self must be a boolean';
|
||||
|
||||
if(user === null) {
|
||||
id ??= '';
|
||||
name ??= '';
|
||||
colour ??= 'inherit';
|
||||
rank ??= 0;
|
||||
avatar ??= new MamiUserAvatarInfo(id);
|
||||
} else {
|
||||
if(typeof user !== 'object')
|
||||
throw 'user must be an object or null';
|
||||
|
||||
id ??= user.id;
|
||||
name ??= user.name;
|
||||
colour ??= user.colour;
|
||||
rank ??= user.perms.rank;
|
||||
avatar ??= user.avatar;
|
||||
}
|
||||
|
||||
if(typeof id !== 'string')
|
||||
throw 'id must be a string';
|
||||
if(typeof name !== 'string')
|
||||
throw 'name must be a string';
|
||||
if(typeof colour !== 'string')
|
||||
throw 'colour must be a string';
|
||||
if(typeof rank !== 'number')
|
||||
throw 'rank must be a number';
|
||||
if(typeof avatar !== 'object')
|
||||
throw 'avatar must be an object';
|
||||
|
||||
return {
|
||||
get self() { return self; },
|
||||
|
||||
get user() { return user; },
|
||||
get hasUser() { return user !== null; },
|
||||
|
||||
get id() { return id; },
|
||||
get name() { return name; },
|
||||
get colour() { return colour; },
|
||||
get rank() { return rank; },
|
||||
get avatar() { return avatar; },
|
||||
};
|
||||
};
|
||||
|
||||
const MamiMessageInfo = function(type, created = null, detail = null, id = '', author = null, channel = '', silent = false) {
|
||||
if(typeof type !== 'string')
|
||||
throw 'type must be a string';
|
||||
if(created === null)
|
||||
created = new Date;
|
||||
else if(!(created instanceof Date))
|
||||
throw 'created must be an instance of window.Date or null';
|
||||
if(typeof id !== 'string')
|
||||
throw 'id must be a string';
|
||||
if(typeof author !== 'object')
|
||||
throw 'author must be an object';
|
||||
if(typeof channel !== 'string')
|
||||
throw 'channel must be a string';
|
||||
if(typeof silent !== 'boolean')
|
||||
throw 'silent must be a boolean';
|
||||
|
||||
return {
|
||||
get type() { return type; },
|
||||
get created() { return created; },
|
||||
get detail() { return detail; },
|
||||
get id() { return id; },
|
||||
get author() { return author; },
|
||||
get channel() { return channel; },
|
||||
get silent() { return silent; },
|
||||
};
|
||||
};
|
12
src/mami.js/mobile.js
Normal file
12
src/mami.js/mobile.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
const MamiIsMobileDevice = () => {
|
||||
if('userAgentData' in navigator)
|
||||
return navigator.userAgentData.mobile;
|
||||
|
||||
if('matchMedia' in window)
|
||||
return !window.matchMedia("(any-pointer:fine)").matches;
|
||||
|
||||
if('maxTouchPoints' in navigator)
|
||||
return navigator.maxTouchPoints > 1;
|
||||
|
||||
return false;
|
||||
};
|
32
src/mami.js/mszauth.js
Normal file
32
src/mami.js/mszauth.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
#include common.js
|
||||
#include utility.js
|
||||
|
||||
const MamiMisuzuAuth = (() => {
|
||||
let userId = null;
|
||||
let authMethod = 'Misuzu';
|
||||
let authToken = null;
|
||||
|
||||
return {
|
||||
hasInfo: () => userId !== null && authToken !== null,
|
||||
getUserId: () => userId,
|
||||
getAuthToken: () => authToken,
|
||||
getLine: () => `${authMethod} ${authToken}`,
|
||||
getInfo: () => {
|
||||
return {
|
||||
method: authMethod,
|
||||
token: authToken,
|
||||
};
|
||||
},
|
||||
update: async () => {
|
||||
const resp = await $x.get(futami.get('token'), { authed: true, type: 'json' });
|
||||
|
||||
const body = resp.body();
|
||||
if(body.ok) {
|
||||
userId = body.usr.toString();
|
||||
authToken = body.tkn;
|
||||
}
|
||||
|
||||
return body;
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -52,58 +52,59 @@ const MamiForceDisconnectNotice = function(banInfo) {
|
|||
} catch(ex) {}
|
||||
bgmSrc = sfxBuf = undefined;
|
||||
},
|
||||
getViewTransition(mode) {
|
||||
getViewTransition: mode => {
|
||||
if(mode === 'push')
|
||||
return async ({ fromElem, toElem }) => {
|
||||
if(sfxBuf !== undefined)
|
||||
mami.sound.audio.createSource(sfxBuf).play();
|
||||
return ctx => MamiAnimate({
|
||||
async: true,
|
||||
duration: (sfxBuf?.duration ?? 1.4) * 1000,
|
||||
start: () => {
|
||||
if(sfxBuf !== undefined)
|
||||
mami.sound.audio.createSource(sfxBuf).play();
|
||||
|
||||
toElem.style.top = '-100%';
|
||||
ctx.toElem.style.top = '-100%';
|
||||
},
|
||||
update: t => {
|
||||
const tOutBounce = MamiEasings.outBounce(t);
|
||||
ctx.toElem.style.top = `${-100 + (tOutBounce * 100)}%`;
|
||||
|
||||
await MamiAnimate({
|
||||
duration: (sfxBuf?.duration ?? 1.4) * 1000,
|
||||
update(t) {
|
||||
const tOutBounce = MamiEasings.outBounce(t);
|
||||
toElem.style.top = `${-100 + (tOutBounce * 100)}%`;
|
||||
|
||||
const tOutExpo = MamiEasings.outExpo(t);
|
||||
fromElem.style.transform = `scale(${1 - (1 * tOutExpo)}) rotate(${rotate * tOutExpo}deg)`;
|
||||
fromElem.style.filter = `grayscale(${tOutExpo * 100}%)`;
|
||||
},
|
||||
});
|
||||
|
||||
bgmSrc?.play();
|
||||
|
||||
toElem.style.top = null;
|
||||
fromElem.style.transform = null;
|
||||
fromElem.style.filter = null;
|
||||
};
|
||||
const tOutExpo = MamiEasings.outExpo(t);
|
||||
ctx.fromElem.style.transform = `scale(${1 - (1 * tOutExpo)}) rotate(${rotate * tOutExpo}deg)`;
|
||||
ctx.fromElem.style.filter = `grayscale(${tOutExpo * 100}%)`;
|
||||
},
|
||||
end: () => {
|
||||
bgmSrc?.play();
|
||||
ctx.toElem.style.top = null;
|
||||
ctx.fromElem.style.transform = null;
|
||||
ctx.fromElem.style.filter = null;
|
||||
},
|
||||
});
|
||||
|
||||
if(mode === 'pop')
|
||||
return async ({ fromElem, toElem }) => {
|
||||
bgmSrc?.stop();
|
||||
if(sfxBuf !== undefined)
|
||||
mami.sound.audio.createSource(sfxBuf, true).play();
|
||||
return ctx => MamiAnimate({
|
||||
async: true,
|
||||
duration: (sfxBuf?.duration ?? 1.4) * 1000,
|
||||
start: () => {
|
||||
bgmSrc?.stop();
|
||||
if(sfxBuf !== undefined)
|
||||
mami.sound.audio.createSource(sfxBuf, true).play();
|
||||
|
||||
toElem.style.transform = `scale(1) rotate(${rotate}deg)`;
|
||||
toElem.style.filter = 'grayscale(100%)';
|
||||
ctx.toElem.style.transform = `scale(1) rotate(${rotate}deg)`;
|
||||
ctx.toElem.style.filter = 'grayscale(100%)';
|
||||
},
|
||||
update: t => {
|
||||
const tOutBounce = MamiEasings.inBounce(t);
|
||||
ctx.fromElem.style.top = `${tOutBounce * -100}%`;
|
||||
|
||||
await MamiAnimate({
|
||||
duration: (sfxBuf?.duration ?? 1.4) * 1000,
|
||||
update(t) {
|
||||
const tOutBounce = MamiEasings.inBounce(t);
|
||||
fromElem.style.top = `${tOutBounce * -100}%`;
|
||||
|
||||
const tOutExpo = MamiEasings.inExpo(t);
|
||||
toElem.style.transform = `scale(${1 * tOutExpo}) rotate(${rotate - (rotate * tOutExpo)}deg)`;
|
||||
toElem.style.filter = `grayscale(${100 - (tOutExpo * 100)}%)`;
|
||||
},
|
||||
});
|
||||
|
||||
fromElem.style.top = null;
|
||||
toElem.style.transform = null;
|
||||
toElem.style.filter = null;
|
||||
};
|
||||
const tOutExpo = MamiEasings.inExpo(t);
|
||||
ctx.toElem.style.transform = `scale(${1 * tOutExpo}) rotate(${rotate - (rotate * tOutExpo)}deg)`;
|
||||
ctx.toElem.style.filter = `grayscale(${100 - (tOutExpo * 100)}%)`;
|
||||
},
|
||||
end: () => {
|
||||
ctx.fromElem.style.top = null;
|
||||
ctx.toElem.style.transform = null;
|
||||
ctx.toElem.style.filter = null;
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ const MamiYouAreAnIdiot = function(sndLibrary, views) {
|
|||
|
||||
const pub = {
|
||||
get element() { return html; },
|
||||
async onViewPush() {
|
||||
onViewPush: async () => {
|
||||
try {
|
||||
soundSrc = await sndLibrary.loadSource('misc:youare');
|
||||
soundSrc.muted = true;
|
||||
|
@ -32,34 +32,29 @@ const MamiYouAreAnIdiot = function(sndLibrary, views) {
|
|||
console.error(ex);
|
||||
}
|
||||
},
|
||||
async onViewForeground() {
|
||||
onViewForeground: async () => {
|
||||
if(soundSrc !== undefined)
|
||||
soundSrc.muted = false;
|
||||
},
|
||||
async onViewBackground() {
|
||||
onViewBackground: async () => {
|
||||
if(soundSrc !== undefined)
|
||||
soundSrc.muted = true;
|
||||
},
|
||||
async onViewPop() {
|
||||
onViewPop: async () => {
|
||||
if(soundSrc !== undefined)
|
||||
soundSrc.stop();
|
||||
soundSrc = undefined;
|
||||
},
|
||||
getViewTransition(mode) {
|
||||
getViewTransition: mode => {
|
||||
if(mode === 'push')
|
||||
return async ({ toElem }) => {
|
||||
toElem.style.top = '-100%';
|
||||
|
||||
await MamiAnimate({
|
||||
duration: 1500,
|
||||
easing: 'outBounce',
|
||||
update(t) {
|
||||
toElem.style.top = `${-100 + (t * 100)}%`;
|
||||
},
|
||||
});
|
||||
|
||||
toElem.style.top = null;
|
||||
};
|
||||
return ctx => MamiAnimate({
|
||||
async: true,
|
||||
duration: 1500,
|
||||
easing: 'outBounce',
|
||||
start: () => ctx.toElem.style.top = '-100%',
|
||||
update: t => ctx.toElem.style.top = `${-100 + (t * 100)}%`,
|
||||
end: () => ctx.toElem.style.top = null,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#include utility.js
|
||||
|
||||
if(!Umi.Parser) Umi.Parser = {};
|
||||
if(!Umi.Parser.SockChatBBcode) Umi.Parser.SockChatBBcode = {};
|
||||
|
||||
|
@ -47,12 +49,6 @@ const UmiBBCodes = [
|
|||
stripArg: ';:{}<>&|\\/~\'"',
|
||||
replace: '<span style="color:{0};">{1}</span>',
|
||||
},
|
||||
{
|
||||
tag: 'bgcolor',
|
||||
hasArg: true,
|
||||
stripArg: ';:{}<>&|\\/~\'"',
|
||||
replace: '<span style="background-color:{0};">{1}</span>',
|
||||
},
|
||||
{
|
||||
tag: 'img',
|
||||
text: 'Image',
|
||||
|
@ -144,9 +140,8 @@ Umi.Parsing = (function() {
|
|||
};
|
||||
};
|
||||
const motivFrame = function(texts, body) {
|
||||
return $element(
|
||||
'div',
|
||||
{
|
||||
return $e({
|
||||
attrs: {
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
textAlign: 'center',
|
||||
|
@ -157,46 +152,48 @@ Umi.Parsing = (function() {
|
|||
lineHeight: '1.4em',
|
||||
},
|
||||
},
|
||||
$element(
|
||||
'div',
|
||||
child: [
|
||||
{
|
||||
style: {
|
||||
border: '3px double #fff',
|
||||
maxWidth: '50vw',
|
||||
maxHeight: '50vh',
|
||||
marginTop: '30px',
|
||||
marginLeft: '50px',
|
||||
marginRight: '50px',
|
||||
boxSizing: 'content-box',
|
||||
display: 'inline-block',
|
||||
tag: 'div',
|
||||
attrs: {
|
||||
style: {
|
||||
border: '3px double #fff',
|
||||
maxWidth: '50vw',
|
||||
maxHeight: '50vh',
|
||||
marginTop: '30px',
|
||||
marginLeft: '50px',
|
||||
marginRight: '50px',
|
||||
boxSizing: 'content-box',
|
||||
display: 'inline-block',
|
||||
},
|
||||
},
|
||||
child: body,
|
||||
},
|
||||
{
|
||||
tag: 'h1',
|
||||
child: texts.top,
|
||||
attrs: {
|
||||
style: {
|
||||
color: '#fff',
|
||||
textDecoration: 'none !important',
|
||||
margin: '10px',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
},
|
||||
},
|
||||
body
|
||||
),
|
||||
$element(
|
||||
'h1',
|
||||
{
|
||||
style: {
|
||||
color: '#fff',
|
||||
textDecoration: 'none !important',
|
||||
margin: '10px',
|
||||
textTransform: 'uppercase',
|
||||
tag: 'p',
|
||||
child: texts.bottom,
|
||||
attrs: {
|
||||
style: {
|
||||
color: '#fff',
|
||||
textDecoration: 'none !important',
|
||||
margin: '10px',
|
||||
},
|
||||
},
|
||||
},
|
||||
texts.top
|
||||
),
|
||||
$element(
|
||||
'p',
|
||||
{
|
||||
style: {
|
||||
color: '#fff',
|
||||
textDecoration: 'none !important',
|
||||
margin: '10px',
|
||||
},
|
||||
},
|
||||
texts.bottom
|
||||
),
|
||||
);
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const toggleImage = function(element) {
|
||||
|
@ -216,9 +213,9 @@ Umi.Parsing = (function() {
|
|||
element.dataset.embed = '1';
|
||||
element.classList.add('markup__link--visited');
|
||||
|
||||
let html = $element(
|
||||
'img',
|
||||
{
|
||||
let html = $e({
|
||||
tag: 'img',
|
||||
attrs: {
|
||||
src: url,
|
||||
alt: url,
|
||||
style: {
|
||||
|
@ -231,7 +228,7 @@ Umi.Parsing = (function() {
|
|||
container.scrollIntoView({ inline: 'end' });
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if(mami.settings.get('motivationalImages'))
|
||||
html = motivFrame(
|
||||
|
@ -253,16 +250,16 @@ Umi.Parsing = (function() {
|
|||
element.dataset.embed = '0';
|
||||
element.textContent = 'Embed';
|
||||
container.textContent = '';
|
||||
container.appendChild($element(
|
||||
'a',
|
||||
{
|
||||
container.appendChild($e({
|
||||
tag: 'a',
|
||||
attrs: {
|
||||
href: url,
|
||||
target: '_blank',
|
||||
rel: 'nofollow noreferrer noopener',
|
||||
className: 'markup__link',
|
||||
},
|
||||
url,
|
||||
));
|
||||
child: url,
|
||||
}));
|
||||
} else {
|
||||
container.title = 'audio';
|
||||
element.dataset.embed = '1';
|
||||
|
@ -270,9 +267,9 @@ Umi.Parsing = (function() {
|
|||
container.textContent = '';
|
||||
element.classList.add('markup__link--visited');
|
||||
|
||||
let media = $element(
|
||||
'audio',
|
||||
{
|
||||
let media = $e({
|
||||
tag: 'audio',
|
||||
attrs: {
|
||||
src: url,
|
||||
controls: true,
|
||||
onloadedmetadata: () => {
|
||||
|
@ -282,7 +279,7 @@ Umi.Parsing = (function() {
|
|||
media.play();
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
container.appendChild(media);
|
||||
}
|
||||
|
@ -297,25 +294,25 @@ Umi.Parsing = (function() {
|
|||
element.dataset.embed = '0';
|
||||
element.textContent = 'Embed';
|
||||
container.textContent = '';
|
||||
container.appendChild($element(
|
||||
'a',
|
||||
{
|
||||
container.appendChild($e({
|
||||
tag: 'a',
|
||||
attrs: {
|
||||
href: url,
|
||||
target: '_blank',
|
||||
rel: 'nofollow noreferrer noopener',
|
||||
className: 'markup__link',
|
||||
},
|
||||
url,
|
||||
));
|
||||
child: url,
|
||||
}));
|
||||
} else {
|
||||
container.title = 'video';
|
||||
element.dataset.embed = '1';
|
||||
element.textContent = 'Remove';
|
||||
element.classList.add('markup__link--visited');
|
||||
|
||||
let media = $element(
|
||||
'video',
|
||||
{
|
||||
let media = $e({
|
||||
tag: 'video',
|
||||
attrs: {
|
||||
src: url,
|
||||
controls: true,
|
||||
style: {
|
||||
|
@ -329,7 +326,7 @@ Umi.Parsing = (function() {
|
|||
media.play();
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
let html = media;
|
||||
|
||||
|
|
|
@ -1,303 +0,0 @@
|
|||
#include proto/sockchat/utils.js
|
||||
|
||||
const SockChatS2CPong = ctx => {
|
||||
const lastPong = Date.now();
|
||||
const lastPing = ctx.lastPing;
|
||||
|
||||
ctx.lastPing = undefined;
|
||||
if(lastPing === undefined)
|
||||
throw 'unexpected pong received??';
|
||||
|
||||
ctx.pingPromise?.resolve({
|
||||
ping: lastPing,
|
||||
pong: lastPong,
|
||||
diff: lastPong - lastPing,
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CBanKick = (ctx, type, expiresTime) => {
|
||||
ctx.wasKicked = true;
|
||||
|
||||
const bakaInfo = {
|
||||
session: { success: false },
|
||||
baka: {
|
||||
type: type === '0' ? 'kick' : 'ban',
|
||||
},
|
||||
};
|
||||
|
||||
if(bakaInfo.baka.type === 'ban') {
|
||||
bakaInfo.baka.perma = expiresTime === '-1';
|
||||
bakaInfo.baka.until = expiresTime === '-1' ? undefined : new Date(parseInt(expiresTime) * 1000);
|
||||
}
|
||||
|
||||
ctx.dispatch('session:term', bakaInfo);
|
||||
};
|
||||
|
||||
const SockChatS2CContextClear = (ctx, mode) => {
|
||||
if(mode === '0' || mode === '3' || mode === '4')
|
||||
ctx.dispatch('msg:clear');
|
||||
|
||||
if(mode === '1' || mode === '3' || mode === '4')
|
||||
ctx.dispatch('user:clear');
|
||||
|
||||
if(mode === '2' || mode === '4')
|
||||
ctx.dispatch('chan:clear');
|
||||
};
|
||||
|
||||
const SockChatS2CChannelPopulate = (ctx, count, ...args) => {
|
||||
count = parseInt(count);
|
||||
ctx.dispatch('chan:clear');
|
||||
|
||||
for(let i = 0; i < count; ++i) {
|
||||
const offset = 3 * i;
|
||||
|
||||
ctx.dispatch('chan:add', {
|
||||
channel: {
|
||||
name: args[offset],
|
||||
hasPassword: args[offset + 1] !== '0',
|
||||
isTemporary: args[offset + 2] !== '0',
|
||||
isCurrent: args[offset] === ctx.channelName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ctx.dispatch('chan:focus', {
|
||||
channel: { name: ctx.channelName },
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CChannelAdd = (ctx, name, hasPass, isTemp) => {
|
||||
ctx.dispatch('chan:add', {
|
||||
channel: {
|
||||
name: name,
|
||||
hasPassword: hasPass !== '0',
|
||||
isTemporary: isTemp !== '0',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CChannelUpdate = (ctx, prevName, name, hasPass, isTemp) => {
|
||||
ctx.dispatch('chan:update', {
|
||||
channel: {
|
||||
previousName: prevName,
|
||||
name: name,
|
||||
hasPassword: hasPass !== '0',
|
||||
isTemporary: isTemp !== '0',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CChannelRemove = (ctx, name) => {
|
||||
ctx.dispatch('chan:remove', {
|
||||
channel: { name: name },
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CUserPopulate = (ctx, count, ...args) => {
|
||||
count = parseInt(count);
|
||||
ctx.dispatch('user:clear');
|
||||
|
||||
for(let i = 0; i < count; ++i) {
|
||||
const offset = 5 * i;
|
||||
const statusInfo = SockChatParseStatusInfo(args[offset + 1]);
|
||||
|
||||
ctx.dispatch('user:add', {
|
||||
user: {
|
||||
id: args[offset],
|
||||
self: args[offset] === ctx.userId,
|
||||
name: statusInfo.name,
|
||||
status: statusInfo.status,
|
||||
colour: SockChatParseUserColour(args[offset + 2]),
|
||||
perms: SockChatParseUserPerms(args[offset + 3]),
|
||||
hidden: args[offset + 4] !== '0',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const SockChatS2CUserAdd = (ctx, timeStamp, userId, userName, userColour, userPerms, msgId) => {
|
||||
const statusInfo = SockChatParseStatusInfo(userName);
|
||||
|
||||
ctx.dispatch('user:add', {
|
||||
msg: {
|
||||
id: msgId,
|
||||
time: new Date(parseInt(timeStamp) * 1000),
|
||||
channel: ctx.channelName,
|
||||
},
|
||||
user: {
|
||||
id: userId,
|
||||
self: userId === ctx.userId,
|
||||
name: statusInfo.name,
|
||||
status: statusInfo.status,
|
||||
colour: SockChatParseUserColour(userColour),
|
||||
perms: SockChatParseUserPerms(userPerms),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CUserUpdate = (ctx, userId, userName, userColour, userPerms) => {
|
||||
const statusInfo = SockChatParseStatusInfo(userName);
|
||||
|
||||
ctx.dispatch('user:update', {
|
||||
user: {
|
||||
id: userId,
|
||||
self: userId === ctx.userId,
|
||||
name: statusInfo.name,
|
||||
status: statusInfo.status,
|
||||
colour: SockChatParseUserColour(userColour),
|
||||
perms: SockChatParseUserPerms(userPerms),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CUserRemove = (ctx, userId, userName, reason, timeStamp, msgId) => {
|
||||
const statusInfo = SockChatParseStatusInfo(userName);
|
||||
|
||||
ctx.dispatch('user:remove', {
|
||||
leave: { type: reason },
|
||||
msg: {
|
||||
id: msgId,
|
||||
time: new Date(parseInt(timeStamp) * 1000),
|
||||
channel: ctx.channelName,
|
||||
},
|
||||
user: {
|
||||
id: userId,
|
||||
self: userId === ctx.userId,
|
||||
name: statusInfo.name,
|
||||
status: statusInfo.status,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CUserChannelJoin = (ctx, userId, userName, userColour, userPerms, msgId) => {
|
||||
const statusInfo = SockChatParseStatusInfo(userName);
|
||||
|
||||
ctx.dispatch('chan:join', {
|
||||
user: {
|
||||
id: userId,
|
||||
self: userId === ctx.userId,
|
||||
name: statusInfo.name,
|
||||
status: statusInfo.status,
|
||||
colour: SockChatParseUserColour(userColour),
|
||||
perms: SockChatParseUserPerms(userPerms),
|
||||
},
|
||||
msg: {
|
||||
id: msgId,
|
||||
channel: ctx.channelName,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CUserChannelLeave = (ctx, userId, msgId) => {
|
||||
ctx.dispatch('chan:leave', {
|
||||
user: {
|
||||
id: userId,
|
||||
self: userId === ctx.userId,
|
||||
},
|
||||
msg: {
|
||||
id: msgId,
|
||||
channel: ctx.channelName,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CUserChannelFocus = (ctx, name) => {
|
||||
ctx.channelName = name;
|
||||
|
||||
ctx.dispatch('chan:focus', {
|
||||
channel: { name: ctx.channelName },
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CMessagePopulate = (ctx, timeStamp, userId, userName, userColour, userPerms, msgText, msgId, msgNotify, msgFlags) => {
|
||||
const mFlags = SockChatParseMsgFlags(msgFlags);
|
||||
const statusInfo = SockChatParseStatusInfo(userName);
|
||||
const info = {
|
||||
msg: {
|
||||
time: new Date(parseInt(timeStamp) * 1000),
|
||||
channel: ctx.channelName,
|
||||
sender: {
|
||||
id: userId,
|
||||
self: userId === ctx.userId,
|
||||
name: statusInfo.name,
|
||||
status: statusInfo.status,
|
||||
colour: SockChatParseUserColour(userColour),
|
||||
perms: SockChatParseUserPerms(userPerms),
|
||||
},
|
||||
isBot: userId === '-1',
|
||||
silent: msgNotify === '0',
|
||||
flags: mFlags,
|
||||
flagsRaw: msgFlags,
|
||||
text: SockChatUnfuckText(msgText, mFlags.isAction),
|
||||
},
|
||||
};
|
||||
|
||||
if(!isNaN(msgId))
|
||||
info.msg.id = msgId;
|
||||
|
||||
if(info.msg.isBot) {
|
||||
const botParts = msgText.split("\f");
|
||||
info.msg.botInfo = {
|
||||
isError: botParts[0] === '1',
|
||||
type: botParts[1],
|
||||
args: botParts.slice(2),
|
||||
};
|
||||
|
||||
if(info.msg.botInfo.type === 'say')
|
||||
info.msg.botInfo.args[0] = SockChatUnfuckText(info.msg.botInfo.args[0]);
|
||||
}
|
||||
|
||||
ctx.dispatch('msg:add', info);
|
||||
};
|
||||
|
||||
const SockChatS2CMessageAdd = (ctx, timeStamp, userId, msgText, msgId, msgFlags) => {
|
||||
const mFlags = SockChatParseMsgFlags(msgFlags);
|
||||
let mText = SockChatUnfuckText(msgText, mFlags.isAction);
|
||||
let mChannelName = ctx.channelName;
|
||||
|
||||
if(msgFlags[4] !== '0') {
|
||||
if(userId === ctx.userId) {
|
||||
const mTextParts = mText.split(' ');
|
||||
mChannelName = `@${mTextParts.shift()}`;
|
||||
mText = mTextParts.join(' ');
|
||||
} else {
|
||||
mChannelName = `@~${userId}`;
|
||||
}
|
||||
}
|
||||
|
||||
const msgInfo = {
|
||||
msg: {
|
||||
id: msgId,
|
||||
time: new Date(parseInt(timeStamp) * 1000),
|
||||
channel: mChannelName,
|
||||
sender: {
|
||||
id: userId,
|
||||
self: userId === ctx.userId,
|
||||
},
|
||||
flags: mFlags,
|
||||
flagsRaw: msgFlags,
|
||||
isBot: userId === '-1',
|
||||
text: mText,
|
||||
},
|
||||
};
|
||||
|
||||
if(msgInfo.msg.isBot) {
|
||||
const botParts = msgText.split("\f");
|
||||
msgInfo.msg.botInfo = {
|
||||
isError: botParts[0] === '1',
|
||||
type: botParts[1],
|
||||
args: botParts.slice(2),
|
||||
};
|
||||
}
|
||||
|
||||
ctx.dispatch('msg:add', msgInfo);
|
||||
};
|
||||
|
||||
const SockChatS2CMessageRemove = (ctx, msgId) => {
|
||||
ctx.dispatch('msg:remove', {
|
||||
msg: {
|
||||
id: msgId,
|
||||
channel: ctx.channelName,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,3 +1,5 @@
|
|||
#include utility.js
|
||||
|
||||
const MamiSettingsBackup = function(settings) {
|
||||
const header = 'Mami Settings Export';
|
||||
const version = 1;
|
||||
|
@ -86,7 +88,7 @@ const MamiSettingsBackup = function(settings) {
|
|||
fileName = 'settings.mami';
|
||||
|
||||
const data = exportData();
|
||||
const html = $element('a', {
|
||||
const html = $e('a', {
|
||||
href: URL.createObjectURL(new Blob([data], { type: 'application/octet-stream' })),
|
||||
download: fileName,
|
||||
target: '_blank',
|
||||
|
@ -100,7 +102,7 @@ const MamiSettingsBackup = function(settings) {
|
|||
importFile: importFile,
|
||||
importUpload: (target) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const html = $element('input', {
|
||||
const html = $e('input', {
|
||||
type: 'file',
|
||||
accept: '.mami',
|
||||
style: { display: 'none' },
|
||||
|
|
|
@ -25,7 +25,7 @@ const MamiSettings = function(storageOrPrefix, eventTarget) {
|
|||
});
|
||||
const dispatchUpdate = (name, value, silent, local) => eventTarget.dispatch(createUpdateEvent(name, value, false, silent, local));
|
||||
|
||||
const broadcast = new BroadcastChannel(`${MAMI_JS}:settings:${storage.name}`);
|
||||
const broadcast = new BroadcastChannel(`${MAMI_MAIN_JS}:settings:${storage.name}`);
|
||||
const broadcastUpdate = (name, value, silent) => {
|
||||
setTimeout(() => broadcast.postMessage({ act: 'update', name: name, value: value, silent: !!silent }), 0);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#include awaitable.js
|
||||
#include ui/messages.jsx
|
||||
|
||||
const MamiSidebarActionClearBacklog = function(settings, sndLib, msgBox) {
|
||||
const MamiSidebarActionClearBacklog = function(settings, sndLib, msgBox, messages) {
|
||||
return {
|
||||
get name() { return 'act:clear-backlog'; },
|
||||
get text() { return 'Clear backlog'; },
|
||||
|
@ -20,6 +20,7 @@ const MamiSidebarActionClearBacklog = function(settings, sndLib, msgBox) {
|
|||
|
||||
sndLib.play('misc:explode');
|
||||
|
||||
messages.clearMessages(settings.get('explosionRadius'));
|
||||
Umi.UI.Messages.Clear(settings.get('explosionRadius'));
|
||||
|
||||
await MamiSleep(1700);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#include utility.js
|
||||
|
||||
const MamiSidebarActionCollapseAll = function() {
|
||||
// this should take a reference to the message container rather than just using $qa
|
||||
|
||||
|
@ -10,7 +12,7 @@ const MamiSidebarActionCollapseAll = function() {
|
|||
},
|
||||
|
||||
onclick: () => {
|
||||
const buttons = $queryAll('[data-embed="1"]');
|
||||
const buttons = $qa('[data-embed="1"]');
|
||||
for(const button of buttons)
|
||||
button.click();
|
||||
},
|
||||
|
|
33
src/mami.js/sidebar/act-toggle.jsx
Normal file
33
src/mami.js/sidebar/act-toggle.jsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
#include animate.js
|
||||
|
||||
const MamiSidebarActionToggle = function(sidebar) {
|
||||
const toggleClose = <i class="fas fa-caret-square-right sidebar-gutter-font-icon"/>;
|
||||
const toggleOpen = <i class="hidden fas fa-caret-square-left sidebar-gutter-font-icon"/>;
|
||||
|
||||
let buttonElem;
|
||||
|
||||
sidebar.frame.watch('mami:sidebar:toggle', ev => {
|
||||
toggleClose.classList.toggle('hidden', !ev.detail.open);
|
||||
toggleOpen.classList.toggle('hidden', ev.detail.open);
|
||||
});
|
||||
|
||||
return {
|
||||
get name() { return 'act:toggle'; },
|
||||
get text() { return 'Toggle sidebar'; },
|
||||
|
||||
click: () => {
|
||||
buttonElem?.click();
|
||||
},
|
||||
|
||||
createdButton: button => {
|
||||
buttonElem = button.element;
|
||||
buttonElem.append(toggleClose, toggleOpen);
|
||||
},
|
||||
|
||||
onclick: async () => {
|
||||
try {
|
||||
await sidebar.toggle();
|
||||
} catch(ex) {}
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,8 +1,6 @@
|
|||
#include animate.js
|
||||
|
||||
const MamiSidebarPanelSettings = function(settings, msgBox) {
|
||||
const copyright = <div class="mami-copyright">
|
||||
<a href="//patchii.net/flashii/mami" target="_blank">Mami</a> # <a href={`//patchii.net/flashii/mami/commit/${window.GIT_HASH}`} target="_blank">{window.GIT_HASH.substring(0, 7)}</a> © <a href="//flash.moe" target="_blank">flash.moe</a><br/>
|
||||
<a href="//patchii.net/flashii/mami" target="_blank">Mami</a> # <a href={`//patchii.net/flashii/mami/commit/${GIT_HASH}`} target="_blank">{GIT_HASH.substring(0, 7)}</a> © <a href="//flash.moe" target="_blank">flash.moe</a><br/>
|
||||
<a href="//railgun.sh/sockchat" target="_blank">Sock Chat documentation</a><br/>
|
||||
</div>;
|
||||
|
||||
|
@ -28,11 +26,12 @@ const MamiSidebarPanelSettings = function(settings, msgBox) {
|
|||
{body}
|
||||
</div>;
|
||||
|
||||
let animeAbort;
|
||||
let animation;
|
||||
header.onclick = () => {
|
||||
animeAbort?.abort();
|
||||
animeAbort = new AbortController;
|
||||
const signal = animeAbort.signal;
|
||||
if(animation !== undefined) {
|
||||
animation.cancel();
|
||||
animation = undefined;
|
||||
}
|
||||
|
||||
const closed = category.classList.contains('js-settings-closed');
|
||||
let start, update, end, height;
|
||||
|
@ -54,13 +53,13 @@ const MamiSidebarPanelSettings = function(settings, msgBox) {
|
|||
|
||||
category.classList.toggle('js-settings-closed', !closed);
|
||||
|
||||
start();
|
||||
MamiAnimate({
|
||||
animation = MamiAnimate({
|
||||
duration: 500,
|
||||
easing: 'outExpo',
|
||||
signal,
|
||||
update,
|
||||
}).then(() => { end(); }).catch(ex => { });
|
||||
start: start,
|
||||
update: update,
|
||||
end: end,
|
||||
});
|
||||
};
|
||||
|
||||
html.insertBefore(category, copyright);
|
||||
|
@ -192,7 +191,7 @@ const MamiSidebarPanelSettings = function(settings, msgBox) {
|
|||
|
||||
const updateSelectOptions = () => {
|
||||
const options = setting.options();
|
||||
$removeChildren(input);
|
||||
$rc(input);
|
||||
for(const name in options)
|
||||
input.appendChild(<option class={`setting__style setting__style--${setting.name}-${name}`} value={name}>
|
||||
{options[name]}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
#include animate.js
|
||||
#include utility.js
|
||||
|
||||
const MamiSidebarPanelUploadsEntry = function(fileInfo) {
|
||||
const options = new Map;
|
||||
let uploadInfo;
|
||||
let detailsElem, thumbElem, nameElem, progElem, optsElem;
|
||||
|
||||
const html = <div class="sidebar__user" style="background: linear-gradient(270deg, transparent 0, #111 40%) #222; margin-bottom: 1px;">
|
||||
{detailsElem = <div class="sidebar__user-details" title={fileInfo.name} style="transition: height .2s" onclick={() => { optsElem.classList.toggle('hidden'); }}>
|
||||
const html = <div class="sidebar__user" style="background: linear-gradient(270deg, transparent 0, var(--theme-colour-sidebar-background) 40%) var(--theme-colour-sidebar-background-highlight)">
|
||||
{detailsElem = <div class="sidebar__user-details" title={fileInfo.name} style="transition: height .2s" onclick={() => { toggleOptions(); }}>
|
||||
{thumbElem = <div class="sidebar__user-avatar hidden" style="transition: width .2s, height .2s" onmouseover={() => {
|
||||
thumbElem.style.width = '100px';
|
||||
detailsElem.style.height = thumbElem.style.height = '100px';
|
||||
|
@ -15,9 +18,98 @@ const MamiSidebarPanelUploadsEntry = function(fileInfo) {
|
|||
{nameElem = <div class="sidebar__user-name" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{fileInfo.name}</div>}
|
||||
</div>}
|
||||
{progElem = <progress class="eeprom-item-progress" max="100" value="0"/>}
|
||||
{optsElem = <div class="sidebar__user-options"/>}
|
||||
{optsElem = <div class="sidebar__user-options hidden"/>}
|
||||
</div>;
|
||||
|
||||
let optionsVisible = false, optionsAnim, optionsTimeout;
|
||||
const toggleOptions = async state => {
|
||||
if(state === undefined)
|
||||
state = !optionsVisible;
|
||||
else if(state === optionsVisible)
|
||||
return;
|
||||
|
||||
if(optionsTimeout !== undefined) {
|
||||
clearTimeout(optionsTimeout);
|
||||
optionsTimeout = undefined;
|
||||
}
|
||||
|
||||
if(optionsAnim !== undefined) {
|
||||
optionsAnim.cancel();
|
||||
optionsAnim = undefined;
|
||||
}
|
||||
|
||||
let start, update, end, height;
|
||||
|
||||
if(state) {
|
||||
await reloadOptions();
|
||||
start = () => {
|
||||
optsElem.classList.remove('hidden');
|
||||
const curHeight = optsElem.style.height;
|
||||
optsElem.style.height = null;
|
||||
height = optsElem.clientHeight;
|
||||
optsElem.style.height = curHeight;
|
||||
};
|
||||
update = function(t) {
|
||||
optsElem.style.height = `${height * t}px`;
|
||||
};
|
||||
end = () => {
|
||||
optsElem.style.height = null;
|
||||
};
|
||||
} else {
|
||||
start = () => {
|
||||
height = optsElem.clientHeight;
|
||||
};
|
||||
update = t => {
|
||||
optsElem.style.height = `${height - (height * t)}px`;
|
||||
};
|
||||
end = () => {
|
||||
optsElem.style.height = '0';
|
||||
optsElem.classList.add('hidden');
|
||||
$rc(optsElem);
|
||||
};
|
||||
}
|
||||
|
||||
optionsAnim = MamiAnimate({
|
||||
async: true,
|
||||
delayed: true,
|
||||
duration: 500,
|
||||
easing: 'outExpo',
|
||||
start: start,
|
||||
update: update,
|
||||
end: end,
|
||||
});
|
||||
|
||||
optionsVisible = state;
|
||||
|
||||
return optionsAnim.start();
|
||||
};
|
||||
|
||||
const reloadOptions = async () => {
|
||||
for(const option of options.values()) {
|
||||
if(typeof option.info.condition === 'function' && !await option.info.condition(public)) {
|
||||
if(optsElem.contains(option.element))
|
||||
optsElem.removeChild(option.element);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(option.element === undefined) {
|
||||
let text;
|
||||
if(typeof option.info.formatText === 'function')
|
||||
text = option.info.formatText(public);
|
||||
else
|
||||
text = option.info.text ?? option.info.name;
|
||||
|
||||
option.element = <div class="sidebar__user-option" onclick={ev => {
|
||||
if(typeof option.info.onclick === 'function')
|
||||
option.info.onclick(public, option.element, ev);
|
||||
}}>{text}</div>;
|
||||
}
|
||||
|
||||
if(!optsElem.contains(option.element))
|
||||
optsElem.appendChild(option.element);
|
||||
}
|
||||
};
|
||||
|
||||
const public = {
|
||||
get element() { return html; },
|
||||
|
||||
|
@ -54,10 +146,8 @@ const MamiSidebarPanelUploadsEntry = function(fileInfo) {
|
|||
progElem.value = Math.ceil(value * progElem.max);
|
||||
},
|
||||
|
||||
get optionsVisible() { return !optsElem.classList.contains('hidden'); },
|
||||
set optionsVisible(value) {
|
||||
optsElem.classList.toggle('hidden', !value);
|
||||
},
|
||||
get optionsVisible() { return optionsVisible; },
|
||||
setOptionsVisible: toggleOptions,
|
||||
|
||||
nukeProgress: () => {
|
||||
if(progElem === undefined)
|
||||
|
@ -69,42 +159,41 @@ const MamiSidebarPanelUploadsEntry = function(fileInfo) {
|
|||
|
||||
hasOption: name => options.has(name),
|
||||
getOptionNames: () => Array.from(options.keys()),
|
||||
addOption: option => {
|
||||
addOption: async option => {
|
||||
if(options.has(option.name))
|
||||
throw 'option has already been defined';
|
||||
|
||||
let text;
|
||||
if(typeof option.formatText === 'function')
|
||||
text = option.formatText(public);
|
||||
else
|
||||
text = option.text ?? option.name;
|
||||
|
||||
const elem = <div class="sidebar__user-option" onclick={ev => {
|
||||
if(typeof option.onclick === 'function')
|
||||
option.onclick(public, elem, ev);
|
||||
}}>{text}</div>;
|
||||
|
||||
options.set(option.name, {
|
||||
option: option,
|
||||
element: elem,
|
||||
info: option,
|
||||
element: undefined,
|
||||
});
|
||||
optsElem.appendChild(elem);
|
||||
|
||||
if(!optionsVisible)
|
||||
await reloadOptions();
|
||||
},
|
||||
removeOption: name => {
|
||||
removeOption: async name => {
|
||||
const info = options.get(name);
|
||||
if(info === undefined)
|
||||
return;
|
||||
|
||||
optsElem.removeChild(info.element);
|
||||
if(optsElem.contains(info.element))
|
||||
optsElem.removeChild(info.element);
|
||||
options.delete(name);
|
||||
|
||||
if(!optionsVisible)
|
||||
await reloadOptions();
|
||||
},
|
||||
clickOption: name => {
|
||||
const info = options.get(name) ?? options.get(`:${name}`);
|
||||
if(info === undefined)
|
||||
const option = options.get(name) ?? options.get(`:${name}`);
|
||||
if(option === undefined)
|
||||
return;
|
||||
|
||||
info.element.click();
|
||||
if(option.element !== undefined)
|
||||
option.element.click();
|
||||
else if(typeof option.info.onclick === 'function')
|
||||
option.info.onclick(public);
|
||||
},
|
||||
reloadOptions: reloadOptions,
|
||||
};
|
||||
|
||||
return public;
|
||||
|
@ -113,29 +202,26 @@ const MamiSidebarPanelUploadsEntry = function(fileInfo) {
|
|||
const MamiSidebarPanelUploads = function() {
|
||||
const html = <div class="sidebar__menu--uploads"/>;
|
||||
const options = new Map;
|
||||
const entries = new Set;
|
||||
const entries = [];
|
||||
|
||||
const reloadOptionsFor = entry => {
|
||||
const reloadOptionsFor = async entry => {
|
||||
const names = entry.getOptionNames();
|
||||
|
||||
for(const name of names)
|
||||
if(name.startsWith(':') && !options.has(name))
|
||||
entry.removeOption(name);
|
||||
await entry.removeOption(name);
|
||||
|
||||
for(const [name, option] of options) {
|
||||
if(typeof option.condition !== 'function' || option.condition(entry)) {
|
||||
if(!names.includes(name))
|
||||
entry.addOption(option);
|
||||
} else {
|
||||
if(names.includes(name))
|
||||
entry.removeOption(name);
|
||||
}
|
||||
}
|
||||
for(const [name, option] of options)
|
||||
if(!names.includes(name))
|
||||
await entry.addOption(option);
|
||||
|
||||
if(entry.optionsVisible)
|
||||
await entry.reloadOptions();
|
||||
};
|
||||
|
||||
const reloadOptions = () => {
|
||||
const reloadOptions = async () => {
|
||||
for(const entry of entries)
|
||||
reloadOptionsFor(entry);
|
||||
await reloadOptionsFor(entry);
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -150,7 +236,7 @@ const MamiSidebarPanelUploads = function() {
|
|||
|
||||
createEntry: info => {
|
||||
const entry = new MamiSidebarPanelUploadsEntry(info);
|
||||
entries.add(entry);
|
||||
entries.push(entry);
|
||||
|
||||
reloadOptionsFor(entry);
|
||||
html.insertBefore(entry.element, html.firstElementChild);
|
||||
|
@ -158,23 +244,24 @@ const MamiSidebarPanelUploads = function() {
|
|||
return entry;
|
||||
},
|
||||
deleteEntry: entry => {
|
||||
if(!entries.has(entry))
|
||||
if(!entries.includes(entry))
|
||||
return;
|
||||
|
||||
html.removeChild(entry.element);
|
||||
entries.delete(entry);
|
||||
if(html.contains(entry.element))
|
||||
html.removeChild(entry.element);
|
||||
$ari(entries, entry);
|
||||
},
|
||||
|
||||
addOption: option => {
|
||||
addOption: async option => {
|
||||
if(!option.name.startsWith(':'))
|
||||
option.name = `:${option.name}`;
|
||||
if(options.has(option.name))
|
||||
throw 'option has already been defined';
|
||||
|
||||
options.set(option.name, option);
|
||||
reloadOptions();
|
||||
await reloadOptions();
|
||||
},
|
||||
removeOption: name => {
|
||||
removeOption: async name => {
|
||||
if(!name.startsWith(':'))
|
||||
name = `:${name}`;
|
||||
|
||||
|
@ -182,7 +269,7 @@ const MamiSidebarPanelUploads = function() {
|
|||
return;
|
||||
|
||||
options.delete(name);
|
||||
reloadOptions();
|
||||
await reloadOptions();
|
||||
},
|
||||
reloadOptions: reloadOptions,
|
||||
reloadOptionsFor: reloadOptionsFor,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include animate.js
|
||||
#include users.js
|
||||
#include avatar.js
|
||||
#include utility.js
|
||||
|
||||
const MamiSidebarPanelUsersEntry = function(info) {
|
||||
const id = info.id;
|
||||
|
@ -12,7 +13,7 @@ const MamiSidebarPanelUsersEntry = function(info) {
|
|||
const options = new Map;
|
||||
let avatarElem, nameElem, nameWrapperElem, statusElem, optsElem;
|
||||
const html = <div class="sidebar__user">
|
||||
<div class="sidebar__user-details" onclick={() => { setOptionsVisible(); }}>
|
||||
<div class="sidebar__user-details" onclick={() => { toggleOptions(); }}>
|
||||
{avatarElem = <div class="sidebar__user-avatar" />}
|
||||
{nameElem = <div class="sidebar__user-name"/>}
|
||||
</div>
|
||||
|
@ -61,10 +62,8 @@ const MamiSidebarPanelUsersEntry = function(info) {
|
|||
|
||||
setStatusMessage(statusMessage);
|
||||
|
||||
let optionsVisible = false;
|
||||
let animeAbort;
|
||||
let optionsTimeout;
|
||||
const setOptionsVisible = state => {
|
||||
let optionsVisible = false, optionsAnim, optionsTimeout;
|
||||
const toggleOptions = async state => {
|
||||
if(state === undefined)
|
||||
state = !optionsVisible;
|
||||
else if(state === optionsVisible)
|
||||
|
@ -75,9 +74,10 @@ const MamiSidebarPanelUsersEntry = function(info) {
|
|||
optionsTimeout = undefined;
|
||||
}
|
||||
|
||||
animeAbort?.abort();
|
||||
animeAbort = new AbortController;
|
||||
const signal = animeAbort.signal;
|
||||
if(optionsAnim !== undefined) {
|
||||
optionsAnim.cancel();
|
||||
optionsAnim = undefined;
|
||||
}
|
||||
|
||||
let start, update, end, height;
|
||||
|
||||
|
@ -85,9 +85,11 @@ const MamiSidebarPanelUsersEntry = function(info) {
|
|||
if(mami.settings.get('autoCloseUserContext'))
|
||||
optionsTimeout = setTimeout(() => {
|
||||
if(mami.settings.get('autoCloseUserContext'))
|
||||
setOptionsVisible(false);
|
||||
toggleOptions(false);
|
||||
}, 300000);
|
||||
|
||||
await reloadOptions();
|
||||
|
||||
start = () => {
|
||||
optsElem.classList.remove('hidden');
|
||||
const curHeight = optsElem.style.height;
|
||||
|
@ -95,7 +97,7 @@ const MamiSidebarPanelUsersEntry = function(info) {
|
|||
height = optsElem.clientHeight;
|
||||
optsElem.style.height = curHeight;
|
||||
};
|
||||
update = t => {
|
||||
update = function(t) {
|
||||
optsElem.style.height = `${height * t}px`;
|
||||
};
|
||||
end = () => {
|
||||
|
@ -111,24 +113,53 @@ const MamiSidebarPanelUsersEntry = function(info) {
|
|||
end = () => {
|
||||
optsElem.style.height = '0';
|
||||
optsElem.classList.add('hidden');
|
||||
$rc(optsElem);
|
||||
};
|
||||
}
|
||||
|
||||
optionsVisible = state;
|
||||
|
||||
start();
|
||||
MamiAnimate({
|
||||
optionsAnim = MamiAnimate({
|
||||
async: true,
|
||||
delayed: true,
|
||||
duration: 500,
|
||||
easing: 'outExpo',
|
||||
signal,
|
||||
update,
|
||||
}).then(() => { end(); }).catch(ex => { });
|
||||
start: start,
|
||||
update: update,
|
||||
end: end,
|
||||
});
|
||||
|
||||
optionsVisible = state;
|
||||
|
||||
return optionsAnim.start();
|
||||
};
|
||||
|
||||
const reloadOptions = async () => {
|
||||
for(const option of options.values()) {
|
||||
if(typeof option.info.condition === 'function' && !await option.info.condition(public)) {
|
||||
if(optsElem.contains(option.element))
|
||||
optsElem.removeChild(option.element);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(option.element === undefined) {
|
||||
let text;
|
||||
if(typeof option.info.formatText === 'function')
|
||||
text = option.info.formatText(public);
|
||||
else
|
||||
text = option.info.text ?? option.info.name;
|
||||
|
||||
option.element = <div class="sidebar__user-option" onclick={ev => {
|
||||
if(typeof option.info.onclick === 'function')
|
||||
option.info.onclick(public, option.element, ev);
|
||||
}}>{text}</div>;
|
||||
}
|
||||
|
||||
if(!optsElem.contains(option.element))
|
||||
optsElem.appendChild(option.element);
|
||||
}
|
||||
};
|
||||
|
||||
let avatar;
|
||||
const updateAvatar = url => {
|
||||
avatar = new MamiUserAvatarInfo(id);
|
||||
avatarElem.style.backgroundImage = `url('${avatar.x60}')`;
|
||||
avatarElem.style.backgroundImage = `url('${MamiFormatUserAvatarUrl(id, 60)}')`;
|
||||
};
|
||||
updateAvatar();
|
||||
|
||||
|
@ -156,48 +187,47 @@ const MamiSidebarPanelUsersEntry = function(info) {
|
|||
set statusMessage(value) { setStatusMessage(value); },
|
||||
|
||||
get optionsVisible() { return optionsVisible; },
|
||||
setOptionsVisible: setOptionsVisible,
|
||||
setOptionsVisible: toggleOptions,
|
||||
|
||||
updateAvatar: updateAvatar,
|
||||
|
||||
hasOption: name => options.has(name),
|
||||
getOptionNames: () => Array.from(options.keys()),
|
||||
addOption: option => {
|
||||
addOption: async option => {
|
||||
if(options.has(option.name))
|
||||
throw 'option has already been defined';
|
||||
|
||||
let text;
|
||||
if(typeof option.formatText === 'function')
|
||||
text = option.formatText(public);
|
||||
else
|
||||
text = option.text ?? option.name;
|
||||
|
||||
const elem = <div class="sidebar__user-option" onclick={ev => {
|
||||
if(typeof option.onclick === 'function')
|
||||
option.onclick(public, elem, ev);
|
||||
}}>{text}</div>;
|
||||
|
||||
options.set(option.name, {
|
||||
option: option,
|
||||
element: elem,
|
||||
info: option,
|
||||
element: undefined,
|
||||
});
|
||||
optsElem.appendChild(elem);
|
||||
|
||||
if(!optionsVisible)
|
||||
await reloadOptions();
|
||||
},
|
||||
removeOption: name => {
|
||||
removeOption: async name => {
|
||||
const info = options.get(name);
|
||||
if(info === undefined)
|
||||
return;
|
||||
|
||||
optsElem.removeChild(info.element);
|
||||
if(optsElem.contains(info.element))
|
||||
optsElem.removeChild(info.element);
|
||||
options.delete(name);
|
||||
|
||||
if(!optionsVisible)
|
||||
await reloadOptions();
|
||||
},
|
||||
clickOption: name => {
|
||||
const info = options.get(name);
|
||||
if(info === undefined)
|
||||
const option = options.get(name) ?? options.get(`:${name}`);
|
||||
if(option === undefined)
|
||||
return;
|
||||
|
||||
info.element.click();
|
||||
if(option.element !== undefined)
|
||||
option.element.click();
|
||||
else if(typeof option.info.onclick === 'function')
|
||||
option.info.onclick(public);
|
||||
},
|
||||
reloadOptions: reloadOptions,
|
||||
};
|
||||
|
||||
return public;
|
||||
|
@ -208,27 +238,24 @@ const MamiSidebarPanelUsers = function() {
|
|||
const options = new Map;
|
||||
const entries = new Map;
|
||||
|
||||
const reloadOptionsFor = entry => {
|
||||
const reloadOptionsFor = async entry => {
|
||||
const names = entry.getOptionNames();
|
||||
|
||||
for(const name of names)
|
||||
if(name.startsWith(':') && !options.has(name))
|
||||
entry.removeOption(name);
|
||||
await entry.removeOption(name);
|
||||
|
||||
for(const [name, option] of options) {
|
||||
if(typeof option.condition !== 'function' || option.condition(entry)) {
|
||||
if(!names.includes(name))
|
||||
entry.addOption(option);
|
||||
} else {
|
||||
if(names.includes(name))
|
||||
entry.removeOption(name);
|
||||
}
|
||||
}
|
||||
for(const [name, option] of options)
|
||||
if(!names.includes(name))
|
||||
await entry.addOption(option);
|
||||
|
||||
if(entry.optionsVisible)
|
||||
await entry.reloadOptions();
|
||||
};
|
||||
|
||||
const reloadOptions = () => {
|
||||
const reloadOptions = async () => {
|
||||
for(const entry of entries)
|
||||
reloadOptionsFor(entry);
|
||||
await reloadOptionsFor(entry);
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -289,16 +316,16 @@ const MamiSidebarPanelUsers = function() {
|
|||
}
|
||||
},
|
||||
|
||||
addOption: option => {
|
||||
addOption: async option => {
|
||||
if(!option.name.startsWith(':'))
|
||||
option.name = `:${option.name}`;
|
||||
if(options.has(option.name))
|
||||
throw 'option has already been defined';
|
||||
|
||||
options.set(option.name, option);
|
||||
reloadOptions();
|
||||
await reloadOptions();
|
||||
},
|
||||
removeOption: name => {
|
||||
removeOption: async name => {
|
||||
if(!name.startsWith(':'))
|
||||
name = `:${name}`;
|
||||
|
||||
|
@ -306,7 +333,7 @@ const MamiSidebarPanelUsers = function() {
|
|||
return;
|
||||
|
||||
options.delete(name);
|
||||
reloadOptions();
|
||||
await reloadOptions();
|
||||
},
|
||||
reloadOptions: reloadOptions,
|
||||
reloadOptionsFor: reloadOptionsFor,
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#include animate.js
|
||||
|
||||
const MamiSidebarGutterButton = function(name, info) {
|
||||
const html = <button type="button" class="sidebar__selector-mode" title={info.text ?? name}/>;
|
||||
const isBottom = info.pos === 'bottom';
|
||||
|
@ -128,7 +126,7 @@ const MamiSidebarPanelFrame = function() {
|
|||
const html = <div class="sidebar__menus"/>;
|
||||
const panels = new Map;
|
||||
let activePanel;
|
||||
let animeAbort;
|
||||
let animation;
|
||||
|
||||
const width = 220; // should probably not be hardcoded
|
||||
const isOpen = () => !html.classList.contains('js-menu-closed');
|
||||
|
@ -150,9 +148,10 @@ const MamiSidebarPanelFrame = function() {
|
|||
else if(isOpened === open)
|
||||
return;
|
||||
|
||||
animeAbort?.abort();
|
||||
animeAbort = new AbortController;
|
||||
const signal = animeAbort.signal;
|
||||
if(animation !== undefined) {
|
||||
animation.cancel();
|
||||
animation = undefined;
|
||||
}
|
||||
|
||||
html.classList.toggle('js-menu-closed', !open);
|
||||
html.dispatchEvent(new CustomEvent('mami:sidebar:toggle', {
|
||||
|
@ -163,26 +162,31 @@ const MamiSidebarPanelFrame = function() {
|
|||
? t => { html.style.width = `${width * t}px`; }
|
||||
: t => { html.style.width = `${width - (width * t)}px`; };
|
||||
|
||||
html.style.width = null;
|
||||
html.style.overflowX = 'hidden';
|
||||
|
||||
if(activePanel !== undefined)
|
||||
activePanel.element.style.minWidth = `${width}px`;
|
||||
|
||||
MamiAnimate({
|
||||
animation = MamiAnimate({
|
||||
async: true,
|
||||
delayed: true,
|
||||
duration: 500,
|
||||
easing: 'outExpo',
|
||||
update,
|
||||
signal,
|
||||
}).then(() => {
|
||||
html.style.overflowX = null;
|
||||
|
||||
if(open)
|
||||
start: () => {
|
||||
html.style.width = null;
|
||||
html.style.overflowX = 'hidden';
|
||||
|
||||
if(activePanel !== undefined)
|
||||
activePanel.element.style.minWidth = null;
|
||||
}).catch(ex => { });
|
||||
if(activePanel !== undefined)
|
||||
activePanel.element.style.minWidth = `${width}px`;
|
||||
},
|
||||
update: update,
|
||||
end: () => {
|
||||
html.style.overflowX = null;
|
||||
|
||||
if(open)
|
||||
html.style.width = null;
|
||||
|
||||
if(activePanel !== undefined)
|
||||
activePanel.element.style.minWidth = null;
|
||||
},
|
||||
});
|
||||
|
||||
return animation.start();
|
||||
},
|
||||
|
||||
getPanel: name => panels.get(name),
|
||||
|
@ -270,13 +274,8 @@ const MamiSidebar = function() {
|
|||
if(info.onclick === undefined)
|
||||
info.onclick = async name => {
|
||||
try {
|
||||
if(frame.isOpen && frame.activePanelName === info.name) {
|
||||
gutter.setActiveButton();
|
||||
frame.toggle(false);
|
||||
} else {
|
||||
switchPanel(name);
|
||||
frame.toggle(true);
|
||||
}
|
||||
switchPanel(name);
|
||||
frame.toggle(true);
|
||||
} catch(ex) {}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
#include compat.js
|
||||
#include proto/sockchat/client.js
|
||||
#include mszauth.js
|
||||
|
||||
const MamiSockChat = function(eventTarget) {
|
||||
const MamiSockChat = function(protoWorker) {
|
||||
const events = protoWorker.eventTarget('sockchat');
|
||||
let restarting = false;
|
||||
let client;
|
||||
let dumpPackets = false;
|
||||
|
@ -9,36 +10,39 @@ const MamiSockChat = function(eventTarget) {
|
|||
return {
|
||||
get client() { return client; },
|
||||
|
||||
get dumpPackets() { return dumpPackets; },
|
||||
set dumpPackets(value) {
|
||||
dumpPackets = !!value;
|
||||
if(client) client.dumpPackets = dumpPackets;
|
||||
},
|
||||
watch: events.watch,
|
||||
unwatch: events.unwatch,
|
||||
|
||||
watch: eventTarget.watch,
|
||||
unwatch: eventTarget.unwatch,
|
||||
|
||||
async create() {
|
||||
if(client) client.close();
|
||||
create: async () => {
|
||||
if(client !== undefined && typeof client.close === 'function')
|
||||
// intentional fire & forget, worker may be gone, don't want to wait for it to time out
|
||||
client.close();
|
||||
|
||||
restarting = false;
|
||||
client = new SockChatClient(eventTarget.dispatch, { ping: 30 });
|
||||
client.dumpPackets = dumpPackets;
|
||||
client = await protoWorker.root.create('sockchat', { ping: futami.get('ping') });
|
||||
await client.setDumpPackets(dumpPackets);
|
||||
|
||||
MamiCompat('Umi.Server', { get: () => client, configurable: true });
|
||||
MamiCompat('Umi.Server.SendMessage', { value: text => client.sendMessage(text), configurable: true });
|
||||
MamiCompat('Umi.Protocol.SockChat.Protocol.Instance.SendMessage', { value: text => client.sendMessage(text), configurable: true });
|
||||
MamiCompat('Umi.Protocol.SockLegacy.Protocol.Instance.SendMessage', { value: text => client.sendMessage(text), configurable: true });
|
||||
},
|
||||
async connect(url) {
|
||||
connect: async url => {
|
||||
try {
|
||||
await client.open(url);
|
||||
} catch(ex) {
|
||||
return ex?.wasKicked === true;
|
||||
return ex.wasKicked === true;
|
||||
}
|
||||
},
|
||||
async authenticate(auth) {
|
||||
await client.sendAuth(auth.type, auth.token);
|
||||
authenticate: async () => {
|
||||
const authInfo = MamiMisuzuAuth.getInfo();
|
||||
await client.sendAuth(authInfo.method, authInfo.token);
|
||||
},
|
||||
setDumpPackets: async state => {
|
||||
dumpPackets = !!state;
|
||||
|
||||
if(client !== undefined && typeof client.setDumpPackets === 'function')
|
||||
await client.setDumpPackets(dumpPackets);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#include animate.js
|
||||
#include messages.js
|
||||
#include parsing.js
|
||||
#include users.js
|
||||
#include notices/baka.jsx
|
||||
#include sockchat/modal.js
|
||||
#include ui/emotes.js
|
||||
|
@ -9,7 +7,7 @@
|
|||
|
||||
const MamiSockChatHandlers = function(
|
||||
ctx, client, setLoadingOverlay, sockChatReconnect, pingIndicator,
|
||||
sbActPing, sbChannels, sbUsers
|
||||
sbActPing, sbChannels, sbUsers, messages
|
||||
) {
|
||||
if(typeof ctx !== 'object' || ctx === null)
|
||||
throw 'ctx must be an non-null object';
|
||||
|
@ -35,14 +33,14 @@ const MamiSockChatHandlers = function(
|
|||
handlers['conn:open'] = ev => {
|
||||
if(dumpEvents) console.log('conn:open', ev.detail);
|
||||
|
||||
setLoadingOverlay('spinner', 'Authenticating...', true);
|
||||
setLoadingOverlay('spinner', 'Connecting...', 'Authenticating...', true);
|
||||
};
|
||||
|
||||
let legacyUmiConnectFired = false;
|
||||
handlers['conn:ready'] = ev => {
|
||||
if(dumpEvents) console.log('conn:ready');
|
||||
|
||||
client.authenticate(ctx.auth).then(() => {
|
||||
client.authenticate().then(() => {
|
||||
if(!legacyUmiConnectFired) {
|
||||
legacyUmiConnectFired = true;
|
||||
ctx.globalEvents.dispatch('umi:connect');
|
||||
|
@ -86,27 +84,10 @@ const MamiSockChatHandlers = function(
|
|||
if(dumpEvents) console.log('session:start', ev.detail);
|
||||
|
||||
sockChatRestarting = false;
|
||||
|
||||
const userInfo = new MamiUserInfo(
|
||||
ev.detail.user.id,
|
||||
ev.detail.user.name,
|
||||
ev.detail.user.colour,
|
||||
new MamiUserStatusInfo(ev.detail.user.status.isAway, ev.detail.user.status.message),
|
||||
new MamiUserPermsInfo(
|
||||
ev.detail.user.perms.rank, ev.detail.user.perms.kick,
|
||||
ev.detail.user.perms.nick, ev.detail.user.perms.chan,
|
||||
),
|
||||
);
|
||||
Umi.User.setCurrentUser(userInfo);
|
||||
Umi.Users.Add(userInfo);
|
||||
|
||||
if(sbUsers.hasEntry(ev.detail.user.id))
|
||||
sbUsers.updateEntry(ev.detail.user.id, ev.detail.user);
|
||||
else
|
||||
sbUsers.createEntry(ev.detail.user);
|
||||
sbUsers.createEntry(ev.detail.user);
|
||||
|
||||
if(ctx.views.count > 1)
|
||||
ctx.views.pop();
|
||||
ctx.views.pop();
|
||||
};
|
||||
handlers['session:fail'] = ev => {
|
||||
if(dumpEvents) console.log('session:fail', ev.detail);
|
||||
|
@ -117,11 +98,11 @@ const MamiSockChatHandlers = function(
|
|||
}
|
||||
|
||||
if(ev.detail.session.needsAuth) {
|
||||
ctx.flashii.v1.chat.login();
|
||||
location.assign(futami.get('login'));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingOverlay('cross', ev.detail.session.outOfConnections ? 'You have too many active connections.' : 'Could not authenticate.');
|
||||
setLoadingOverlay('cross', 'Failed!', ev.detail.session.outOfConnections ? 'Too many active connections.' : 'Unspecified reason.');
|
||||
};
|
||||
handlers['session:term'] = ev => {
|
||||
if(dumpEvents) console.log('session:term', ev.detail);
|
||||
|
@ -136,76 +117,32 @@ const MamiSockChatHandlers = function(
|
|||
if(ev.detail.user.self)
|
||||
return;
|
||||
|
||||
const userInfo = new MamiUserInfo(
|
||||
ev.detail.user.id,
|
||||
ev.detail.user.name,
|
||||
ev.detail.user.colour,
|
||||
new MamiUserStatusInfo(ev.detail.user.status.isAway, ev.detail.user.status.message),
|
||||
new MamiUserPermsInfo(
|
||||
ev.detail.user.perms.rank, ev.detail.user.perms.kick,
|
||||
ev.detail.user.perms.nick, ev.detail.user.perms.chan,
|
||||
),
|
||||
);
|
||||
Umi.Users.Add(userInfo);
|
||||
|
||||
sbUsers.createEntry(ev.detail.user);
|
||||
|
||||
if(ev.detail.msg !== undefined)
|
||||
Umi.UI.Messages.Add(new MamiMessageInfo(
|
||||
'user:join',
|
||||
ev.detail.msg.time,
|
||||
null,
|
||||
ev.detail.msg.id,
|
||||
new MamiMessageAuthorInfo(ev.detail.user.self, userInfo),
|
||||
ev.detail.msg.channel
|
||||
));
|
||||
if(ev.detail.msg !== undefined) {
|
||||
messages.addMessage(ev.detail.msg);
|
||||
Umi.UI.Messages.Add(ev.detail.msg);
|
||||
}
|
||||
};
|
||||
handlers['user:remove'] = ev => {
|
||||
if(dumpEvents) console.log('user:remove', ev.detail);
|
||||
|
||||
sbUsers.deleteEntry(ev.detail.user.id);
|
||||
|
||||
const userInfo = Umi.Users.Get(ev.detail.user.id);
|
||||
if(userInfo === null)
|
||||
return;
|
||||
|
||||
if(ev.detail.msg !== undefined)
|
||||
Umi.UI.Messages.Add(new MamiMessageInfo(
|
||||
'user:leave',
|
||||
ev.detail.msg.time,
|
||||
{ reason: ev.detail.leave.type },
|
||||
ev.detail.msg.id,
|
||||
new MamiMessageAuthorInfo(ev.detail.user.self, userInfo),
|
||||
ev.detail.msg.channel
|
||||
));
|
||||
|
||||
Umi.Users.Remove(userInfo);
|
||||
if(ev.detail.msg !== undefined) {
|
||||
messages.addMessage(ev.detail.msg);
|
||||
Umi.UI.Messages.Add(ev.detail.msg);
|
||||
}
|
||||
};
|
||||
handlers['user:update'] = ev => {
|
||||
if(dumpEvents) console.log('user:update', ev.detail);
|
||||
|
||||
sbUsers.updateEntry(ev.detail.user.id, ev.detail.user);
|
||||
|
||||
const userInfo = Umi.Users.Get(ev.detail.user.id);
|
||||
userInfo.name = ev.detail.user.name;
|
||||
userInfo.colour = ev.detail.user.colour;
|
||||
userInfo.avatar = new MamiUserAvatarInfo(ev.detail.user.id);
|
||||
userInfo.status = new MamiUserStatusInfo(ev.detail.user.status.isAway, ev.detail.user.status.message);
|
||||
userInfo.perms = new MamiUserPermsInfo(
|
||||
ev.detail.user.perms.rank, ev.detail.user.perms.kick,
|
||||
ev.detail.user.perms.nick, ev.detail.user.perms.chan,
|
||||
);
|
||||
Umi.Users.Update(userInfo.id, userInfo);
|
||||
};
|
||||
handlers['user:clear'] = () => {
|
||||
if(dumpEvents) console.log('user:clear');
|
||||
|
||||
sbUsers.clearEntries();
|
||||
|
||||
const self = Umi.User.getCurrentUser();
|
||||
Umi.Users.Clear();
|
||||
if(self !== undefined)
|
||||
Umi.Users.Add(self);
|
||||
};
|
||||
|
||||
|
||||
|
@ -233,33 +170,18 @@ const MamiSockChatHandlers = function(
|
|||
if(dumpEvents) console.log('chan:focus', ev.detail);
|
||||
|
||||
sbChannels.setActiveEntry(ev.detail.channel.name);
|
||||
messages.switchChannel(ev.detail.channel.name);
|
||||
Umi.UI.Messages.SwitchChannel(ev.detail.channel);
|
||||
};
|
||||
handlers['chan:join'] = ev => {
|
||||
if(dumpEvents) console.log('chan:join', ev.detail);
|
||||
|
||||
const userInfo = new MamiUserInfo(
|
||||
ev.detail.user.id,
|
||||
ev.detail.user.name,
|
||||
ev.detail.user.colour,
|
||||
new MamiUserStatusInfo(ev.detail.user.status.isAway, ev.detail.user.status.message),
|
||||
new MamiUserPermsInfo(
|
||||
ev.detail.user.perms.rank, ev.detail.user.perms.kick,
|
||||
ev.detail.user.perms.nick, ev.detail.user.perms.chan,
|
||||
)
|
||||
);
|
||||
Umi.Users.Add(userInfo);
|
||||
|
||||
sbUsers.createEntry(ev.detail.user);
|
||||
|
||||
if(ev.detail.msg !== undefined)
|
||||
Umi.UI.Messages.Add(new MamiMessageInfo(
|
||||
'channel:join',
|
||||
null, null,
|
||||
ev.detail.msg.id,
|
||||
new MamiMessageAuthorInfo(ev.detail.user.self, userInfo),
|
||||
ev.detail.msg.channel
|
||||
));
|
||||
if(ev.detail.msg !== undefined) {
|
||||
messages.addMessage(ev.detail.msg);
|
||||
Umi.UI.Messages.Add(ev.detail.msg);
|
||||
}
|
||||
};
|
||||
handlers['chan:leave'] = ev => {
|
||||
if(dumpEvents) console.log('chan:leave', ev.detail);
|
||||
|
@ -267,135 +189,29 @@ const MamiSockChatHandlers = function(
|
|||
if(ev.detail.user.self)
|
||||
return;
|
||||
|
||||
const userInfo = Umi.Users.Get(ev.detail.user.id);
|
||||
if(userInfo === null)
|
||||
return;
|
||||
|
||||
if(ev.detail.msg !== undefined)
|
||||
Umi.UI.Messages.Add(new MamiMessageInfo(
|
||||
'channel:leave',
|
||||
null, null,
|
||||
ev.detail.msg.id,
|
||||
new MamiMessageAuthorInfo(ev.detail.user.self, userInfo),
|
||||
ev.detail.msg.channel
|
||||
));
|
||||
|
||||
Umi.Users.Remove(userInfo);
|
||||
if(ev.detail.msg !== undefined) {
|
||||
messages.addMessage(ev.detail.msg);
|
||||
Umi.UI.Messages.Add(ev.detail.msg);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
handlers['msg:add'] = ev => {
|
||||
if(dumpEvents) console.log('msg:add', ev.detail);
|
||||
|
||||
const senderInfo = ev.detail.msg.sender;
|
||||
const rawUserInfo = Umi.Users.Get(senderInfo.id);
|
||||
const userInfo = senderInfo.name === undefined
|
||||
? rawUserInfo
|
||||
: new MamiUserInfo(
|
||||
senderInfo.id,
|
||||
senderInfo.name,
|
||||
senderInfo.colour,
|
||||
new MamiUserStatusInfo(senderInfo.status.isAway, senderInfo.status.message),
|
||||
new MamiUserPermsInfo(
|
||||
senderInfo.perms.rank, senderInfo.perms.kick,
|
||||
senderInfo.perms.nick, senderInfo.perms.chan,
|
||||
)
|
||||
);
|
||||
|
||||
// hack
|
||||
let channelName = ev.detail.msg.channel;
|
||||
if(channelName !== undefined && channelName.startsWith('@~')) {
|
||||
const chanUserInfo = Umi.Users.Get(channelName.substring(2));
|
||||
if(chanUserInfo !== null)
|
||||
channelName = `@${chanUserInfo.name}`;
|
||||
if(ev.detail.msg.type.startsWith('legacy:') && modals.handled(ev.detail.msg.botInfo.type)) {
|
||||
modals.show(ev.detail.msg.botInfo.type, ev.detail.msg.botInfo.args);
|
||||
return;
|
||||
}
|
||||
|
||||
// also hack
|
||||
if(ev.detail.msg.flags.isPM && !sbChannels.hasEntry(channelName))
|
||||
sbChannels.createEntry({
|
||||
name: channelName,
|
||||
hasPassword: false,
|
||||
isTemporary: true,
|
||||
isUserChannel: true,
|
||||
});
|
||||
|
||||
let type, detail, author;
|
||||
if(ev.detail.msg.isBot) {
|
||||
const botInfo = ev.detail.msg.botInfo;
|
||||
let authorMethod;
|
||||
|
||||
if(botInfo.type === 'join') {
|
||||
type = 'user:join';
|
||||
authorMethod = 'nameArg';
|
||||
} else if(['leave', 'kick', 'flood', 'timeout'].includes(botInfo.type)) {
|
||||
type = 'user:leave';
|
||||
authorMethod = 'nameArg';
|
||||
detail = { reason: botInfo.type };
|
||||
} else if(botInfo.type === 'jchan') {
|
||||
type = 'channel:join';
|
||||
authorMethod = 'nameArg';
|
||||
} else if(botInfo.type === 'lchan') {
|
||||
type = 'channel:leave';
|
||||
authorMethod = 'nameArg';
|
||||
}
|
||||
|
||||
if(authorMethod === 'nameArg') {
|
||||
author = botInfo.args[0];
|
||||
authorMethod = 'name';
|
||||
}
|
||||
|
||||
if(authorMethod === 'name') {
|
||||
const botUserInfo = Umi.Users.FindExact(author);
|
||||
author = new MamiMessageAuthorInfo(
|
||||
Umi.User.isCurrentUser(botUserInfo),
|
||||
botUserInfo,
|
||||
null,
|
||||
author
|
||||
);
|
||||
}
|
||||
|
||||
if(typeof type !== 'string') {
|
||||
if(modals.handled(botInfo.type)) {
|
||||
modals.show(botInfo.type, botInfo.args);
|
||||
return;
|
||||
}
|
||||
|
||||
type = `legacy:${botInfo.type}`;
|
||||
detail = {
|
||||
error: botInfo.isError,
|
||||
args: botInfo.args,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
author = new MamiMessageAuthorInfo(
|
||||
senderInfo.self,
|
||||
rawUserInfo,
|
||||
senderInfo.id ?? rawUserInfo?.id,
|
||||
senderInfo.name ?? rawUserInfo?.name,
|
||||
senderInfo.colour ?? rawUserInfo?.colour,
|
||||
senderInfo.perms?.rank ?? rawUserInfo?.perms?.rank ?? 0,
|
||||
senderInfo.avatar ?? rawUserInfo?.avatar ?? new MamiUserAvatarInfo(senderInfo.id ?? rawUserInfo?.id ?? '0'),
|
||||
);
|
||||
|
||||
type = `message:${ev.detail.msg.flags.isAction ? 'action' : 'text'}`;
|
||||
detail = { body: ev.detail.msg.text };
|
||||
}
|
||||
|
||||
sbChannels.setUnreadEntry(channelName);
|
||||
|
||||
Umi.UI.Messages.Add(new MamiMessageInfo(
|
||||
type,
|
||||
ev.detail.msg.time,
|
||||
detail,
|
||||
ev.detail.msg.id,
|
||||
author,
|
||||
channelName,
|
||||
ev.detail.msg.silent,
|
||||
));
|
||||
sbChannels.setUnreadEntry(ev.detail.msg.channel);
|
||||
messages.addMessage(ev.detail.msg);
|
||||
Umi.UI.Messages.Add(ev.detail.msg);
|
||||
};
|
||||
handlers['msg:remove'] = ev => {
|
||||
if(dumpEvents) console.log('msg:remove', ev.detail);
|
||||
|
||||
messages.removeMessage(ev.detail.msg.id);
|
||||
Umi.UI.Messages.Remove(ev.detail.msg.id);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#include utility.js
|
||||
|
||||
const MamiSoundManager = function(context) {
|
||||
const probablySupported = [];
|
||||
const maybeSupported = [];
|
||||
|
@ -10,7 +12,7 @@ const MamiSoundManager = function(context) {
|
|||
};
|
||||
|
||||
(() => {
|
||||
const elem = $element('audio');
|
||||
const elem = $e('audio');
|
||||
for(const name in formats) {
|
||||
const format = formats[name];
|
||||
const support = elem.canPlayType(format);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#include animate.js
|
||||
#include awaitable.js
|
||||
|
||||
const MamiSoundTest = function(settings, audio, manager, library, clickPos) {
|
||||
|
@ -19,7 +18,7 @@ const MamiSoundTest = function(settings, audio, manager, library, clickPos) {
|
|||
const detuneSetting = settings.info('soundDetune');
|
||||
const loopStartSetting = settings.info('soundLoopStart');
|
||||
const loopEndSetting = settings.info('soundLoopEnd');
|
||||
const sources = new Set;
|
||||
const sources = [];
|
||||
|
||||
let libraryButtons;
|
||||
let nowPlaying;
|
||||
|
@ -108,7 +107,7 @@ const MamiSoundTest = function(settings, audio, manager, library, clickPos) {
|
|||
name.textContent += ` (${buffer.duration})`;
|
||||
|
||||
source = audio.createSource(buffer, settings.get('soundReverse'));
|
||||
sources.add(source);
|
||||
sources.push(source);
|
||||
|
||||
state.textContent = 'Configuring...';
|
||||
const rate = settings.get('soundRate');
|
||||
|
@ -131,7 +130,7 @@ const MamiSoundTest = function(settings, audio, manager, library, clickPos) {
|
|||
console.error(ex);
|
||||
state.textContent = `Error: ${ex}`;
|
||||
} finally {
|
||||
sources.delete(source);
|
||||
$ari(sources, source);
|
||||
await MamiSleep(2000);
|
||||
nowPlaying.removeChild(player);
|
||||
}
|
||||
|
@ -150,48 +149,45 @@ const MamiSoundTest = function(settings, audio, manager, library, clickPos) {
|
|||
|
||||
return {
|
||||
get element() { return container; },
|
||||
async onViewPop() {
|
||||
onViewPop: async () => {
|
||||
for(const source of sources)
|
||||
source.stop();
|
||||
},
|
||||
getViewTransition(mode) {
|
||||
getViewTransition: mode => {
|
||||
if(!hasClickPos)
|
||||
return;
|
||||
|
||||
if(mode === 'push')
|
||||
return async ({ toElem }) => {
|
||||
library.play('mario:keyhole');
|
||||
|
||||
toElem.style.transform = 'scale(0) translate(25%, 25%)';
|
||||
toElem.style.transformOrigin = `${clickPos[0]}px ${clickPos[1]}px`;
|
||||
|
||||
await MamiAnimate({
|
||||
duration: 1500,
|
||||
easing: 'inQuad',
|
||||
update(t, rt) {
|
||||
toElem.style.transform = `scale(${t}) translate(${25 * (1 - rt)}%, ${25 * (1 - rt)}%)`;
|
||||
},
|
||||
});
|
||||
|
||||
toElem.style.transform = null;
|
||||
toElem.style.transformOrigin = null;
|
||||
};
|
||||
return ctx => MamiAnimate({
|
||||
async: true,
|
||||
duration: 1500,
|
||||
easing: 'inQuad',
|
||||
start: () => {
|
||||
library.play('mario:keyhole');
|
||||
ctx.toElem.style.transform = 'scale(0) translate(25%, 25%)';
|
||||
ctx.toElem.style.transformOrigin = `${clickPos[0]}px ${clickPos[1]}px`;
|
||||
},
|
||||
update: (t, rt) => ctx.toElem.style.transform = `scale(${t}) translate(${25 * (1 - rt)}%, ${25 * (1 - rt)}%)`,
|
||||
end: () => {
|
||||
ctx.toElem.style.transform = null;
|
||||
ctx.toElem.style.transformOrigin = null;
|
||||
},
|
||||
});
|
||||
|
||||
if(mode === 'pop')
|
||||
return async ({ fromElem }) => {
|
||||
fromElem.style.transformOrigin = `${clickPos[0]}px ${clickPos[1]}px`;
|
||||
|
||||
await MamiAnimate({
|
||||
duration: 1000,
|
||||
easing: 'outQuad',
|
||||
update(t, rt) {
|
||||
fromElem.style.transform = `scale(${1 - t}) rotate(${-1080 * t}deg) translate(${50 * rt}%, ${50 * rt}%)`;
|
||||
},
|
||||
})
|
||||
|
||||
fromElem.style.transform = null;
|
||||
fromElem.style.transformOrigin = null;
|
||||
};
|
||||
return ctx => MamiAnimate({
|
||||
async: true,
|
||||
duration: 1000,
|
||||
easing: 'outQuad',
|
||||
start: () => {
|
||||
ctx.fromElem.style.transformOrigin = `${clickPos[0]}px ${clickPos[1]}px`;
|
||||
},
|
||||
update: (t, rt) => ctx.fromElem.style.transform = `scale(${1 - t}) rotate(${-1080 * t}deg) translate(${50 * rt}%, ${50 * rt}%)`,
|
||||
end: () => {
|
||||
ctx.fromElem.style.transform = null;
|
||||
ctx.fromElem.style.transformOrigin = null;
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
63
src/mami.js/srle.js
Normal file
63
src/mami.js/srle.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
const MamiSRLE = (() => {
|
||||
return {
|
||||
encode: (input, cutoff) => {
|
||||
let output = '';
|
||||
let last = '';
|
||||
let repeat = 0;
|
||||
|
||||
input = (input || '').toString();
|
||||
cutoff = cutoff || 1
|
||||
|
||||
for(let i = 0; i <= input.length; ++i) {
|
||||
const chr = input[i];
|
||||
|
||||
if(last === chr)
|
||||
++repeat;
|
||||
else {
|
||||
if(repeat > cutoff)
|
||||
for(const repChr in repeat.toString())
|
||||
output += ')!@#$%^&*('[parseInt(repChr)];
|
||||
else
|
||||
output += last.repeat(repeat);
|
||||
|
||||
repeat = 0;
|
||||
|
||||
if(chr !== undefined) {
|
||||
output += chr;
|
||||
last = chr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
decode: input => {
|
||||
let output = '';
|
||||
let repeat = '';
|
||||
let chr;
|
||||
|
||||
input = (input || '').toString().split('').reverse();
|
||||
|
||||
for(;;) {
|
||||
const chr = input.pop(), num = ')!@#$%^&*('.indexOf(chr);
|
||||
|
||||
if(num >= 0)
|
||||
repeat += num;
|
||||
else {
|
||||
if(repeat) {
|
||||
output += output.slice(-1).repeat(parseInt(repeat));
|
||||
repeat = '';
|
||||
}
|
||||
|
||||
if(chr === undefined)
|
||||
break;
|
||||
|
||||
output += chr;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -311,7 +311,7 @@ const UmiThemeApply = function(theme) {
|
|||
|
||||
if(theme.colours) {
|
||||
if(typeof theme.colours['main-background'] === 'number') {
|
||||
const themeColour = $query('meta[name="theme-color"]');
|
||||
const themeColour = $q('meta[name="theme-color"]');
|
||||
if(themeColour instanceof Element)
|
||||
themeColour.content = MamiColour.hex(theme.colours['main-background']);
|
||||
}
|
||||
|
|
|
@ -1,40 +1,27 @@
|
|||
const MamiWindowTitle = function({
|
||||
getName=()=>{},
|
||||
setTitle=text=> { document.title = text },
|
||||
strobeInterval=500,
|
||||
strobeRepeat=5,
|
||||
}) {
|
||||
if(typeof getName !== 'function')
|
||||
throw new Error('getName argument must be a function');
|
||||
if(typeof setTitle !== 'function')
|
||||
throw new Error('setTitle argument must be a function');
|
||||
#include args.js
|
||||
|
||||
const setTitleWrap = text => {
|
||||
const MamiWindowTitle = function(options) {
|
||||
options = MamiArgs('options', options, define => {
|
||||
define('getName').default(() => {}).done();
|
||||
define('setTitle').default(text => { document.title = text; }).done();
|
||||
define('strobeInterval').constraint(value => typeof value === 'number' || typeof value === 'function').default(500).done();
|
||||
define('strobeRepeat').constraint(value => typeof value === 'number' || typeof value === 'function').default(5).done();
|
||||
});
|
||||
|
||||
const getName = options.getName;
|
||||
|
||||
const setTitleImpl = options.setTitle;
|
||||
const setTitle = text => {
|
||||
if(text === undefined || text === null)
|
||||
text = '';
|
||||
else if(typeof text !== 'string')
|
||||
text = text.toString();
|
||||
|
||||
setTitle(text);
|
||||
setTitleImpl(text);
|
||||
};
|
||||
|
||||
const getStrobeInverval = () => {
|
||||
let value = strobeInterval;
|
||||
if(typeof value === 'function')
|
||||
value = value();
|
||||
if(typeof value !== 'number' || value < 1)
|
||||
return 500;
|
||||
return value;
|
||||
};
|
||||
|
||||
const getStrobeRepeat = () => {
|
||||
let value = strobeRepeat;
|
||||
if(typeof value === 'function')
|
||||
value = value();
|
||||
if(typeof value !== 'number' || value < 1)
|
||||
return 5;
|
||||
return value;
|
||||
};
|
||||
const defaultStrobeInterval = typeof options.strobeInterval === 'function' ? options.strobeInterval : () => options.strobeInterval;
|
||||
const defaultStrobeRepeat = typeof options.strobeRepeat === 'function' ? options.strobeRepeat : () => options.strobeRepeat;
|
||||
|
||||
let activeStrobe;
|
||||
|
||||
|
@ -45,7 +32,7 @@ const MamiWindowTitle = function({
|
|||
clearInterval(activeStrobe);
|
||||
activeStrobe = undefined;
|
||||
|
||||
setTitleWrap(getName());
|
||||
setTitle(getName());
|
||||
};
|
||||
|
||||
const strobeTitle = (titles, interval, repeat) => {
|
||||
|
@ -54,30 +41,30 @@ const MamiWindowTitle = function({
|
|||
if(titles.length < 1)
|
||||
throw 'titles must contain at least one item';
|
||||
if(typeof interval !== 'number')
|
||||
interval = getStrobeInverval();
|
||||
interval = defaultStrobeInterval();
|
||||
if(typeof repeat !== 'number')
|
||||
repeat = getStrobeRepeat();
|
||||
repeat = defaultStrobeRepeat();
|
||||
|
||||
const target = titles.length * repeat;
|
||||
let round = 0;
|
||||
|
||||
clearTitle();
|
||||
setTitleWrap(titles[0]);
|
||||
setTitle(titles[0]);
|
||||
|
||||
activeStrobe = setInterval(() => {
|
||||
if(round >= target) {
|
||||
clearTitle();
|
||||
setTitleWrap(getName());
|
||||
setTitle(getName());
|
||||
return;
|
||||
}
|
||||
|
||||
++round;
|
||||
setTitleWrap(titles[round % titles.length]);
|
||||
setTitle(titles[round % titles.length]);
|
||||
}, interval);
|
||||
};
|
||||
|
||||
return {
|
||||
set: setTitleWrap,
|
||||
set: setTitle,
|
||||
clear: clearTitle,
|
||||
strobe: strobeTitle,
|
||||
};
|
||||
|
|
|
@ -1,12 +1,26 @@
|
|||
#include utility.js
|
||||
#include ui/chat-message-list.js
|
||||
|
||||
Umi.UI.ChatInterface = function(chatForm) {
|
||||
const messages = new Umi.UI.ChatMessageList;
|
||||
Umi.UI.ChatInterface = function(messages, chatForm) {
|
||||
const messagesOld = new Umi.UI.ChatMessageList;
|
||||
|
||||
const element = $element('div', { className: 'main' }, messages, chatForm);
|
||||
const html = $e({
|
||||
attrs: {
|
||||
className: 'main',
|
||||
},
|
||||
child: [
|
||||
messages,
|
||||
messagesOld,
|
||||
chatForm,
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
get messageList() { return messages; },
|
||||
get element() { return element; },
|
||||
getMessageList: function() {
|
||||
return messagesOld;
|
||||
},
|
||||
getElement: function() {
|
||||
return html;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,12 +1,27 @@
|
|||
#include utility.js
|
||||
#include ui/chat-interface.js
|
||||
|
||||
// this needs revising at some point but will suffice for now
|
||||
Umi.UI.ChatLayout = function(chatForm, sideBar) {
|
||||
const main = new Umi.UI.ChatInterface(chatForm);
|
||||
const element = $element('div', { id: 'umi-chat', className: 'umi' }, main, sideBar);
|
||||
Umi.UI.ChatLayout = function(messages, chatForm, sideBar) {
|
||||
const main = new Umi.UI.ChatInterface(messages, chatForm);
|
||||
|
||||
const html = $e({
|
||||
attrs: {
|
||||
id: 'umi-chat',
|
||||
className: 'umi',
|
||||
},
|
||||
child: [
|
||||
main,
|
||||
sideBar,
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
get interface() { return main; },
|
||||
get element() { return element; },
|
||||
getInterface: function() {
|
||||
return main;
|
||||
},
|
||||
getElement: function() {
|
||||
return html;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
#include utility.js
|
||||
|
||||
Umi.UI.ChatMessageList = function() {
|
||||
const element = $element('div', { id: 'umi-messages', className: 'chat' });
|
||||
const html = $e({
|
||||
attrs: {
|
||||
id: 'umi-messages',
|
||||
className: 'chat',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
get element() { return element; },
|
||||
getElement: function() {
|
||||
return html;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
#include emotes.js
|
||||
#include utility.js
|
||||
|
||||
Umi.UI.Emoticons = (function() {
|
||||
return {
|
||||
Parse: function(element, author) {
|
||||
let inner = element.innerHTML;
|
||||
|
||||
MamiEmotes.forEach(author?.rank ?? 0, function(emote) {
|
||||
const image = $element('img', { className: 'emoticon', src: emote.url });
|
||||
MamiEmotes.forEach(author?.perms?.rank ?? 0, function(emote) {
|
||||
const image = $e({
|
||||
tag: 'img',
|
||||
attrs: {
|
||||
className: 'emoticon',
|
||||
src: emote.url,
|
||||
},
|
||||
});
|
||||
|
||||
for (const i in emote.strings) {
|
||||
const trigger = ':' + emote.strings[i] + ':',
|
||||
|
|
|
@ -1,56 +1,64 @@
|
|||
#include animate.js
|
||||
#include controls/throbber.jsx
|
||||
#include utility.js
|
||||
|
||||
Umi.UI.LoadingOverlay = function(icon, message) {
|
||||
let throbber, wrapper, messageElem;
|
||||
const html = <div class="overlay overlay-filter">
|
||||
{wrapper = <div class="overlay-wrapper">
|
||||
<div class="overlay-icon">
|
||||
{throbber = <Throbber paused={true} size={2} inline={true} />}
|
||||
</div>
|
||||
<div class="overlay-message">
|
||||
{messageElem = <div class="overlay-message-text" />}
|
||||
</div>
|
||||
</div>}
|
||||
Umi.UI.LoadingOverlay = function(icon, header, message) {
|
||||
const icons = {
|
||||
'spinner': 'fas fa-3x fa-fw fa-spinner fa-pulse',
|
||||
'checkmark': 'fas fa-3x fa-fw fa-check-circle',
|
||||
'cross': 'fas fa-3x fa-fw fa-times-circle',
|
||||
'hammer': 'fas fa-3x fa-fw fa-gavel',
|
||||
'bomb': 'fas fa-3x fa-fw fa-bomb',
|
||||
'unlink': 'fas fa-3x fa-fw fa-unlink',
|
||||
'reload': 'fas fa-3x fa-fw fa-sync fa-spin',
|
||||
'warning': 'fas fa-exclamation-triangle fa-3x',
|
||||
'question': 'fas fa-3x fa-question-circle',
|
||||
'poop': 'fas fa-3x fa-poop',
|
||||
};
|
||||
|
||||
let iconElem, headerElem, messageElem;
|
||||
const html = <div class="overlay">
|
||||
<div class="overlay__inner">
|
||||
{iconElem = <div class="fas fa-3x fa-question-circle" />}
|
||||
{headerElem = <div class="overlay__message" />}
|
||||
{messageElem = <div class="overlay__status" />}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const setIcon = name => {
|
||||
if(name === 'spinner') {
|
||||
if(!throbber.icon.playing)
|
||||
throbber.icon.restart();
|
||||
} else
|
||||
throbber.icon.stop().then(() => { throbber.icon.batsu(); });
|
||||
name = (name || '').toString();
|
||||
if(!(name in icons))
|
||||
name = 'question';
|
||||
iconElem.className = icons[name];
|
||||
};
|
||||
|
||||
const setHeader = text => headerElem.textContent = (text || '').toString();
|
||||
const setMessage = text => messageElem.innerHTML = (text || '').toString();
|
||||
|
||||
setIcon(icon);
|
||||
setHeader(header);
|
||||
setMessage(message);
|
||||
|
||||
return {
|
||||
get icon() { return throbber.icon.playing ? 'spinner' : 'cross'; },
|
||||
get icon() { return iconElem.className; },
|
||||
set icon(value) { setIcon(value); },
|
||||
|
||||
get header() { return headerElem.textContent; },
|
||||
set header(value) { setHeader(value); },
|
||||
get message() { return messageElem.innerHTML; },
|
||||
set message(value) { setMessage(value); },
|
||||
|
||||
get element() { return html; },
|
||||
|
||||
getViewTransition(mode) {
|
||||
getViewTransition: mode => {
|
||||
if(mode === 'pop')
|
||||
return async () => {
|
||||
html.classList.remove('overlay-filter');
|
||||
html.style.pointerEvents = 'none';
|
||||
|
||||
await MamiAnimate({
|
||||
duration: 200,
|
||||
easing: 'inQuint',
|
||||
update(t) {
|
||||
wrapper.style.bottom = `${0 - (104 * t)}px`;
|
||||
wrapper.style.opacity = 1 - (1 * t).toString();
|
||||
},
|
||||
});
|
||||
};
|
||||
return ctx => MamiAnimate({
|
||||
async: true,
|
||||
duration: 200,
|
||||
easing: 'inOutSine',
|
||||
start: () => {
|
||||
ctx.fromElem.style.pointerEvents = 'none';
|
||||
},
|
||||
update: t => {
|
||||
ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`;
|
||||
ctx.fromElem.style.opacity = 1 - (1 * t).toString();
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
#include avatar.js
|
||||
#include common.js
|
||||
#include parsing.js
|
||||
#include title.js
|
||||
#include txtrigs.js
|
||||
#include users.js
|
||||
#include weeb.js
|
||||
#include utility.js
|
||||
#include sound/umisound.js
|
||||
#include ui/emotes.js
|
||||
|
||||
Umi.UI.Messages = (function() {
|
||||
let focusChannelName = '';
|
||||
|
||||
const title = new MamiWindowTitle({
|
||||
getName: () => window.TITLE,
|
||||
getName: () => futami.get('title'),
|
||||
});
|
||||
|
||||
window.addEventListener('focus', () => title.clear());
|
||||
|
@ -27,113 +25,35 @@ Umi.UI.Messages = (function() {
|
|||
};
|
||||
|
||||
const botMsgs = {
|
||||
'say': { text: '%0' },
|
||||
'join': { text: '%0 has joined.', action: 'has joined', sound: 'join' },
|
||||
'leave': { text: '%0 has disconnected.', action: 'has disconnected', avatar: 'greyscale', sound: 'leave' },
|
||||
'jchan': { text: '%0 has joined the channel.', action: 'has joined the channel', sound: 'join' },
|
||||
'lchan': { text: '%0 has left the channel.', action: 'has left the channel', avatar: 'greyscale', sound: 'leave' },
|
||||
'kick': { text: '%0 got bludgeoned to death.', action: 'got bludgeoned to death', avatar: 'invert', sound: 'kick' },
|
||||
'flood': { text: '%0 got kicked for flood protection.', action: 'got kicked for flood protection', avatar: 'invert', sound: 'flood' },
|
||||
'timeout': { text: '%0 exploded.', action: 'exploded', avatar: 'greyscale', sound: 'timeout' },
|
||||
'nick': { text: '%0 changed their name to %1.', action: 'changed their name to %1' },
|
||||
'ipaddr': { text: 'IP address of %0 is %1.' },
|
||||
'banlist': {
|
||||
text: 'Banned: %0',
|
||||
filter: args => {
|
||||
const bans = args[0].split(', ');
|
||||
for(const i in bans)
|
||||
bans[i] = bans[i].slice(92, -4);
|
||||
|
||||
args[0] = bans.join(', ');
|
||||
return args;
|
||||
},
|
||||
},
|
||||
'who': {
|
||||
text: 'Online: %0',
|
||||
filter: args => {
|
||||
const users = args[0].split(', ');
|
||||
for(const i in users) {
|
||||
const isSelf = users[i].includes(' style="font-weight: bold;"');
|
||||
users[i] = users[i].slice(isSelf ? 102 : 75, -4);
|
||||
if(isSelf) users[i] += ' (You)';
|
||||
}
|
||||
|
||||
args[0] = users.join(', ');
|
||||
return args;
|
||||
},
|
||||
},
|
||||
'whochan': {
|
||||
text: 'Online in %0: %1',
|
||||
filter: args => {
|
||||
const users = args[1].split(', ');
|
||||
for(const i in users) {
|
||||
const isSelf = users[i].includes(' style="font-weight: bold;"');
|
||||
users[i] = users[i].slice(isSelf ? 102 : 75, -4);
|
||||
if(isSelf) users[i] += ' (You)';
|
||||
}
|
||||
|
||||
args[1] = users.join(', ');
|
||||
return args;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const formatTemplate = (template, args) => {
|
||||
if(typeof template !== 'string')
|
||||
template = '';
|
||||
|
||||
if(Array.isArray(args))
|
||||
for(let i = 0; i < args.length; ++i) {
|
||||
const arg = args[i] === undefined || args[i] === null ? '' : args[i].toString();
|
||||
template = template.replace(new RegExp(`%${i}`, 'g'), arg);
|
||||
}
|
||||
|
||||
return template;
|
||||
'join': { sound: 'join' },
|
||||
'leave': { sound: 'leave' },
|
||||
'jchan': { sound: 'join' },
|
||||
'lchan': { sound: 'leave' },
|
||||
'kick': { sound: 'kick' },
|
||||
'flood': { sound: 'flood' },
|
||||
'timeout': { sound: 'timeout' },
|
||||
};
|
||||
|
||||
return {
|
||||
Add: function(msg) {
|
||||
Add: async function(msg) {
|
||||
const elementId = `message-${msg.id}`;
|
||||
|
||||
if(msg.id !== '' && $id(elementId))
|
||||
if(msg.id !== '' && $i(elementId))
|
||||
return;
|
||||
|
||||
let isTiny = false;
|
||||
let skipTextParsing = false;
|
||||
let msgText = '';
|
||||
let msgTextLong = '';
|
||||
let msgAuthor = msg.author;
|
||||
|
||||
let soundIsLegacy = true;
|
||||
let soundName;
|
||||
let soundVolume;
|
||||
let soundRate;
|
||||
|
||||
const eText = <div/>;
|
||||
const eAvatar = <div class="message__avatar"/>;
|
||||
const eUser = <div class="message__user"/>;
|
||||
const eMeta = <div class="message__meta">{eUser}</div>;
|
||||
const eTime = <div class="message__time">
|
||||
{msg.created.getHours().toString().padStart(2, '0')}
|
||||
:{msg.created.getMinutes().toString().padStart(2, '0')}
|
||||
:{msg.created.getSeconds().toString().padStart(2, '0')}
|
||||
</div>;
|
||||
const eContainer = <div class="message__container">{eMeta}</div>;
|
||||
const eBase = <div class="message">
|
||||
{eAvatar}
|
||||
{eContainer}
|
||||
</div>;
|
||||
const eBase = <div class="message">{msg.id ?? 'no id'}</div>;
|
||||
|
||||
if(msg.type.startsWith('message:')) {
|
||||
msgText = msgTextLong = msg.detail.body;
|
||||
if(msg.type.startsWith('msg:')) {
|
||||
soundName = msg.author?.self === true ? 'outgoing' : 'incoming';
|
||||
|
||||
if(msg.type === 'message:action')
|
||||
isTiny = true;
|
||||
|
||||
if(mami.settings.get('playJokeSounds'))
|
||||
try {
|
||||
const trigger = mami.textTriggers.getTrigger(msgText);
|
||||
const trigger = mami.textTriggers.getTrigger(msg.text);
|
||||
if(trigger.isSoundType) {
|
||||
soundIsLegacy = false;
|
||||
soundName = trigger.getRandomSoundName();
|
||||
|
@ -144,182 +64,45 @@ Umi.UI.Messages = (function() {
|
|||
} else {
|
||||
let bIsError = false;
|
||||
let bType;
|
||||
let bArgs;
|
||||
|
||||
if(msg.type === 'user:join') {
|
||||
if(msg.type === 'user:add') {
|
||||
bType = 'join';
|
||||
bArgs = [msgAuthor.name];
|
||||
} else if(msg.type === 'user:leave') {
|
||||
} else if(msg.type === 'user:remove') {
|
||||
bType = msg.detail.reason;
|
||||
bArgs = [msgAuthor.name];
|
||||
} else if(msg.type === 'channel:join') {
|
||||
} else if(msg.type === 'chan:join') {
|
||||
bType = 'jchan';
|
||||
bArgs = [msgAuthor.name];
|
||||
} else if(msg.type === 'channel:leave') {
|
||||
} else if(msg.type === 'chan:leave') {
|
||||
bType = 'lchan';
|
||||
bArgs = [msgAuthor.name];
|
||||
} else if(msg.type.startsWith('legacy:')) {
|
||||
bType = msg.type.substring(7);
|
||||
bIsError = msg.detail.error;
|
||||
bArgs = msg.detail.args;
|
||||
bType = msg.botInfo.type;
|
||||
bIsError = msg.botInfo.isError;
|
||||
}
|
||||
|
||||
soundName = bIsError ? 'error' : 'server';
|
||||
|
||||
if(botMsgs.hasOwnProperty(bType)) {
|
||||
const bmInfo = botMsgs[bType];
|
||||
|
||||
if(typeof bmInfo.filter === 'function')
|
||||
bArgs = bmInfo.filter(bArgs);
|
||||
|
||||
if(typeof bmInfo.sound === 'string')
|
||||
soundName = bmInfo.sound;
|
||||
|
||||
let actionSuccess = false;
|
||||
if(typeof bmInfo.action === 'string')
|
||||
if(msgAuthor) {
|
||||
actionSuccess = true;
|
||||
isTiny = true;
|
||||
skipTextParsing = true;
|
||||
|
||||
msgText = formatTemplate(bmInfo.action, bArgs);
|
||||
if(typeof bmInfo.avatar === 'string')
|
||||
eAvatar.classList.add(`avatar-filter-${bmInfo.avatar}`);
|
||||
}
|
||||
|
||||
msgTextLong = formatTemplate(bmInfo.text, bArgs);
|
||||
|
||||
if(!actionSuccess)
|
||||
msgText = msgTextLong;
|
||||
} else
|
||||
msgText = msgTextLong = `!!! Received unsupported message type: ${msg.type} !!!`;
|
||||
}
|
||||
}
|
||||
|
||||
if(msgAuthor !== null) {
|
||||
eUser.style.color = msgAuthor.colour;
|
||||
eUser.textContent = msgAuthor.name;
|
||||
}
|
||||
|
||||
if(isTiny) {
|
||||
eText.classList.add('message-tiny-text');
|
||||
eBase.classList.add('message-tiny');
|
||||
|
||||
if(msgText.indexOf("'") !== 0 || (msgText.match(/\'/g).length % 2) === 0)
|
||||
msgText = "\xA0" + msgText;
|
||||
|
||||
eMeta.append(eText, eTime);
|
||||
} else {
|
||||
eText.classList.add('message__text');
|
||||
eMeta.append(eTime);
|
||||
eContainer.append(eText);
|
||||
}
|
||||
|
||||
eBase.classList.add(`message--user-${msgAuthor?.id ?? '-1'}`);
|
||||
|
||||
if(focusChannelName !== '' && msg.channel !== '' && msg.channel !== focusChannelName)
|
||||
if(focusChannelName !== '' && msg.channel && msg.channel !== focusChannelName)
|
||||
eBase.classList.add('hidden');
|
||||
|
||||
if(msg.id !== '') {
|
||||
if(msg.id) {
|
||||
eBase.id = elementId;
|
||||
eBase.dataset.id = msg.id;
|
||||
}
|
||||
if(msgAuthor)
|
||||
eBase.dataset.author = msgAuthor.id;
|
||||
if(msg.channel !== '')
|
||||
if(msg.sender)
|
||||
eBase.dataset.author = msg.sender.id;
|
||||
if(msg.channel)
|
||||
eBase.dataset.channel = msg.channel;
|
||||
if(isTiny)
|
||||
eBase.dataset.tiny = '1';
|
||||
eBase.dataset.created = msg.created.toISOString();
|
||||
eBase.dataset.created = msg.time.toISOString();
|
||||
|
||||
eBase.dataset.body = msgText;
|
||||
eText.innerText = msgText;
|
||||
eBase.dataset.body = msg.text;
|
||||
|
||||
if(!skipTextParsing) {
|
||||
Umi.UI.Emoticons.Parse(eText, msgAuthor);
|
||||
Umi.Parsing.Parse(eText, msg);
|
||||
|
||||
const linkify = element => {
|
||||
let childNode = element.firstChild;
|
||||
while(childNode instanceof Node)
|
||||
try {
|
||||
if(childNode instanceof HTMLAnchorElement)
|
||||
continue;
|
||||
|
||||
if(childNode instanceof HTMLElement) {
|
||||
linkify(childNode);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!(childNode instanceof Text))
|
||||
continue;
|
||||
|
||||
let offset = 0;
|
||||
const parts = childNode.data.split(/([\p{White_Space}{}\[\]<>'"]+)/u);
|
||||
for(let part of parts) {
|
||||
while(part.startsWith('(') && part.endsWith(')')) {
|
||||
offset += 1;
|
||||
part = part.substring(1, part.length - 1);
|
||||
}
|
||||
|
||||
const noProto = part.startsWith('//');
|
||||
if(!noProto && !part.includes('://')) {
|
||||
offset += part.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
let url;
|
||||
try {
|
||||
url = new URL(noProto ? (location.protocol + part) : part);
|
||||
} catch(ex) {
|
||||
offset += part.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(offset > 0) {
|
||||
childNode = childNode.splitText(offset);
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
const length = part.length;
|
||||
while(!part.endsWith('/') && /\p{Po}$/u.test(part))
|
||||
part = part.substring(0, part.length - 1);
|
||||
|
||||
if(part.length < length) {
|
||||
url = new URL(noProto ? (location.protocol + part) : part);
|
||||
offset += length - part.length;
|
||||
}
|
||||
|
||||
const replaceNode = childNode;
|
||||
childNode = replaceNode.splitText(part.length);
|
||||
|
||||
replaceNode.replaceWith(<a class="markup__link" href={url.toString()} target="_blank" rel="nofollow noreferrer noopener">
|
||||
{decodeURIComponent(part)}
|
||||
</a>);
|
||||
}
|
||||
} finally {
|
||||
childNode = childNode.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
linkify(eText);
|
||||
|
||||
if(mami.settings.get('weeaboo')) {
|
||||
eUser.appendChild($text(Weeaboo.getNameSuffix(msgAuthor)));
|
||||
eText.appendChild($text(Weeaboo.getTextSuffix(msgAuthor)));
|
||||
|
||||
const kaomoji = Weeaboo.getRandomKaomoji(true, msg);
|
||||
if(kaomoji)
|
||||
eText.append(` ${kaomoji}`);
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = msgAuthor?.avatar?.[isTiny ? 'x40' : 'x80'] ?? '';
|
||||
if(avatarUrl.length > 0)
|
||||
eAvatar.style.backgroundImage = `url('${avatarUrl}')`;
|
||||
else
|
||||
eAvatar.classList.add('message__avatar--disabled');
|
||||
|
||||
const msgsList = $id('umi-messages');
|
||||
const msgsList = $i('umi-messages');
|
||||
|
||||
let insertAfter = msgsList.lastElementChild;
|
||||
if(insertAfter instanceof Element) {
|
||||
|
@ -332,38 +115,22 @@ Umi.UI.Messages = (function() {
|
|||
|
||||
eBase.classList.toggle('message--first', shouldDisplayAuthorInfo(eBase, insertAfter));
|
||||
|
||||
if(eBase.dataset.tiny !== insertAfter.dataset.tiny)
|
||||
eBase.classList.add(isTiny ? 'message-tiny-fix' : 'message-big-fix');
|
||||
|
||||
insertAfter.after(eBase);
|
||||
|
||||
if(eBase.nextElementSibling instanceof Element)
|
||||
eBase.nextElementSibling.classList.toggle('message--first', shouldDisplayAuthorInfo(eBase.nextElementSibling, eBase));
|
||||
} else {
|
||||
eBase.classList.add('message--first');
|
||||
if(isTiny) eBase.classList.add('message-tiny-fix');
|
||||
msgsList.append(eBase);
|
||||
}
|
||||
|
||||
if(!eBase.classList.contains('hidden')) {
|
||||
if(mami.settings.get('autoEmbedV1')) {
|
||||
const callEmbedOn = eBase.querySelectorAll('a[onclick^="Umi.Parser.SockChatBBcode.Embed"]');
|
||||
for(const embedElem of callEmbedOn)
|
||||
if(embedElem.dataset.embed !== '1')
|
||||
embedElem.click();
|
||||
}
|
||||
|
||||
if(mami.settings.get('autoScroll'))
|
||||
eBase.scrollIntoView({ inline: 'end' });
|
||||
}
|
||||
|
||||
let isMentioned = false;
|
||||
const mentionTriggers = mami.settings.get('notificationTriggers').toLowerCase().split(' ');
|
||||
const currentUser = Umi.User.getCurrentUser();
|
||||
const currentUser = await Umi.Server.getCurrentUserInfo();
|
||||
if(typeof currentUser === 'object' && typeof currentUser.name === 'string')
|
||||
mentionTriggers.push(currentUser.name.toLowerCase());
|
||||
|
||||
const mentionText = ` ${msgTextLong} `.toLowerCase();
|
||||
const mentionText = ` ${msg.text} `.toLowerCase();
|
||||
for(const trigger of mentionTriggers) {
|
||||
if(trigger.trim() === '')
|
||||
continue;
|
||||
|
@ -379,7 +146,7 @@ Umi.UI.Messages = (function() {
|
|||
|
||||
if(document.hidden) {
|
||||
if(mami.settings.get('flashTitle')) {
|
||||
let titleText = msgAuthor?.name ?? msgTextLong;
|
||||
let titleText = msg.sender?.name ?? msg.text;
|
||||
if(focusChannelName !== '' && focusChannelName !== msg.channel)
|
||||
titleText += ` @ ${msg.channel}`;
|
||||
|
||||
|
@ -393,14 +160,12 @@ Umi.UI.Messages = (function() {
|
|||
if(mami.settings.get('enableNotifications') && isMentioned) {
|
||||
const options = {};
|
||||
|
||||
options.icon = MamiFormatUserAvatarUrl(msg.sender.id, 100);
|
||||
options.body = 'Click here to see what they said.';
|
||||
if(mami.settings.get('notificationShowMessage'))
|
||||
options.body += "\n" + msgTextLong;
|
||||
options.body += `\n${msg.text}`;
|
||||
|
||||
if(avatarUrl.length > 0)
|
||||
options.icon = avatarUrl;
|
||||
|
||||
const notif = new Notification(`${msgAuthor.name} mentioned you!`, options);
|
||||
const notif = new Notification(`${msg.sender.name} mentioned you!`, options);
|
||||
notif.addEventListener('click', () => {
|
||||
window.focus();
|
||||
});
|
||||
|
@ -418,15 +183,9 @@ Umi.UI.Messages = (function() {
|
|||
|
||||
mami.sound.library.play(soundName, soundVolume, soundRate);
|
||||
}
|
||||
|
||||
mami.globalEvents.dispatch('umi:ui:message_add', { element: eBase });
|
||||
},
|
||||
IsScrolledToBottom: () => {
|
||||
const msgsList = $id('umi-messages');
|
||||
return msgsList.scrollTop === (msgsList.scrollHeight - msgsList.offsetHeight);
|
||||
},
|
||||
ScrollIfNeeded: (offsetOrForce = 0) => {
|
||||
const msgsList = $id('umi-messages');
|
||||
const msgsList = $i('umi-messages');
|
||||
if(!(msgsList instanceof Element))
|
||||
return;
|
||||
|
||||
|
@ -447,7 +206,7 @@ Umi.UI.Messages = (function() {
|
|||
|
||||
focusChannelName = channel;
|
||||
|
||||
const root = $id('umi-messages');
|
||||
const root = $i('umi-messages');
|
||||
for(const elem of root.children)
|
||||
elem.classList.toggle('hidden', elem.dataset.channel !== undefined && elem.dataset.channel !== focusChannelName);
|
||||
|
||||
|
@ -459,7 +218,7 @@ Umi.UI.Messages = (function() {
|
|||
if(typeof retain !== 'number')
|
||||
return;
|
||||
|
||||
const root = $id('umi-messages');
|
||||
const root = $i('umi-messages');
|
||||
|
||||
// remove messages
|
||||
if(root.childElementCount > retain)
|
||||
|
@ -469,7 +228,7 @@ Umi.UI.Messages = (function() {
|
|||
if(!elem.dataset.channel || elem.classList.contains('hidden') || --retain > 0)
|
||||
continue;
|
||||
|
||||
elem.remove();
|
||||
$r(elem);
|
||||
}
|
||||
|
||||
// fix author display
|
||||
|
@ -491,7 +250,7 @@ Umi.UI.Messages = (function() {
|
|||
if(msgId === '')
|
||||
return;
|
||||
|
||||
const elem = $id(`message-${msgId}`);
|
||||
const elem = $i(`message-${msgId}`);
|
||||
if(!(elem instanceof Element))
|
||||
return;
|
||||
|
||||
|
@ -499,7 +258,7 @@ Umi.UI.Messages = (function() {
|
|||
if(elem.nextElementSibling && elem.nextElementSibling.dataset.author === elem.dataset.author)
|
||||
elem.nextElementSibling.classList.add('message--first');
|
||||
|
||||
elem.remove();
|
||||
$r(elem);
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
38
src/mami.js/uniqstr.js
Normal file
38
src/mami.js/uniqstr.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
const MamiRandomInt = (min, max) => {
|
||||
let ret = 0;
|
||||
const range = max - min;
|
||||
|
||||
const bitsNeeded = Math.ceil(Math.log2(range));
|
||||
if(bitsNeeded > 53)
|
||||
return -1;
|
||||
|
||||
const bytesNeeded = Math.ceil(bitsNeeded / 8),
|
||||
mask = Math.pow(2, bitsNeeded) - 1;
|
||||
|
||||
const bytes = new Uint8Array(bytesNeeded);
|
||||
crypto.getRandomValues(bytes);
|
||||
|
||||
let p = (bytesNeeded - 1) * 8;
|
||||
for(let i = 0; i < bytesNeeded; ++i) {
|
||||
ret += bytes[i] * Math.pow(2, p);
|
||||
p -= 8;
|
||||
}
|
||||
|
||||
ret &= mask;
|
||||
|
||||
if(ret >= range)
|
||||
return MamiRandomInt(min, max);
|
||||
|
||||
return min + ret;
|
||||
};
|
||||
|
||||
const MamiUniqueStr = (() => {
|
||||
const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789';
|
||||
|
||||
return length => {
|
||||
let str = '';
|
||||
for(let i = 0; i < length; ++i)
|
||||
str += chars[MamiRandomInt(0, chars.length)];
|
||||
return str;
|
||||
};
|
||||
})();
|
22
src/mami.js/url.js
Normal file
22
src/mami.js/url.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
Umi.URI = (function() {
|
||||
const regex = new RegExp("([A-Za-z][A-Za-z0-9+\\-.]*):(?:(//)(?:((?:[A-Za-z0-9\\-._~!$&'()*+,;=:]|%[0-9A-Fa-f]{2})*)@)?((?:\\[(?:(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|::(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)|[Vv][0-9A-Fa-f]+\\.[A-Za-z0-9\\-._~!$&'()*+,;=:]+)\\]|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[A-Za-z0-9\\-._~!$&'()*+,;=]|%[0-9A-Fa-f]{2})*))(?::([0-9]*))?((?:/(?:[A-Za-z0-9\\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)|/((?:(?:[A-Za-z0-9\\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:/(?:[A-Za-z0-9\\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)?)|((?:[A-Za-z0-9\\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:/(?:[A-Za-z0-9\\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)|)(?:\\?((?:[A-Za-z0-9\\-._~!$&'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*))?(?:\\#((?:[A-Za-z0-9\\-._~!$&'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*))?");
|
||||
|
||||
return {
|
||||
Parse: function(url) {
|
||||
const match = url.match(regex);
|
||||
if(match === null)
|
||||
return null;
|
||||
|
||||
return {
|
||||
Protocol: match[1] || null,
|
||||
Slashes: match[2] || null,
|
||||
Authority: match[3] || null,
|
||||
Host: match[4] || null,
|
||||
Port: match[5] || null,
|
||||
Path: match[6] || match[7] || match[8] || null,
|
||||
Query: match[9] || null,
|
||||
Hash: match[10] || null,
|
||||
};
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -1,182 +0,0 @@
|
|||
const MamiUserPermsInfo = function(rank = 0, canKick = false, canSetNick = false, canCreateChannels = false) {
|
||||
if(typeof rank !== 'number')
|
||||
throw 'rank must be a number';
|
||||
if(typeof canKick !== 'boolean')
|
||||
throw 'canKick must be a boolean';
|
||||
if(typeof canSetNick !== 'boolean')
|
||||
throw 'canSetNick must be a boolean';
|
||||
if(typeof canCreateChannels !== 'boolean')
|
||||
throw 'canCreateChannels must be a boolean';
|
||||
|
||||
return {
|
||||
get rank() { return rank; },
|
||||
get canKick() { return canKick; },
|
||||
get canSetNick() { return canSetNick; },
|
||||
get canCreateChannels() { return canCreateChannels; },
|
||||
};
|
||||
};
|
||||
|
||||
const MamiUserStatusInfo = function(isAway = false, message = '') {
|
||||
if(typeof isAway !== 'boolean')
|
||||
throw 'isAway must be a boolean';
|
||||
if(typeof message !== 'string')
|
||||
throw 'message must be a string';
|
||||
|
||||
return {
|
||||
get isAway() { return isAway; },
|
||||
get message() { return message; },
|
||||
};
|
||||
};
|
||||
|
||||
const MamiUserAvatarInfo = function(userId = null) {
|
||||
userId ??= '';
|
||||
if(typeof userId !== 'string')
|
||||
throw 'userId must be a string or null';
|
||||
|
||||
const changeTime = Date.now();
|
||||
|
||||
const getAvatar = res => {
|
||||
return !res || res === '0'
|
||||
? `${window.FII_URL}/assets/avatar/${encodeURIComponent(userId)}?ver=${encodeURIComponent(changeTime)}`
|
||||
: `${window.FII_URL}/assets/avatar/${encodeURIComponent(userId)}?res=${encodeURIComponent(res)}&ver=${encodeURIComponent(changeTime)}`;
|
||||
};
|
||||
|
||||
return {
|
||||
get original() { return getAvatar('0'); },
|
||||
get x80() { return getAvatar('80'); },
|
||||
get x60() { return getAvatar('60'); },
|
||||
get x40() { return getAvatar('40'); },
|
||||
};
|
||||
};
|
||||
|
||||
const MamiUserInfo = function(id, name, colour = 'inherit', status = null, perms = null, avatar = null) {
|
||||
if(typeof id !== 'string')
|
||||
throw 'id must be a string';
|
||||
if(typeof name !== 'string')
|
||||
throw 'name must be a string';
|
||||
if(typeof colour !== 'string') // should be like, object or something maybe
|
||||
throw 'colour must be a string';
|
||||
if(status === null)
|
||||
status = new MamiUserStatusInfo;
|
||||
else if(typeof status !== 'object')
|
||||
throw 'status must be an object';
|
||||
if(perms === null)
|
||||
perms = new MamiUserPermsInfo;
|
||||
else if(typeof perms !== 'object')
|
||||
throw 'perms must be an object';
|
||||
if(avatar === null)
|
||||
avatar = new MamiUserAvatarInfo(id);
|
||||
else if(typeof avatar !== 'object')
|
||||
throw 'avatar must be an object';
|
||||
|
||||
return {
|
||||
get id() { return id; },
|
||||
|
||||
get name() { return name; },
|
||||
set name(value) {
|
||||
if(typeof value !== 'string')
|
||||
throw 'value must be a string';
|
||||
name = value;
|
||||
},
|
||||
|
||||
get colour() { return colour; },
|
||||
set colour(value) {
|
||||
if(typeof value !== 'string') // ^
|
||||
throw 'value must be a string';
|
||||
colour = value;
|
||||
},
|
||||
|
||||
get status() { return status; },
|
||||
set status(value) {
|
||||
if(typeof value !== 'object' || value === null)
|
||||
throw 'value must be an object';
|
||||
status = value;
|
||||
},
|
||||
|
||||
get perms() { return perms; },
|
||||
set perms(value) {
|
||||
if(typeof value !== 'object' || value === null)
|
||||
throw 'value must be an object';
|
||||
perms = value;
|
||||
},
|
||||
|
||||
get avatar() { return avatar; },
|
||||
set avatar(value) {
|
||||
if(typeof value !== 'object' || value === null)
|
||||
throw 'value must be an object';
|
||||
avatar = value;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Umi.User = (() => {
|
||||
let userInfo;
|
||||
|
||||
return {
|
||||
hasCurrentUser: () => userInfo !== undefined,
|
||||
getCurrentUser: () => userInfo,
|
||||
setCurrentUser: value => { userInfo = value; },
|
||||
isCurrentUser: otherInfo => otherInfo !== null && typeof otherInfo === 'object' && typeof otherInfo.id === 'string'
|
||||
&& userInfo !== null && typeof userInfo === 'object'
|
||||
&& typeof userInfo.id === 'string'
|
||||
&& (userInfo === otherInfo || userInfo.id === otherInfo.id),
|
||||
};
|
||||
})();
|
||||
|
||||
Umi.Users = (function() {
|
||||
const users = new Map;
|
||||
|
||||
return {
|
||||
Add: function(user) {
|
||||
const userId = user.id;
|
||||
if(!users.has(userId)) {
|
||||
users.set(userId, user);
|
||||
}
|
||||
},
|
||||
Remove: function(user) {
|
||||
const userId = user.id;
|
||||
if(users.has(userId)) {
|
||||
users.delete(userId);
|
||||
}
|
||||
},
|
||||
Clear: function() {
|
||||
users.clear();
|
||||
},
|
||||
All: function() {
|
||||
return Array.from(users.values());
|
||||
},
|
||||
Get: function(userId) {
|
||||
userId = userId.toString();
|
||||
if(users.has(userId))
|
||||
return users.get(userId);
|
||||
return null;
|
||||
},
|
||||
Find: function(userName) {
|
||||
const found = [];
|
||||
userName = userName.toLowerCase();
|
||||
|
||||
users.forEach(function(user) {
|
||||
if(user.name.toLowerCase().includes(userName))
|
||||
found.push(user);
|
||||
});
|
||||
|
||||
return found;
|
||||
},
|
||||
FindExact: function(userName) {
|
||||
if(typeof userName !== 'string')
|
||||
return null;
|
||||
|
||||
userName = userName.toLowerCase();
|
||||
|
||||
for(const user of users.values())
|
||||
if(user.name.toLowerCase() === userName)
|
||||
return user;
|
||||
|
||||
return null;
|
||||
},
|
||||
Update: function(userId, user) {
|
||||
userId = userId.toString();
|
||||
users.set(userId, user);
|
||||
},
|
||||
};
|
||||
})();
|
285
src/mami.js/utility.js
Normal file
285
src/mami.js/utility.js
Normal file
|
@ -0,0 +1,285 @@
|
|||
const $i = document.getElementById.bind(document);
|
||||
const $c = document.getElementsByClassName.bind(document);
|
||||
const $q = document.querySelector.bind(document);
|
||||
const $qa = document.querySelectorAll.bind(document);
|
||||
const $t = document.createTextNode.bind(document);
|
||||
|
||||
const $r = function(element) {
|
||||
if(element && element.parentNode)
|
||||
element.parentNode.removeChild(element);
|
||||
};
|
||||
|
||||
const $ri = function(name) {
|
||||
$r($i(name));
|
||||
};
|
||||
|
||||
const $rq = function(query) {
|
||||
$r($q(query));
|
||||
};
|
||||
|
||||
const $ib = function(ref, elem) {
|
||||
ref.parentNode.insertBefore(elem, ref);
|
||||
};
|
||||
|
||||
const $rc = function(element) {
|
||||
while(element.lastChild)
|
||||
element.removeChild(element.lastChild);
|
||||
};
|
||||
|
||||
const $e = function(info, attrs, child, created) {
|
||||
info = info || {};
|
||||
|
||||
if(typeof info === 'string') {
|
||||
info = {tag: info};
|
||||
if(attrs)
|
||||
info.attrs = attrs;
|
||||
if(child)
|
||||
info.child = child;
|
||||
if(created)
|
||||
info.created = created;
|
||||
}
|
||||
|
||||
const elem = document.createElement(info.tag || 'div');
|
||||
|
||||
if(info.attrs) {
|
||||
const attrs = info.attrs;
|
||||
|
||||
for(let key in attrs) {
|
||||
const attr = attrs[key];
|
||||
if(attr === undefined || attr === null)
|
||||
continue;
|
||||
|
||||
switch(typeof attr) {
|
||||
case 'function':
|
||||
if(key.substring(0, 2) === 'on')
|
||||
key = key.substring(2).toLowerCase();
|
||||
elem.addEventListener(key, attr);
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
if(attr instanceof Array) {
|
||||
if(key === 'class')
|
||||
key = 'classList';
|
||||
|
||||
const prop = elem[key];
|
||||
let addFunc = null;
|
||||
|
||||
if(prop instanceof Array)
|
||||
addFunc = prop.push.bind(prop);
|
||||
else if(prop instanceof DOMTokenList)
|
||||
addFunc = prop.add.bind(prop);
|
||||
|
||||
if(addFunc !== null) {
|
||||
for(let j = 0; j < attr.length; ++j)
|
||||
addFunc(attr[j]);
|
||||
} else {
|
||||
if(key === 'classList')
|
||||
key = 'class';
|
||||
elem.setAttribute(key, attr.toString());
|
||||
}
|
||||
} else {
|
||||
for(const attrKey in attr)
|
||||
elem[key][attrKey] = attr[attrKey];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
if(attr)
|
||||
elem.setAttribute(key, '');
|
||||
break;
|
||||
|
||||
default:
|
||||
if(key === 'className')
|
||||
key = 'class';
|
||||
elem.setAttribute(key, attr.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(info.child) {
|
||||
let children = info.child;
|
||||
|
||||
if(!Array.isArray(children))
|
||||
children = [children];
|
||||
|
||||
for(const child of children) {
|
||||
switch(typeof child) {
|
||||
case 'string':
|
||||
elem.appendChild($t(child));
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
if(child instanceof Element) {
|
||||
elem.appendChild(child);
|
||||
} else if('element' in child) {
|
||||
const childElem = child.element;
|
||||
if(childElem instanceof Element)
|
||||
elem.appendChild(childElem);
|
||||
else
|
||||
elem.appendChild($e(child));
|
||||
} else if('getElement' in child) {
|
||||
const childElem = child.getElement();
|
||||
if(childElem instanceof Element)
|
||||
elem.appendChild(childElem);
|
||||
else
|
||||
elem.appendChild($e(child));
|
||||
} else {
|
||||
elem.appendChild($e(child));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
elem.appendChild($t(child.toString()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(info.created)
|
||||
info.created(elem);
|
||||
|
||||
return elem;
|
||||
};
|
||||
const $er = (type, props, ...children) => $e({ tag: type, attrs: props, child: children });
|
||||
|
||||
const $ar = function(array, index) {
|
||||
array.splice(index, 1);
|
||||
};
|
||||
const $ari = function(array, item) {
|
||||
let index;
|
||||
while(array.length > 0 && (index = array.indexOf(item)) >= 0)
|
||||
$ar(array, index);
|
||||
};
|
||||
const $arf = function(array, predicate) {
|
||||
let index;
|
||||
while(array.length > 0 && (index = array.findIndex(predicate)) >= 0)
|
||||
$ar(array, index);
|
||||
};
|
||||
|
||||
const $as = function(array) {
|
||||
if(array.length < 2)
|
||||
return;
|
||||
|
||||
for(let i = array.length - 1; i > 0; --i) {
|
||||
let j = Math.floor(Math.random() * (i + 1)),
|
||||
tmp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = tmp;
|
||||
}
|
||||
};
|
||||
|
||||
const $x = (function() {
|
||||
const send = function(method, url, options, body) {
|
||||
if(options === undefined)
|
||||
options = {};
|
||||
else if(typeof options !== 'object')
|
||||
throw 'options must be undefined or an object';
|
||||
|
||||
const xhr = new XMLHttpRequest;
|
||||
const requestHeaders = new Map;
|
||||
|
||||
if('headers' in options && typeof options.headers === 'object')
|
||||
for(const name in options.headers)
|
||||
if(options.headers.hasOwnProperty(name))
|
||||
requestHeaders.set(name.toLowerCase(), options.headers[name]);
|
||||
|
||||
if(typeof options.download === 'function') {
|
||||
xhr.onloadstart = ev => options.download(ev);
|
||||
xhr.onprogress = ev => options.download(ev);
|
||||
xhr.onloadend = ev => options.download(ev);
|
||||
}
|
||||
|
||||
if(typeof options.upload === 'function') {
|
||||
xhr.upload.onloadstart = ev => options.upload(ev);
|
||||
xhr.upload.onprogress = ev => options.upload(ev);
|
||||
xhr.upload.onloadend = ev => options.upload(ev);
|
||||
}
|
||||
|
||||
if(options.authed)
|
||||
xhr.withCredentials = true;
|
||||
|
||||
if(typeof options.timeout === 'number')
|
||||
xhr.timeout = options.timeout;
|
||||
|
||||
if(typeof options.type === 'string')
|
||||
xhr.responseType = options.type;
|
||||
|
||||
if(typeof options.abort === 'function')
|
||||
options.abort(() => xhr.abort());
|
||||
|
||||
if(typeof options.xhr === 'function')
|
||||
options.xhr(() => xhr);
|
||||
|
||||
if(typeof body === 'object') {
|
||||
if(body instanceof URLSearchParams) {
|
||||
requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
|
||||
} else if(body instanceof FormData) {
|
||||
// content-type is implicitly set
|
||||
} else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) {
|
||||
if(!requestHeaders.has('content-type'))
|
||||
requestHeaders.set('content-type', 'application/octet-stream');
|
||||
} else if(!requestHeaders.has('content-type')) {
|
||||
const bodyParts = [];
|
||||
for(const name in body)
|
||||
if(body.hasOwnProperty(name))
|
||||
bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name]));
|
||||
body = bodyParts.join('&');
|
||||
requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let responseHeaders = undefined;
|
||||
|
||||
xhr.onload = ev => resolve({
|
||||
status: xhr.status,
|
||||
body: () => xhr.response,
|
||||
text: () => xhr.responseText,
|
||||
headers: () => {
|
||||
if(responseHeaders !== undefined)
|
||||
return responseHeaders;
|
||||
|
||||
responseHeaders = new Map;
|
||||
|
||||
const raw = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
|
||||
for(const name in raw)
|
||||
if(raw.hasOwnProperty(name)) {
|
||||
const parts = raw[name].split(': ');
|
||||
responseHeaders.set(parts.shift(), parts.join(': '));
|
||||
}
|
||||
|
||||
return responseHeaders;
|
||||
},
|
||||
xhr: xhr,
|
||||
ev: ev,
|
||||
});
|
||||
|
||||
xhr.onabort = ev => reject({
|
||||
abort: true,
|
||||
xhr: xhr,
|
||||
ev: ev,
|
||||
});
|
||||
|
||||
xhr.onerror = ev => reject({
|
||||
abort: false,
|
||||
xhr: xhr,
|
||||
ev: ev,
|
||||
});
|
||||
|
||||
xhr.open(method, url);
|
||||
for(const [name, value] of requestHeaders)
|
||||
xhr.setRequestHeader(name, value);
|
||||
xhr.send(body);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
send: send,
|
||||
get: (url, options, body) => send('GET', url, options, body),
|
||||
post: (url, options, body) => send('POST', url, options, body),
|
||||
delete: (url, options, body) => send('DELETE', url, options, body),
|
||||
patch: (url, options, body) => send('PATCH', url, options, body),
|
||||
put: (url, options, body) => send('PUT', url, options, body),
|
||||
};
|
||||
})();
|
|
@ -1,5 +1,6 @@
|
|||
#include common.js
|
||||
#include rng.js
|
||||
#include utility.js
|
||||
|
||||
const Weeaboo = (function() {
|
||||
let kaomoji = [];
|
||||
|
@ -11,12 +12,12 @@ const Weeaboo = (function() {
|
|||
const userSfx = new Map;
|
||||
const pub = {};
|
||||
|
||||
pub.init = function(flashii) {
|
||||
pub.init = function() {
|
||||
if(kaomoji.length > 0)
|
||||
return;
|
||||
|
||||
flashii.v1.kaomoji({ as: 'array' })
|
||||
.then(list => kaomoji = list);
|
||||
$x.get(futami.get('kaomoji'))
|
||||
.then(resp => kaomoji = resp.text().split("\n"));
|
||||
};
|
||||
|
||||
pub.getRandomKaomoji = function(allowEmpty, message) {
|
||||
|
@ -35,12 +36,12 @@ const Weeaboo = (function() {
|
|||
};
|
||||
|
||||
pub.getNameSuffix = function(user) {
|
||||
if(typeof user !== 'object' || user === null)
|
||||
if(typeof user !== 'object' || user === null || user === undefined)
|
||||
return '';
|
||||
|
||||
if(user.rank >= 10)
|
||||
if(user.perms.rank >= 10)
|
||||
return '-sama';
|
||||
if(user.rank >= 5)
|
||||
if(user.perms.rank >= 5)
|
||||
return '-sensei';
|
||||
if(user.colour.toLowerCase() === '#f02d7d')
|
||||
return '-san';
|
||||
|
@ -58,7 +59,7 @@ const Weeaboo = (function() {
|
|||
};
|
||||
|
||||
pub.getTextSuffix = function(user) {
|
||||
if(typeof user !== 'object' || user === null)
|
||||
if(typeof user !== 'object' || user === null || user === undefined)
|
||||
return '';
|
||||
|
||||
const userId = user.id;
|
||||
|
|
281
src/mami.js/worker.js
Normal file
281
src/mami.js/worker.js
Normal file
|
@ -0,0 +1,281 @@
|
|||
#include uniqstr.js
|
||||
|
||||
const MamiWorker = function(url, eventTarget) {
|
||||
const timeOutMs = 30000;
|
||||
|
||||
let worker, workerId;
|
||||
let connectTimeout;
|
||||
let pingId;
|
||||
let hasTimedout;
|
||||
|
||||
const root = {};
|
||||
const objects = new Map;
|
||||
const pending = new Map;
|
||||
const clearObjects = () => {
|
||||
for(const [name, object] of objects)
|
||||
for(const method in object)
|
||||
delete object[method];
|
||||
objects.clear();
|
||||
objects.set('', root);
|
||||
};
|
||||
|
||||
clearObjects();
|
||||
|
||||
const broadcastTimeoutZone = body => {
|
||||
const localWorkerId = workerId;
|
||||
|
||||
body(detail => {
|
||||
if(localWorkerId !== workerId || hasTimedout)
|
||||
return;
|
||||
hasTimedout = true;
|
||||
|
||||
eventTarget.dispatch(':timeout', detail);
|
||||
});
|
||||
};
|
||||
|
||||
const handlers = {};
|
||||
|
||||
const handleMessage = ev => {
|
||||
if(typeof ev.data === 'object' && ev.data !== null && typeof ev.data.type === 'string') {
|
||||
if(ev.data.type in handlers)
|
||||
handlers[ev.data.type](ev.data.detail);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const callObjectMethod = (objName, metName, ...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(typeof objName !== 'string')
|
||||
throw 'objName must be a string';
|
||||
if(typeof metName !== 'string')
|
||||
throw 'metName must be a string';
|
||||
|
||||
const id = MamiUniqueStr(8);
|
||||
const info = { id: id, resolve: resolve, reject: reject };
|
||||
pending.set(id, info);
|
||||
|
||||
worker.postMessage({ type: 'metcall', detail: { id: id, object: objName, method: metName, args: args } });
|
||||
|
||||
broadcastTimeoutZone(timeout => {
|
||||
info.timeOut = setTimeout(() => {
|
||||
const reject = info.reject;
|
||||
info.resolve = info.reject = undefined;
|
||||
|
||||
info.timeOut = undefined;
|
||||
pending.delete(id);
|
||||
|
||||
timeout({ at: 'call', obj: objName, met: metName });
|
||||
|
||||
if(typeof reject === 'function')
|
||||
reject('timeout');
|
||||
}, timeOutMs);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const defineObjectMethod = (object, objName, method) => object[method] = (...args) => callObjectMethod(objName, method, ...args);
|
||||
|
||||
handlers['objdef'] = info => {
|
||||
let object = objects.get(info.object);
|
||||
if(object === undefined)
|
||||
objects.set(info.object, object = {});
|
||||
|
||||
if(typeof info.eventPrefix === 'string') {
|
||||
const scopedTarget = eventTarget.scopeTo(info.eventPrefix);
|
||||
object.watch = scopedTarget.watch;
|
||||
object.unwatch = scopedTarget.unwatch;
|
||||
}
|
||||
|
||||
for(const method of info.methods)
|
||||
defineObjectMethod(object, info.object, method);
|
||||
};
|
||||
|
||||
handlers['objdel'] = info => {
|
||||
// this should never happen
|
||||
if(info.object === '') {
|
||||
console.error('Worker attempted to delete root object!!!!!');
|
||||
return;
|
||||
}
|
||||
|
||||
const object = objects.get(info.object);
|
||||
if(object === undefined)
|
||||
return;
|
||||
|
||||
objects.delete(info.object);
|
||||
|
||||
const methods = Object.keys(object);
|
||||
for(const method of methods)
|
||||
delete object[method];
|
||||
};
|
||||
|
||||
handlers['metdef'] = info => {
|
||||
const object = objects.get(info.object);
|
||||
if(object === undefined) {
|
||||
console.error('Worker attempted to define method on undefined object.');
|
||||
return;
|
||||
}
|
||||
|
||||
defineObjectMethod(object, info.object, info.method);
|
||||
};
|
||||
|
||||
handlers['metdel'] = info => {
|
||||
const object = objects.get(info.object);
|
||||
if(object === undefined) {
|
||||
console.error('Worker attempted to delete method on undefined object.');
|
||||
return;
|
||||
}
|
||||
|
||||
delete object[info.method];
|
||||
};
|
||||
|
||||
handlers['funcret'] = resp => {
|
||||
const info = pending.get(resp.id);
|
||||
if(info === undefined)
|
||||
return;
|
||||
|
||||
pending.delete(info.id);
|
||||
|
||||
if(info.timeOut !== undefined)
|
||||
clearTimeout(info.timeOut);
|
||||
|
||||
const handler = resp.success ? info.resolve : info.reject;
|
||||
info.resolve = info.reject = undefined;
|
||||
|
||||
if(handler !== undefined) {
|
||||
let result = resp.result;
|
||||
if(resp.object)
|
||||
result = objects.get(result);
|
||||
|
||||
handler(result);
|
||||
}
|
||||
};
|
||||
|
||||
handlers['evtdisp'] = resp => {
|
||||
eventTarget.dispatch(resp.name, resp.detail);
|
||||
};
|
||||
|
||||
return {
|
||||
get root() { return root; },
|
||||
|
||||
watch: eventTarget.watch,
|
||||
unwatch: eventTarget.unwatch,
|
||||
eventTarget: prefix => eventTarget.scopeTo(prefix),
|
||||
|
||||
ping: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(worker === undefined)
|
||||
throw 'no worker active';
|
||||
|
||||
let pingTimeout;
|
||||
let localPingId = pingId;
|
||||
|
||||
const pingHandleMessage = ev => {
|
||||
if(typeof ev.data === 'string' && ev.data.startsWith('pong:') && ev.data.substring(5) === localPingId)
|
||||
try {
|
||||
reject = undefined;
|
||||
pingId = undefined;
|
||||
|
||||
if(pingTimeout !== undefined)
|
||||
clearTimeout(pingTimeout);
|
||||
|
||||
worker?.removeEventListener('message', pingHandleMessage);
|
||||
|
||||
if(typeof resolve === 'function')
|
||||
resolve();
|
||||
} finally {
|
||||
resolve = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
worker.addEventListener('message', pingHandleMessage);
|
||||
|
||||
if(localPingId === undefined) {
|
||||
pingId = localPingId = MamiUniqueStr(8);
|
||||
|
||||
broadcastTimeoutZone(timeout => {
|
||||
pingTimeout = setTimeout(() => {
|
||||
try {
|
||||
resolve = undefined;
|
||||
|
||||
worker?.removeEventListener('message', pingHandleMessage);
|
||||
|
||||
timeout({ at: 'ping' });
|
||||
if(typeof reject === 'function')
|
||||
reject('ping timeout');
|
||||
} finally {
|
||||
reject = undefined;
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
worker.postMessage(`ping:${localPingId}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
sabotage: () => {
|
||||
worker?.terminate();
|
||||
},
|
||||
|
||||
connect: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const connectFinally = () => {
|
||||
if(connectTimeout !== undefined) {
|
||||
clearTimeout(connectTimeout);
|
||||
connectTimeout = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const connectHandleMessage = ev => {
|
||||
worker?.removeEventListener('message', connectHandleMessage);
|
||||
|
||||
if(typeof ev.data !== 'object' || ev.data === null || ev.data.type !== 'objdef'
|
||||
|| typeof ev.data.detail !== 'object' || ev.data.detail.object !== '') {
|
||||
callReject('data');
|
||||
} else
|
||||
callResolve();
|
||||
};
|
||||
|
||||
const callResolve = () => {
|
||||
reject = undefined;
|
||||
connectFinally();
|
||||
try {
|
||||
if(typeof resolve === 'function')
|
||||
resolve(root);
|
||||
} finally {
|
||||
resolve = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const callReject = (...args) => {
|
||||
resolve = undefined;
|
||||
connectFinally();
|
||||
try {
|
||||
broadcastTimeoutZone(timeout => {
|
||||
timeout({ at: 'connect' });
|
||||
});
|
||||
|
||||
if(typeof reject === 'function')
|
||||
reject(...args);
|
||||
} finally {
|
||||
reject = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if(worker !== undefined) {
|
||||
worker.terminate();
|
||||
workerId = worker = undefined;
|
||||
}
|
||||
|
||||
hasTimedout = false;
|
||||
workerId = MamiUniqueStr(5);
|
||||
worker = new Worker(url);
|
||||
worker.addEventListener('message', handleMessage);
|
||||
worker.addEventListener('message', connectHandleMessage);
|
||||
|
||||
connectTimeout = setTimeout(() => callReject('timeout'), timeOutMs);
|
||||
worker.postMessage({ type: 'init' });
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,119 +0,0 @@
|
|||
const $xhr = (function() {
|
||||
const methods = ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'];
|
||||
|
||||
const send = function(
|
||||
method,
|
||||
url,
|
||||
{
|
||||
authed=false,
|
||||
download=null,
|
||||
headers=null,
|
||||
signal=null,
|
||||
timeout=null,
|
||||
type=null,
|
||||
upload=null,
|
||||
}={},
|
||||
body=null
|
||||
) {
|
||||
const xhr = new XMLHttpRequest;
|
||||
const requestHeaders = new Map;
|
||||
|
||||
if(signal instanceof AbortSignal)
|
||||
signal.onabort = () => { xhr.abort(); };
|
||||
|
||||
if(typeof headers === 'object')
|
||||
for(const name in headers)
|
||||
if(headers.hasOwnProperty(name))
|
||||
requestHeaders.set(name.toLowerCase(), headers[name]);
|
||||
|
||||
if(typeof download === 'function') {
|
||||
xhr.onloadstart = ev => { download(ev); };
|
||||
xhr.onprogress = ev => { download(ev); };
|
||||
xhr.onloadend = ev => { download(ev); };
|
||||
}
|
||||
|
||||
if(typeof upload === 'function') {
|
||||
xhr.upload.onloadstart = ev => { upload(ev); };
|
||||
xhr.upload.onprogress = ev => { upload(ev); };
|
||||
xhr.upload.onloadend = ev => { upload(ev); };
|
||||
}
|
||||
|
||||
if(authed !== null)
|
||||
xhr.withCredentials = authed;
|
||||
if(timeout !== null)
|
||||
xhr.timeout = timeout;
|
||||
if(type !== null)
|
||||
xhr.responseType = type;
|
||||
|
||||
if(typeof body === 'object' && body !== null) {
|
||||
if(body instanceof URLSearchParams) {
|
||||
requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
|
||||
} else if(body instanceof FormData) {
|
||||
// content-type is implicitly set
|
||||
} else if(body instanceof Blob || body instanceof ArrayBuffer
|
||||
|| body instanceof DataView || body instanceof Uint8Array) {
|
||||
if(!requestHeaders.has('content-type'))
|
||||
requestHeaders.set('content-type', 'application/octet-stream');
|
||||
} else if(!requestHeaders.has('content-type')) {
|
||||
const bodyParts = [];
|
||||
for(const name in body)
|
||||
if(body.hasOwnProperty(name))
|
||||
bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name]));
|
||||
body = bodyParts.join('&');
|
||||
requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr.onload = ev => {
|
||||
const headers = (headersString => {
|
||||
const headers = new Map;
|
||||
|
||||
const raw = headersString.trim().split(/[\r\n]+/);
|
||||
for(const name in raw)
|
||||
if(raw.hasOwnProperty(name)) {
|
||||
const parts = raw[name].split(': ');
|
||||
headers.set(parts.shift(), parts.join(': '));
|
||||
}
|
||||
|
||||
return headers;
|
||||
})(xhr.getAllResponseHeaders());
|
||||
|
||||
resolve({
|
||||
get ev() { return ev; },
|
||||
get xhr() { return xhr; },
|
||||
|
||||
get status() { return xhr.status; },
|
||||
get headers() { return headers; },
|
||||
get body() { return xhr.response; },
|
||||
get text() { return xhr.responseText; },
|
||||
});
|
||||
};
|
||||
|
||||
xhr.onabort = ev => {
|
||||
reject({
|
||||
aborted: true,
|
||||
ev,
|
||||
});
|
||||
};
|
||||
|
||||
xhr.onerror = ev => {
|
||||
reject({
|
||||
aborted: false,
|
||||
ev,
|
||||
});
|
||||
};
|
||||
|
||||
xhr.open(method, url);
|
||||
for(const [name, value] of requestHeaders)
|
||||
xhr.setRequestHeader(name, value);
|
||||
xhr.send(body);
|
||||
});
|
||||
};
|
||||
|
||||
const pub = { send };
|
||||
for(const method of methods)
|
||||
pub[method.toLowerCase()] = (...args) => send(method, ...args);
|
||||
|
||||
return pub;
|
||||
})();
|
19
src/proto.js/main.js
Normal file
19
src/proto.js/main.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
#include skel.js
|
||||
#include sockchat/proto.js
|
||||
|
||||
const skel = new WorkerSkeleton;
|
||||
|
||||
skel.defineMethod('create', (name, options) => {
|
||||
if(typeof name !== 'string')
|
||||
throw 'name must be a string';
|
||||
|
||||
let proto, prefix;
|
||||
|
||||
if(name === 'sockchat')
|
||||
proto = new SockChatProtocol(
|
||||
skel.createDispatcher(prefix = 'sockchat'),
|
||||
options
|
||||
);
|
||||
|
||||
return skel.defineObject(proto, prefix);
|
||||
}, true);
|
214
src/proto.js/skel.js
Normal file
214
src/proto.js/skel.js
Normal file
|
@ -0,0 +1,214 @@
|
|||
#include uniqstr.js
|
||||
|
||||
const WorkerSkeletonObject = function(name, defineObjectMethod, deleteObjectMethod, deleteObject) {
|
||||
if(typeof name !== 'string')
|
||||
throw 'name must be a string';
|
||||
if(typeof defineObjectMethod !== 'function')
|
||||
throw 'defineObjectMethod must be a function';
|
||||
if(typeof deleteObjectMethod !== 'function')
|
||||
throw 'deleteObjectMethod must be a function';
|
||||
if(typeof deleteObject !== 'function')
|
||||
throw 'deleteObject must be a function';
|
||||
|
||||
return {
|
||||
getObjectName: () => name,
|
||||
defineMethod: (...args) => defineObjectMethod(name, ...args),
|
||||
deleteMethod: (...args) => deleteObjectMethod(name, ...args),
|
||||
destroy: () => deleteObject(name),
|
||||
};
|
||||
};
|
||||
|
||||
const WorkerSkeleton = function(globalScope) {
|
||||
if(globalScope === undefined)
|
||||
globalScope = self;
|
||||
|
||||
const objects = new Map;
|
||||
const handlers = {};
|
||||
let initialised = false;
|
||||
|
||||
const sendPayload = (type, detail) => {
|
||||
globalScope.postMessage({ type: type, detail: detail });
|
||||
};
|
||||
|
||||
const sendObjectDefinePayload = (objName, objBody, eventPrefix) => {
|
||||
sendPayload('objdef', { object: objName, methods: Object.keys(objBody), eventPrefix: eventPrefix });
|
||||
};
|
||||
|
||||
const defineObject = (objName, objBody, eventPrefix) => {
|
||||
if(typeof objName !== 'string')
|
||||
throw 'objName must be a string';
|
||||
if(typeof eventPrefix !== 'string' && eventPrefix !== undefined)
|
||||
throw 'eventPrefix must be string or undefined';
|
||||
if(objects.has(objName))
|
||||
throw 'objName is already defined';
|
||||
|
||||
const object = {};
|
||||
for(const name in objBody) {
|
||||
const item = objBody[name];
|
||||
if(typeof item === 'function')
|
||||
object[name] = { body: item };
|
||||
}
|
||||
|
||||
objects.set(objName, object);
|
||||
if(initialised)
|
||||
sendObjectDefinePayload(objName, object, eventPrefix);
|
||||
};
|
||||
|
||||
const deleteObject = objName => {
|
||||
if(typeof objName !== 'string')
|
||||
throw 'objName must be a string';
|
||||
if(!objects.has(objName))
|
||||
throw 'objName is not defined';
|
||||
|
||||
objects.delete(objName);
|
||||
if(initialised)
|
||||
sendPayload('objdel', { object: objName });
|
||||
};
|
||||
|
||||
const defineObjectMethod = (objName, metName, metBody, returnsObject) => {
|
||||
if(typeof objName !== 'string')
|
||||
throw 'objName must be a string';
|
||||
if(typeof metName !== 'string')
|
||||
throw 'metName must be a string';
|
||||
if(typeof metBody !== 'function')
|
||||
throw 'metBody must be a function';
|
||||
|
||||
const objBody = objects.get(objName);
|
||||
if(objBody === undefined)
|
||||
throw 'objName has not been defined';
|
||||
|
||||
objBody[metName] = { body: metBody, returnsObject: returnsObject === true };
|
||||
if(initialised)
|
||||
sendPayload('metdef', { object: objName, method: metName });
|
||||
};
|
||||
|
||||
const deleteObjectMethod = (objName, metName) => {
|
||||
if(typeof objName !== 'string')
|
||||
throw 'objName must be a string';
|
||||
if(typeof metName !== 'string')
|
||||
throw 'metName must be a string';
|
||||
|
||||
const objBody = objects.get(objName);
|
||||
if(objBody === undefined)
|
||||
throw 'objName has not been defined';
|
||||
|
||||
delete objBody[objName];
|
||||
if(initialised)
|
||||
sendPayload('metdel', { object: objName, method: metName });
|
||||
};
|
||||
|
||||
const createDispatcher = prefix => {
|
||||
if(prefix === undefined)
|
||||
prefix = '';
|
||||
else if(typeof prefix !== 'string')
|
||||
throw 'prefix must be a string or undefined';
|
||||
|
||||
if(prefix !== '' && !prefix.endsWith(':'))
|
||||
prefix += ':';
|
||||
|
||||
return (name, detail) => sendPayload('evtdisp', { name: prefix + name, detail: detail });
|
||||
};
|
||||
|
||||
defineObject('', {});
|
||||
|
||||
const defineHandler = (name, handler) => {
|
||||
if(typeof name !== 'string')
|
||||
throw 'name must be a string';
|
||||
if(typeof handler !== 'function')
|
||||
throw 'handler must be a function';
|
||||
if(name in handlers)
|
||||
throw 'name is already defined';
|
||||
|
||||
handlers[name] = handler;
|
||||
};
|
||||
|
||||
defineHandler('init', () => {
|
||||
if(initialised)
|
||||
return;
|
||||
initialised = true;
|
||||
|
||||
sendObjectDefinePayload('', objects.get(''));
|
||||
});
|
||||
|
||||
defineHandler('metcall', req => {
|
||||
if(typeof req.id !== 'string')
|
||||
throw 'call id is not a string';
|
||||
|
||||
const respond = (id, success, result, mightBeObject) => {
|
||||
let isObject = false;
|
||||
if(mightBeObject) {
|
||||
const resultType = typeof result;
|
||||
if(resultType === 'string' && objects.has(result))
|
||||
isObject = true;
|
||||
else if(resultType === 'object' && result !== null && typeof result.getObjectName === 'function') {
|
||||
const objectName = result.getObjectName();
|
||||
if(objects.has(objectName)) {
|
||||
isObject = true;
|
||||
result = objectName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendPayload('funcret', {
|
||||
id: id,
|
||||
success: success,
|
||||
result: result,
|
||||
object: isObject,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
if(typeof req.object !== 'string')
|
||||
throw 'object name is not a string';
|
||||
if(typeof req.method !== 'string')
|
||||
throw 'method name is not a string';
|
||||
|
||||
const object = objects.get(req.object);
|
||||
if(object === undefined)
|
||||
throw 'object is not defined';
|
||||
if(!(req.method in object))
|
||||
throw 'method is not defined in object';
|
||||
|
||||
const args = Array.isArray(req.args) ? req.args : [];
|
||||
const info = object[req.method];
|
||||
let result = info.body(...args);
|
||||
|
||||
if(result instanceof Promise) {
|
||||
result.then(result => respond(req.id, true, result, info.returnsObject)).catch(ex => respond(req.id, false, ex));
|
||||
} else
|
||||
respond(req.id, true, result, info.returnsObject);
|
||||
} catch(ex) {
|
||||
respond(req.id, false, ex);
|
||||
}
|
||||
});
|
||||
|
||||
globalScope.addEventListener('message', ev => {
|
||||
if(typeof ev.data === 'string') {
|
||||
if(ev.data.startsWith('ping:')) {
|
||||
globalScope.postMessage(`pong:${ev.data.substring(5)}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(typeof ev.data === 'object' && ev.data !== null && typeof ev.data.type === 'string') {
|
||||
if(ev.data.type in handlers)
|
||||
handlers[ev.data.type](ev.data.detail);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
sendPayload: sendPayload,
|
||||
createDispatcher: createDispatcher,
|
||||
defineObject: (object, eventPrefix) => {
|
||||
if(typeof object !== 'object' || object === null)
|
||||
return undefined;
|
||||
|
||||
const name = MamiUniqueStr(8);
|
||||
defineObject(name, object, eventPrefix);
|
||||
return new WorkerSkeletonObject(name, defineObjectMethod, deleteObjectMethod, deleteObject);
|
||||
},
|
||||
defineMethod: (...args) => defineObjectMethod('', ...args),
|
||||
deleteMethod: (...args) => deleteObjectMethod('', ...args),
|
||||
};
|
||||
};
|
449
src/proto.js/sockchat/authed.js
Normal file
449
src/proto.js/sockchat/authed.js
Normal file
|
@ -0,0 +1,449 @@
|
|||
#include sockchat/utils.js
|
||||
|
||||
const SockChatS2CPong = ctx => {
|
||||
const lastPong = Date.now();
|
||||
const lastPing = ctx.lastPing;
|
||||
|
||||
ctx.lastPing = undefined;
|
||||
if(lastPing === undefined)
|
||||
throw 'unexpected pong received??';
|
||||
|
||||
ctx.pingPromise?.resolve({
|
||||
ping: lastPing,
|
||||
pong: lastPong,
|
||||
diff: lastPong - lastPing,
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CBanKick = (ctx, type, expiresTime) => {
|
||||
ctx.wasKicked = true;
|
||||
|
||||
const bakaInfo = {
|
||||
session: { success: false },
|
||||
baka: {
|
||||
type: type === '0' ? 'kick' : 'ban',
|
||||
},
|
||||
};
|
||||
|
||||
if(bakaInfo.baka.type === 'ban') {
|
||||
bakaInfo.baka.perma = expiresTime === '-1';
|
||||
bakaInfo.baka.until = expiresTime === '-1' ? undefined : new Date(parseInt(expiresTime) * 1000);
|
||||
}
|
||||
|
||||
ctx.dispatch('session:term', bakaInfo);
|
||||
};
|
||||
|
||||
const SockChatS2CContextClear = (ctx, mode) => {
|
||||
if(mode === '0' || mode === '3' || mode === '4')
|
||||
ctx.dispatch('msg:clear');
|
||||
|
||||
if(mode === '1' || mode === '3' || mode === '4') {
|
||||
ctx.users.clear();
|
||||
ctx.dispatch('user:clear');
|
||||
}
|
||||
|
||||
if(mode === '2' || mode === '4') {
|
||||
ctx.channels.clear();
|
||||
ctx.dispatch('chan:clear');
|
||||
}
|
||||
};
|
||||
|
||||
const SockChatS2CChannelPopulate = (ctx, count, ...args) => {
|
||||
count = parseInt(count);
|
||||
ctx.dispatch('chan:clear');
|
||||
|
||||
for(let i = 0; i < count; ++i) {
|
||||
const offset = 3 * i;
|
||||
const chanInfo = {
|
||||
name: args[offset],
|
||||
hasPassword: args[offset + 1] !== '0',
|
||||
isTemporary: args[offset + 2] !== '0',
|
||||
};
|
||||
|
||||
ctx.channels.set(chanInfo.name, chanInfo);
|
||||
ctx.dispatch('chan:add', {
|
||||
channel: {
|
||||
name: chanInfo.name,
|
||||
hasPassword: chanInfo.hasPassword,
|
||||
isTemporary: chanInfo.name.isTemporary,
|
||||
isCurrent: chanInfo.name === ctx.channelName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ctx.dispatch('chan:focus', {
|
||||
channel: { name: ctx.channelName },
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CChannelAdd = (ctx, name, hasPass, isTemp) => {
|
||||
const chanInfo = {
|
||||
name: name,
|
||||
hasPassword: hasPass !== '0',
|
||||
isTemporary: isTemp !== '0',
|
||||
};
|
||||
|
||||
ctx.channels.set(chanInfo.name, chanInfo);
|
||||
ctx.dispatch('chan:add', {
|
||||
channel: chanInfo,
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CChannelUpdate = (ctx, prevName, name, hasPass, isTemp) => {
|
||||
const chanInfo = ctx.channels.get(prevName);
|
||||
|
||||
if(prevName !== name) {
|
||||
chanInfo.name = name;
|
||||
ctx.channels.delete(prevName);
|
||||
ctx.channels.set(name, chanInfo);
|
||||
}
|
||||
|
||||
chanInfo.hasPassword = hasPass !== '0';
|
||||
chanInfo.isTemporary = isTemp !== '0';
|
||||
|
||||
ctx.dispatch('chan:update', {
|
||||
channel: {
|
||||
previousName: prevName,
|
||||
name: chanInfo.name,
|
||||
hasPassword: chanInfo.hasPassword,
|
||||
isTemporary: chanInfo.isTemporary,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CChannelRemove = (ctx, name) => {
|
||||
ctx.channels.delete(name);
|
||||
ctx.dispatch('chan:remove', {
|
||||
channel: { name: name },
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CUserPopulate = (ctx, count, ...args) => {
|
||||
count = parseInt(count);
|
||||
ctx.dispatch('user:clear');
|
||||
|
||||
for(let i = 0; i < count; ++i) {
|
||||
const offset = 5 * i;
|
||||
const statusInfo = SockChatParseStatusInfo(args[offset + 1]);
|
||||
const userInfo = {
|
||||
id: args[offset],
|
||||
self: args[offset] === ctx.userId,
|
||||
name: statusInfo.name,
|
||||
status: statusInfo.status,
|
||||
colour: SockChatParseUserColour(args[offset + 2]),
|
||||
perms: SockChatParseUserPerms(args[offset + 3]),
|
||||
hidden: args[offset + 4] !== '0',
|
||||
};
|
||||
|
||||
ctx.users.set(userInfo.id, userInfo);
|
||||
ctx.dispatch('user:add', {
|
||||
user: userInfo,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const SockChatS2CUserAdd = (ctx, timeStamp, userId, userName, userColour, userPerms, msgId) => {
|
||||
const statusInfo = SockChatParseStatusInfo(userName);
|
||||
const userInfo = {
|
||||
id: userId,
|
||||
self: userId === ctx.userId,
|
||||
name: statusInfo.name,
|
||||
status: statusInfo.status,
|
||||
colour: SockChatParseUserColour(userColour),
|
||||
perms: SockChatParseUserPerms(userPerms),
|
||||
};
|
||||
|
||||
ctx.users.set(userInfo.id, userInfo);
|
||||
ctx.dispatch('user:add', {
|
||||
msg: {
|
||||
type: 'user:add',
|
||||
id: msgId,
|
||||
time: new Date(parseInt(timeStamp) * 1000),
|
||||
channel: ctx.channelName,
|
||||
sender: userInfo,
|
||||
},
|
||||
user: userInfo,
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CUserUpdate = (ctx, userId, userName, userColour, userPerms) => {
|
||||
const statusInfo = SockChatParseStatusInfo(userName);
|
||||
const userInfo = ctx.users.get(userId);
|
||||
|
||||
userInfo.self = userId === ctx.userId;
|
||||
userInfo.name = statusInfo.name;
|
||||
userInfo.status = statusInfo.status;
|
||||
userInfo.colour = SockChatParseUserColour(userColour);
|
||||
userInfo.perms = SockChatParseUserPerms(userPerms);
|
||||
|
||||
ctx.users.set(userInfo.id, userInfo);
|
||||
ctx.dispatch('user:update', {
|
||||
user: userInfo,
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CUserRemove = (ctx, userId, userName, reason, timeStamp, msgId) => {
|
||||
const statusInfo = SockChatParseStatusInfo(userName);
|
||||
const userInfo = ctx.users.get(userId);
|
||||
userInfo.name = statusInfo.name;
|
||||
userInfo.status = statusInfo.status;
|
||||
ctx.users.delete(userId);
|
||||
|
||||
ctx.dispatch('user:remove', {
|
||||
leave: { type: reason },
|
||||
msg: {
|
||||
type: 'user:remove',
|
||||
id: msgId,
|
||||
time: new Date(parseInt(timeStamp) * 1000),
|
||||
channel: ctx.channelName,
|
||||
sender: userInfo,
|
||||
detail: { reason: reason },
|
||||
},
|
||||
user: userInfo,
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CUserChannelJoin = (ctx, userId, userName, userColour, userPerms, msgId) => {
|
||||
const statusInfo = SockChatParseStatusInfo(userName);
|
||||
const userInfo = {
|
||||
id: userId,
|
||||
self: userId === ctx.userId,
|
||||
name: statusInfo.name,
|
||||
status: statusInfo.status,
|
||||
colour: SockChatParseUserColour(userColour),
|
||||
perms: SockChatParseUserPerms(userPerms),
|
||||
};
|
||||
|
||||
ctx.users.set(userInfo.id, userInfo);
|
||||
ctx.dispatch('chan:join', {
|
||||
user: userInfo,
|
||||
msg: {
|
||||
type: 'chan:join',
|
||||
id: msgId,
|
||||
time: new Date,
|
||||
channel: ctx.channelName,
|
||||
sender: userInfo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CUserChannelLeave = (ctx, userId, msgId) => {
|
||||
const userInfo = ctx.users.get(userId);
|
||||
ctx.users.delete(userId); // should this happen? probably not!
|
||||
|
||||
ctx.dispatch('chan:leave', {
|
||||
user: userInfo,
|
||||
msg: {
|
||||
type: 'chan:leave',
|
||||
id: msgId,
|
||||
time: new Date,
|
||||
channel: ctx.channelName,
|
||||
sender: userInfo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CUserChannelFocus = (ctx, name) => {
|
||||
ctx.channelName = name;
|
||||
|
||||
ctx.dispatch('chan:focus', {
|
||||
channel: { name: ctx.channelName },
|
||||
});
|
||||
};
|
||||
|
||||
const SockChatS2CMessagePopulate = (ctx, timeStamp, userId, userName, userColour, userPerms, msgText, msgId, msgNotify, msgFlags) => {
|
||||
const mFlags = SockChatParseMsgFlags(msgFlags);
|
||||
const statusInfo = SockChatParseStatusInfo(userName);
|
||||
const info = {
|
||||
msg: {
|
||||
type: `msg:${mFlags.isAction ? 'action' : 'text'}`,
|
||||
time: new Date(parseInt(timeStamp) * 1000),
|
||||
channel: ctx.channelName,
|
||||
sender: {
|
||||
id: userId,
|
||||
self: userId === ctx.userId,
|
||||
name: statusInfo.name,
|
||||
status: statusInfo.status,
|
||||
colour: SockChatParseUserColour(userColour),
|
||||
perms: SockChatParseUserPerms(userPerms),
|
||||
},
|
||||
isBot: userId === '-1',
|
||||
silent: msgNotify === '0',
|
||||
flags: mFlags,
|
||||
text: SockChatUnfuckText(msgText, mFlags.isAction),
|
||||
},
|
||||
};
|
||||
|
||||
if(!isNaN(msgId))
|
||||
info.msg.id = msgId;
|
||||
|
||||
if(info.msg.isBot) {
|
||||
const botParts = msgText.split("\f");
|
||||
info.msg.botInfo = {
|
||||
isError: botParts[0] === '1',
|
||||
type: botParts[1],
|
||||
args: botParts.slice(2),
|
||||
};
|
||||
|
||||
let botUserNameIndex = 2;
|
||||
if(info.msg.botInfo.type === 'say') {
|
||||
info.msg.botInfo.args[0] = info.msg.text = SockChatUnfuckText(info.msg.botInfo.args[0], mFlags.isAction);
|
||||
} else if(info.msg.botInfo.type === 'join') {
|
||||
info.msg.type = 'user:add';
|
||||
} else if(['leave', 'kick', 'flood', 'timeout'].includes(info.msg.botInfo.type)) {
|
||||
info.msg.type = 'user:remove';
|
||||
info.msg.detail = { reason: info.msg.botInfo.type };
|
||||
} else if(info.msg.botInfo.type === 'nick') {
|
||||
info.msg.type = 'user:nick';
|
||||
info.msg.detail = {
|
||||
previousName: info.msg.botInfo.args[0],
|
||||
name: info.msg.botInfo.args[1],
|
||||
};
|
||||
} else if(info.msg.botInfo.type === 'jchan') {
|
||||
info.msg.type = 'chan:join';
|
||||
} else if(info.msg.botInfo.type === 'lchan') {
|
||||
info.msg.type = 'chan:leave';
|
||||
} else {
|
||||
info.msg.type = `legacy:${info.msg.botInfo.type}`;
|
||||
info.msg.detail = {
|
||||
error: info.msg.botInfo.isError,
|
||||
args: info.msg.botInfo.args,
|
||||
};
|
||||
}
|
||||
|
||||
if(['join', 'leave', 'kick', 'flood', 'timeout', 'jchan', 'lchan', 'nick'].includes(botParts[1])) {
|
||||
info.msg.sender = undefined;
|
||||
|
||||
const botUserTarget = botParts[botUserNameIndex].toLowerCase();
|
||||
for(const botUserInfo of ctx.users.values())
|
||||
if(botUserInfo.name.toLowerCase() === botUserTarget) {
|
||||
info.msg.sender = botUserInfo;
|
||||
break;
|
||||
}
|
||||
|
||||
if(info.msg.sender === undefined)
|
||||
info.msg.sender = {
|
||||
id: '0',
|
||||
self: false,
|
||||
name: botParts[botUserNameIndex],
|
||||
colour: 'inherit',
|
||||
perms: { rank: 0 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ctx.dispatch('msg:add', info);
|
||||
};
|
||||
|
||||
const SockChatS2CMessageAdd = (ctx, timeStamp, userId, msgText, msgId, msgFlags) => {
|
||||
const mFlags = SockChatParseMsgFlags(msgFlags);
|
||||
let mText = SockChatUnfuckText(msgText, mFlags.isAction);
|
||||
let mChannelName = ctx.channelName;
|
||||
|
||||
const userInfo = userId === '-1' ? {
|
||||
id: '-1',
|
||||
self: false,
|
||||
name: 'ChatBot',
|
||||
colour: 'inherit',
|
||||
perms: { rank: 0 },
|
||||
} : ctx.users.get(userId);
|
||||
|
||||
if(msgFlags[4] !== '0') {
|
||||
if(userId === ctx.userId) {
|
||||
const mTextParts = mText.split(' ');
|
||||
mChannelName = `@${mTextParts.shift()}`;
|
||||
mText = mTextParts.join(' ');
|
||||
} else
|
||||
mChannelName = `@${userInfo.name}`;
|
||||
|
||||
if(!ctx.channels.has(mChannelName)) {
|
||||
const chanInfo = {
|
||||
name: mChannelName,
|
||||
hasPassword: false,
|
||||
isTemporary: true,
|
||||
isUserChannel: true,
|
||||
};
|
||||
|
||||
ctx.channels.set(chanInfo.name, chanInfo);
|
||||
ctx.dispatch('chan:add', { channel: chanInfo });
|
||||
}
|
||||
}
|
||||
|
||||
const msgInfo = {
|
||||
msg: {
|
||||
type: `msg:${mFlags.isAction ? 'action' : 'text'}`,
|
||||
id: msgId,
|
||||
time: new Date(parseInt(timeStamp) * 1000),
|
||||
channel: mChannelName,
|
||||
sender: userInfo,
|
||||
flags: mFlags,
|
||||
isBot: userId === '-1',
|
||||
text: mText,
|
||||
},
|
||||
};
|
||||
|
||||
if(msgInfo.msg.isBot) {
|
||||
const botParts = msgText.split("\f");
|
||||
msgInfo.msg.botInfo = {
|
||||
isError: botParts[0] === '1',
|
||||
type: botParts[1],
|
||||
args: botParts.slice(2),
|
||||
};
|
||||
|
||||
let botUserNameIndex = 2;
|
||||
if(msgInfo.msg.botInfo.type === 'join') {
|
||||
msgInfo.msg.type = 'user:add';
|
||||
} else if(['leave', 'kick', 'flood', 'timeout'].includes(msgInfo.msg.botInfo.type)) {
|
||||
msgInfo.msg.type = 'user:remove';
|
||||
msgInfo.msg.detail = { reason: msgInfo.msg.botInfo.type };
|
||||
} else if(msgInfo.msg.botInfo.type === 'nick') {
|
||||
msgInfo.msg.type = 'user:nick';
|
||||
msgInfo.msg.detail = {
|
||||
previousName: msgInfo.msg.botInfo.args[0],
|
||||
name: msgInfo.msg.botInfo.args[1],
|
||||
};
|
||||
} else if(msgInfo.msg.botInfo.type === 'jchan') {
|
||||
msgInfo.msg.type = 'chan:join';
|
||||
} else if(msgInfo.msg.botInfo.type === 'lchan') {
|
||||
msgInfo.msg.type = 'chan:leave';
|
||||
} else {
|
||||
msgInfo.msg.type = `legacy:${msgInfo.msg.botInfo.type}`;
|
||||
msgInfo.msg.detail = {
|
||||
error: msgInfo.msg.botInfo.isError,
|
||||
args: msgInfo.msg.botInfo.args,
|
||||
};
|
||||
}
|
||||
|
||||
if(['join', 'leave', 'kick', 'flood', 'timeout', 'jchan', 'lchan', 'nick'].includes(botParts[1])) {
|
||||
msgInfo.msg.sender = undefined;
|
||||
|
||||
const botUserTarget = botParts[botUserNameIndex].toLowerCase();
|
||||
for(const botUserInfo of ctx.users.values())
|
||||
if(botUserInfo.name.toLowerCase() === botUserTarget) {
|
||||
msgInfo.msg.sender = botUserInfo;
|
||||
break;
|
||||
}
|
||||
|
||||
if(msgInfo.msg.sender === undefined)
|
||||
msgInfo.msg.sender = {
|
||||
id: '0',
|
||||
self: false,
|
||||
name: botParts[botUserNameIndex],
|
||||
colour: 'inherit',
|
||||
perms: { rank: 0 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ctx.dispatch('msg:add', msgInfo);
|
||||
};
|
||||
|
||||
const SockChatS2CMessageRemove = (ctx, msgId) => {
|
||||
ctx.dispatch('msg:remove', {
|
||||
msg: {
|
||||
type: 'msg:remove',
|
||||
id: msgId,
|
||||
channel: ctx.channelName,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
#include proto/sockchat/keepalive.js
|
||||
#include sockchat/keepalive.js
|
||||
|
||||
const SockChatContext = function(dispatch, sendPing, pingDelay) {
|
||||
if(typeof dispatch !== 'function')
|
||||
|
@ -12,6 +12,9 @@ const SockChatContext = function(dispatch, sendPing, pingDelay) {
|
|||
|
||||
get isAuthed() { return userId !== undefined; },
|
||||
|
||||
users: new Map,
|
||||
channels: new Map,
|
||||
|
||||
channelName: undefined,
|
||||
pseudoChannelName: undefined,
|
||||
|
||||
|
@ -34,9 +37,9 @@ const SockChatContext = function(dispatch, sendPing, pingDelay) {
|
|||
// what is this? C#?
|
||||
public.dispose = () => {
|
||||
public.keepAlive?.stop();
|
||||
public.openPromise?.reject();
|
||||
public.authPromise?.reject();
|
||||
public.pingPromise?.reject();
|
||||
public.openPromise?.cancel();
|
||||
public.authPromise?.cancel();
|
||||
public.pingPromise?.cancel();
|
||||
};
|
||||
|
||||
return public;
|
|
@ -49,7 +49,7 @@ const SockChatKeepAlive = function(ctx, sendPing, delay) {
|
|||
start: run,
|
||||
stop: () => {
|
||||
clear();
|
||||
ctx.pingPromise?.reject();
|
||||
ctx.pingPromise?.cancel();
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,9 +1,9 @@
|
|||
#include awaitable.js
|
||||
#include proto/sockchat/authed.js
|
||||
#include proto/sockchat/ctx.js
|
||||
#include proto/sockchat/unauthed.js
|
||||
#include timedp.js
|
||||
#include sockchat/authed.js
|
||||
#include sockchat/ctx.js
|
||||
#include sockchat/unauthed.js
|
||||
|
||||
const SockChatClient = function(dispatch, options) {
|
||||
const SockChatProtocol = function(dispatch, options) {
|
||||
if(typeof dispatch !== 'function')
|
||||
throw 'dispatch must be a function';
|
||||
if(typeof options !== 'object' || options === null)
|
||||
|
@ -81,6 +81,8 @@ const SockChatClient = function(dispatch, options) {
|
|||
ctx.userId = undefined;
|
||||
ctx.channelName = undefined;
|
||||
ctx.pseudoChannelName = undefined;
|
||||
ctx.users.clear();
|
||||
ctx.channels.clear();
|
||||
|
||||
if(ev.code === 1012)
|
||||
ctx.isRestarting = true;
|
||||
|
@ -125,48 +127,36 @@ const SockChatClient = function(dispatch, options) {
|
|||
sock?.send(args.join("\t"));
|
||||
};
|
||||
|
||||
const sendPing = async () => {
|
||||
return await MamiTimeout(
|
||||
new Promise((resolve, reject) => {
|
||||
if(ctx === undefined)
|
||||
throw 'no connection opened';
|
||||
if(!ctx.isAuthed)
|
||||
throw 'must be authenticated';
|
||||
if(ctx.pingPromise !== undefined)
|
||||
throw 'already sending a ping';
|
||||
const sendPing = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(ctx === undefined)
|
||||
throw 'no connection opened';
|
||||
if(!ctx.isAuthed)
|
||||
throw 'must be authenticated';
|
||||
if(ctx.pingPromise !== undefined)
|
||||
throw 'already sending a ping';
|
||||
|
||||
ctx.lastPing = Date.now();
|
||||
ctx.pingPromise = {
|
||||
resolve(...args) { resolve(...args); },
|
||||
reject(...args) { reject(...args); },
|
||||
};
|
||||
ctx.lastPing = Date.now();
|
||||
ctx.pingPromise = new TimedPromise(resolve, reject, () => ctx.pingPromise = undefined, 2000);
|
||||
|
||||
send('0', ctx.userId);
|
||||
}),
|
||||
2000
|
||||
).finally(() => { ctx.pingPromise = undefined; });
|
||||
send('0', ctx.userId);
|
||||
});
|
||||
};
|
||||
|
||||
const sendAuth = async (...args) => {
|
||||
return await MamiTimeout(
|
||||
new Promise((resolve, reject) => {
|
||||
if(ctx === undefined)
|
||||
throw 'no connection opened';
|
||||
if(ctx.isAuthed)
|
||||
throw 'already authenticated';
|
||||
if(ctx.authPromise !== undefined)
|
||||
throw 'already authenticating';
|
||||
const sendAuth = (...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(ctx === undefined)
|
||||
throw 'no connection opened';
|
||||
if(ctx.isAuthed)
|
||||
throw 'already authenticated';
|
||||
if(ctx.authPromise !== undefined)
|
||||
throw 'already authenticating';
|
||||
|
||||
ctx.authPromise = {
|
||||
resolve(...args) { resolve(...args); },
|
||||
reject(...args) { reject(...args); },
|
||||
};
|
||||
|
||||
send('1', ...args);
|
||||
}),
|
||||
// HttpClient in C# has its Moments, so lets give this way too long to do its thing
|
||||
10000
|
||||
).finally(() => { ctx.authPromise = undefined; });
|
||||
ctx.authPromise = new TimedPromise(resolve, reject, () => ctx.authPromise = undefined, 10000);
|
||||
|
||||
send('1', ...args);
|
||||
});
|
||||
};
|
||||
|
||||
const sendMessage = text => {
|
||||
|
@ -192,7 +182,8 @@ const SockChatClient = function(dispatch, options) {
|
|||
// server doesn't send a direct ACK and we can't tell what message
|
||||
// which response messages is actually associated with this
|
||||
// an ACK or request/response extension to the protocol will be required
|
||||
resolve();
|
||||
if(typeof resolve === 'function')
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -217,43 +208,32 @@ const SockChatClient = function(dispatch, options) {
|
|||
};
|
||||
|
||||
return {
|
||||
get dumpPackets() { return dumpPackets; },
|
||||
set dumpPackets(value) { dumpPackets = !!value; },
|
||||
|
||||
async open(url, timeout=5000) {
|
||||
return await MamiTimeout(
|
||||
new Promise((resolve, reject) => {
|
||||
if(typeof url !== 'string')
|
||||
throw 'url must be a string';
|
||||
if(ctx?.openPromise !== undefined)
|
||||
throw 'already opening a connection';
|
||||
|
||||
closeWebSocket();
|
||||
createContext();
|
||||
|
||||
ctx.openPromise = {
|
||||
resolve(...args) { resolve(...args); },
|
||||
reject(...args) { reject(...args); },
|
||||
};
|
||||
|
||||
sock = new WebSocket(url);
|
||||
sock.addEventListener('open', handleOpen);
|
||||
sock.addEventListener('close', handleClose);
|
||||
sock.addEventListener('message', handleMessage);
|
||||
}),
|
||||
timeout
|
||||
).finally(() => { ctx.openPromise = undefined; });
|
||||
setDumpPackets: state => {
|
||||
dumpPackets = !!state;
|
||||
},
|
||||
open: url => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(typeof url !== 'string')
|
||||
throw 'url must be a string';
|
||||
if(ctx?.openPromise !== undefined)
|
||||
throw 'already opening a connection';
|
||||
|
||||
close() {
|
||||
closeWebSocket();
|
||||
closeWebSocket();
|
||||
createContext();
|
||||
|
||||
ctx.openPromise = new TimedPromise(resolve, reject, () => ctx.openPromise = undefined, 5000);
|
||||
|
||||
sock = new WebSocket(url);
|
||||
sock.addEventListener('open', handleOpen);
|
||||
sock.addEventListener('close', handleClose);
|
||||
sock.addEventListener('message', handleMessage);
|
||||
});
|
||||
},
|
||||
|
||||
sendPing,
|
||||
sendAuth,
|
||||
sendMessage,
|
||||
|
||||
async switchChannel(info) {
|
||||
close: () => { closeWebSocket(); },
|
||||
sendPing: sendPing,
|
||||
sendAuth: sendAuth,
|
||||
sendMessage: sendMessage,
|
||||
switchChannel: async info => {
|
||||
if(!ctx.isAuthed)
|
||||
return;
|
||||
|
||||
|
@ -274,5 +254,42 @@ const SockChatClient = function(dispatch, options) {
|
|||
await sendMessage(`/join ${name}`);
|
||||
}
|
||||
},
|
||||
getCurrentUserId: () => ctx.userId,
|
||||
getCurrentUserInfo: () => ctx.users.get(ctx.userId),
|
||||
getUserInfos: () => Array.from(ctx.users.values()),
|
||||
getUserInfoById: userId => {
|
||||
if(typeof userId === 'string')
|
||||
userId = parseInt(userId);
|
||||
if(typeof userId !== 'number')
|
||||
throw 'userId must be a number';
|
||||
|
||||
return ctx.users.get(userId);
|
||||
},
|
||||
getUserInfoByName: (userName, exact = true) => {
|
||||
if(typeof userName !== 'string')
|
||||
throw 'userName must be a string';
|
||||
|
||||
const compare = exact ? (a, b) => a.toLowerCase() === b : (a, b) => a.toLowerCase().includes(b);
|
||||
userName = userName.toLowerCase();
|
||||
|
||||
for(const info of ctx.users.values())
|
||||
if(compare(info.name, userName))
|
||||
return info;
|
||||
|
||||
return undefined;
|
||||
},
|
||||
getUserInfosByName: userName => {
|
||||
if(typeof userName !== 'string')
|
||||
throw 'userName must be a string';
|
||||
|
||||
const users = [];
|
||||
userName = userName.toLowerCase();
|
||||
|
||||
for(const info of ctx.users.values())
|
||||
if(info.name.toLowerCase().includes(userName))
|
||||
users.push(info);
|
||||
|
||||
return users;
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,10 +1,19 @@
|
|||
#include proto/sockchat/utils.js
|
||||
#include sockchat/utils.js
|
||||
|
||||
const SockChatS2CAuthSuccess = (ctx, userId, userName, userColour, userPerms, chanName, maxLength) => {
|
||||
ctx.userId = userId;
|
||||
ctx.channelName = chanName;
|
||||
|
||||
const statusInfo = SockChatParseStatusInfo(userName);
|
||||
const userInfo = {
|
||||
id: ctx.userId,
|
||||
self: true,
|
||||
name: statusInfo.name,
|
||||
status: statusInfo.status,
|
||||
colour: SockChatParseUserColour(userColour),
|
||||
perms: SockChatParseUserPerms(userPerms),
|
||||
};
|
||||
ctx.users.set(userInfo.id, userInfo);
|
||||
|
||||
const info = {
|
||||
wasConnected: ctx.wasConnected,
|
||||
|
@ -12,14 +21,7 @@ const SockChatS2CAuthSuccess = (ctx, userId, userName, userColour, userPerms, ch
|
|||
ctx: {
|
||||
maxMsgLength: parseInt(maxLength),
|
||||
},
|
||||
user: {
|
||||
id: ctx.userId,
|
||||
self: true,
|
||||
name: statusInfo.name,
|
||||
status: statusInfo.status,
|
||||
colour: SockChatParseUserColour(userColour),
|
||||
perms: SockChatParseUserPerms(userPerms),
|
||||
},
|
||||
user: userInfo,
|
||||
channel: {
|
||||
name: ctx.channelName,
|
||||
},
|
|
@ -14,8 +14,9 @@ const SockChatParseUserColour = str => {
|
|||
|
||||
const SockChatParseUserPerms = str => {
|
||||
const parts = str.split(str.includes("\f") ? "\f" : ' ');
|
||||
const rank = parseInt(parts[0] ?? '0');
|
||||
return {
|
||||
rank: parseInt(parts[0] ?? '0'),
|
||||
rank: isNaN(rank) ? 0 : rank,
|
||||
kick: parts[1] !== undefined && parts[1] !== '0',
|
||||
nick: parts[3] !== undefined && parts[3] !== '0',
|
||||
chan: parts[4] !== undefined && parts[4] !== '0',
|
48
src/proto.js/timedp.js
Normal file
48
src/proto.js/timedp.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
const TimedPromise = function(resolve, reject, always, timeoutMs) {
|
||||
let timeout, resolved = false;
|
||||
|
||||
const cancelTimeout = () => {
|
||||
if(timeout === undefined) {
|
||||
clearTimeout(timeout);
|
||||
timeout = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const doResolve = (...args) => {
|
||||
if(resolved) return;
|
||||
resolved = true;
|
||||
|
||||
cancelTimeout();
|
||||
reject = undefined;
|
||||
|
||||
if(typeof resolve === 'function')
|
||||
resolve(...args);
|
||||
if(typeof always === 'function')
|
||||
always();
|
||||
};
|
||||
|
||||
const doReject = (...args) => {
|
||||
if(resolved) return;
|
||||
resolved = true;
|
||||
|
||||
cancelTimeout();
|
||||
resolve = undefined;
|
||||
|
||||
if(typeof reject === 'function')
|
||||
reject(...args);
|
||||
if(typeof always === 'function')
|
||||
always();
|
||||
};
|
||||
|
||||
timeout = setTimeout(() => doReject('timeout'), timeoutMs);
|
||||
|
||||
return {
|
||||
resolve: doResolve,
|
||||
reject: doReject,
|
||||
cancel: () => {
|
||||
if(timeout === undefined)
|
||||
return;
|
||||
doReject('timeout');
|
||||
},
|
||||
};
|
||||
};
|
38
src/proto.js/uniqstr.js
Normal file
38
src/proto.js/uniqstr.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
const MamiRandomInt = (min, max) => {
|
||||
let ret = 0;
|
||||
const range = max - min;
|
||||
|
||||
const bitsNeeded = Math.ceil(Math.log2(range));
|
||||
if(bitsNeeded > 53)
|
||||
return -1;
|
||||
|
||||
const bytesNeeded = Math.ceil(bitsNeeded / 8),
|
||||
mask = Math.pow(2, bitsNeeded) - 1;
|
||||
|
||||
const bytes = new Uint8Array(bytesNeeded);
|
||||
crypto.getRandomValues(bytes);
|
||||
|
||||
let p = (bytesNeeded - 1) * 8;
|
||||
for(let i = 0; i < bytesNeeded; ++i) {
|
||||
ret += bytes[i] * Math.pow(2, p);
|
||||
p -= 8;
|
||||
}
|
||||
|
||||
ret &= mask;
|
||||
|
||||
if(ret >= range)
|
||||
return MamiRandomInt(min, max);
|
||||
|
||||
return min + ret;
|
||||
};
|
||||
|
||||
const MamiUniqueStr = (() => {
|
||||
const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789';
|
||||
|
||||
return length => {
|
||||
let str = '';
|
||||
for(let i = 0; i < length; ++i)
|
||||
str += chars[MamiRandomInt(0, chars.length)];
|
||||
return str;
|
||||
};
|
||||
})();
|
35
src/utils.js
Normal file
35
src/utils.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
const crypto = require('crypto');
|
||||
|
||||
exports.strtr = (str, replacements) => str.toString().replace(
|
||||
/{([^}]+)}/g, (match, key) => replacements[key] || match
|
||||
);
|
||||
|
||||
const trim = function(str, chars, flags) {
|
||||
if(chars === undefined)
|
||||
chars = " \n\r\t\v\0";
|
||||
|
||||
let start = 0,
|
||||
end = str.length;
|
||||
|
||||
if(flags & 0x01)
|
||||
while(start < end && chars.indexOf(str[start]) >= 0)
|
||||
++start;
|
||||
|
||||
if(flags & 0x02)
|
||||
while(end > start && chars.indexOf(str[end - 1]) >= 0)
|
||||
--end;
|
||||
|
||||
return (start > 0 || end < str.length)
|
||||
? str.substring(start, end)
|
||||
: str;
|
||||
};
|
||||
|
||||
exports.trimStart = (str, chars) => trim(str, chars, 0x01);
|
||||
exports.trimEnd = (str, chars) => trim(str, chars, 0x02);
|
||||
exports.trim = (str, chars) => trim(str, chars, 0x03);
|
||||
|
||||
exports.shortHash = function(text) {
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(text);
|
||||
return hash.digest('hex').substring(0, 8);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue