mami/src/mami.js/main.js
2024-02-24 20:26:40 +00:00

859 lines
28 KiB
JavaScript

window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };
#include animate.js
#include channel.js
#include channels.js
#include common.js
#include context.js
#include emotes.js
#include message.js
#include messages.js
#include mszauth.js
#include parsing.js
#include server.js
#include txtrigs.js
#include user.js
#include users.js
#include utility.js
#include weeb.js
#include audio/autoplay.js
#include audio/context.js
#include controls/views.js
#include eeprom/eeprom.js
#include settings/backup.js
#include settings/settings.js
#include sound/context.js
#include sound/osukeys.js
#include sound/umisound.js
#include ui/baka.jsx
#include ui/chat-layout.js
#include ui/hooks.js
#include ui/emotes.js
#include ui/input-menus.js
#include ui/loading-overlay.jsx
#include ui/markup.js
#include ui/menus.js
#include ui/messages.jsx
#include ui/view.js
#include ui/settings.jsx
#include ui/toggles.js
#include ui/uploads.js
(async () => {
const views = new MamiViewsControl({ body: document.body });
const loadingOverlay = new Umi.UI.LoadingOverlay('spinner', 'Loading...');
await views.push(loadingOverlay);
loadingOverlay.setMessage('Loading environment...');
try {
window.futami = await FutamiCommon.load();
} catch(ex) {
console.error(ex);
loadingOverlay.setIcon('cross');
loadingOverlay.setHeader('Failed!');
loadingOverlay.setMessage('Failed to load common settings!');
return;
}
loadingOverlay.setMessage('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;
}
setInterval(() => {
MamiMisuzuAuth.update()
.then(auth => {
if(!auth.ok)
location.assign(futami.get('login'));
})
}, 600000);
loadingOverlay.setMessage('Loading settings...');
const settings = new MamiSettings('umi-');
settings.define('style', 'string', 'dark');
settings.define('compactView', 'boolean', false);
settings.define('autoScroll', 'boolean', true);
settings.define('closeTabConfirm', 'boolean', false);
settings.define('showChannelList', 'boolean', false);
settings.define('fancyInfo', 'boolean', true);
settings.define('autoCloseUserContext', 'boolean', true);
settings.define('enableParser', 'boolean', true);
settings.define('enableEmoticons', 'boolean', true);
settings.define('autoParseUrls', 'boolean', true);
settings.define('preventOverflow', 'boolean', false);
settings.define('expandTextBox', 'boolean', false);
settings.define('eepromAutoInsert', 'boolean', true);
settings.define('autoEmbedV1', 'boolean', false);
settings.define('soundEnable', 'boolean', true, false, true);
settings.define('soundPack', 'string', 'ajax-chat');
settings.define('soundVolume', 'number', 80);
settings.define('soundEnableJoin', 'boolean', true);
settings.define('soundEnableLeave', 'boolean', true);
settings.define('soundEnableError', 'boolean', true);
settings.define('soundEnableServer', 'boolean', true);
settings.define('soundEnableIncoming', 'boolean', true);
settings.define('onlySoundOnMention', 'boolean', false);
settings.define('soundEnableOutgoing', 'boolean', true);
settings.define('soundEnablePrivate', 'boolean', true);
settings.define('soundEnableForceLeave', 'boolean', true);
settings.define('minecraft', ['no', 'yes', 'old'], 'no');
settings.define('windowsLiveMessenger', 'boolean', false);
settings.define('seinfeld', 'boolean', false);
settings.define('flashTitle', 'boolean', true);
settings.define('showServerMsgInTitle', 'boolean', true);
settings.define('playJokeSounds', 'boolean', true);
settings.define('weeaboo', 'boolean', false);
settings.define('motivationalImages', 'boolean', false);
settings.define('motivationalVideos', 'boolean', false);
settings.define('osuKeys', 'boolean', false);
settings.define('osuKeysV2', ['no', 'yes', 'rng'], 'no');
settings.define('explosionRadius', 'number', 20);
settings.define('dumpPackets', 'boolean', FUTAMI_DEBUG);
settings.define('dumpEvents', 'boolean', FUTAMI_DEBUG);
settings.define('neverUseWorker', 'boolean', false, false, true);
settings.define('forceUseWorker', 'boolean', false, false, true);
settings.define('marqueeAllNames', 'boolean', false);
settings.define('tmpDisableOldThemeSys', 'boolean', false, false, true);
const noNotifSupport = !('Notification' in window);
settings.define('enableNotifications', 'boolean', false, noNotifSupport, true);
settings.define('notificationShowMessage', 'boolean', false, noNotifSupport);
settings.define('notificationTriggers', 'string', '', noNotifSupport);
loadingOverlay.setMessage('Loading sounds...');
const soundCtx = new MamiSoundContext;
try {
const sounds = await futami.getJson('sounds2');
if(Array.isArray(sounds.library))
soundCtx.library.register(sounds.library, true);
if(Array.isArray(sounds.packs))
soundCtx.packs.register(sounds.packs, true);
} catch(ex) {
console.error(ex);
}
if(!await MamiDetectAutoPlay()) {
settings.set('soundEnable', false);
settings.virtualise('soundEnable');
}
settings.watch('soundEnable', (v, n, i) => {
if(v) {
if(!soundCtx.ready)
soundCtx.reset();
settings.touch('soundVolume');
settings.touch('soundPack');
soundCtx.library.play(soundCtx.pack.getEventSound('server'));
}
soundCtx.muted = !v;
});
settings.watch('soundPack', (v, n, i) => {
const packs = soundCtx.packs;
if(!packs.has(v)) {
settings.delete(n);
return;
}
soundCtx.pack = packs.get(v);
if(!i) soundCtx.library.play(soundCtx.pack.getEventSound('server'));
});
settings.watch('soundVolume', v => {
soundCtx.volume = v / 100;
})
loadingOverlay.setMessage('Loading emoticons...');
try {
const emotes = await futami.getJson('emotes');
MamiEmotes.loadLegacy(emotes);
} catch(ex) {
console.error(ex);
}
const onHashChange = () => {
if(location.hash === '#reset') {
settings.clear(true);
location.assign('/');
}
};
window.addEventListener('hashchange', onHashChange);
onHashChange();
window.addEventListener('keydown', ev => {
if(ev.altKey && ev.shiftKey && (ev.key === 'R' || ev.key === 'r')) {
Umi.Server.close();
location.hash = 'reset';
}
});
let eeprom;
loadingOverlay.setMessage('Loading EEPROM...');
try {
await MamiEEPROM.init();
eeprom = new EEPROM('1', futami.get('eeprom2'), MamiMisuzuAuth.getLine);
} catch(ex) {
console.error(ex);
eeprom = undefined;
}
loadingOverlay.setMessage('Preparing UI...');
const textTriggers = new MamiTextTriggers;
// define this as late as possible'
const ctx = new MamiContext({
settings: settings,
views: views,
sound: soundCtx,
textTriggers: textTriggers,
eeprom: eeprom,
});
Object.defineProperty(window, 'mami', { enumerable: true, value: ctx });
// should be dynamic when possible
const layout = new Umi.UI.ChatLayout;
await views.unshift(layout);
Umi.UI.View.AccentReload();
Umi.UI.Hooks.AddHooks();
settings.watch('style', (v, n, i) => { if(!i) Umi.UI.View.AccentReload(); });
settings.watch('compactView', (v, n, i) => { if(!i) Umi.UI.View.AccentReload(); });
settings.watch('preventOverflow', v => document.body.classList.toggle('prevent-overflow', v));
settings.watch('tmpDisableOldThemeSys', (v, n, i) => { if(!i) Umi.UI.View.AccentReload(); });
settings.watch('minecraft', (v, n, i) => {
if(i && v === 'no')
return;
soundCtx.library.play((() => {
if(i)
return 'minecraft:nether:enter';
if(v === 'yes')
return 'minecraft:door:open';
if(v === 'old')
return 'minecraft:door:open-old';
return soundCtx.pack.getEventSound('join');
})());
});
settings.watch('enableNotifications', v => {
if(!v || !('Notification' in window)
|| (Notification.permission === 'granted' && Notification.permission !== 'denied'))
return;
Notification.requestPermission()
.then(perm => {
if(perm !== 'granted')
settings.set('enableNotifications', false);
});
});
settings.watch('playJokeSounds', v => {
if(!v) return;
if(!textTriggers.hasTriggers())
futami.getJson('texttriggers').then(trigInfos => textTriggers.addTriggers(trigInfos));
});
settings.watch('weeaboo', v => {
if(v) Weeaboo.init();
});
settings.watch('osuKeysV2', (v, n, i) => {
// migrate old value
if(i) {
if(settings.has('osuKeys')) {
settings.set('osuKeysV2', settings.get('osuKeys') ? 'yes' : 'no');
settings.delete('osuKeys');
return;
}
}
OsuKeys.setEnable(v !== 'no');
OsuKeys.setRandomRate(v === 'rng');
});
loadingOverlay.setMessage('Building menus...');
Umi.UI.Menus.Add('users', 'Users');
Umi.UI.Menus.Add('channels', 'Channels', !settings.get('showChannelList'));
Umi.UI.Menus.Add('settings', 'Settings');
let sidebarAnimation = null;
Umi.UI.Settings.Init();
Umi.UI.Toggles.Add('menu-toggle', {
'click': function() {
const sidebar = $c('sidebar')[0],
toggle = Umi.UI.Toggles.Get('menu-toggle'),
toggleOpened = 'sidebar__selector-mode--menu-toggle-opened',
toggleClosed = 'sidebar__selector-mode--menu-toggle-closed',
isClosed = toggle.classList.contains(toggleClosed);
if(sidebarAnimation !== null) {
sidebarAnimation.cancel();
sidebarAnimation = null;
}
if(isClosed) {
toggle.classList.add(toggleOpened);
toggle.classList.remove(toggleClosed);
} else {
toggle.classList.add(toggleClosed);
toggle.classList.remove(toggleOpened);
}
let update;
if(isClosed)
update = function(t) {
sidebar.style.width = (40 + (220 * t)).toString() + 'px';
};
else
update = function(t) {
sidebar.style.width = (260 - (220 * t)).toString() + 'px';
};
sidebarAnimation = MamiAnimate({
duration: 500,
easing: 'outExpo',
update: update,
});
}
}, 'Toggle Sidebar');
Umi.UI.Toggles.Get('menu-toggle').classList.add('sidebar__selector-mode--menu-toggle-opened');
Umi.UI.Toggles.Add('scroll', {
'click': function() {
settings.toggle('autoScroll');
}
}, 'Autoscroll');
settings.watch('autoScroll', function(value) {
Umi.UI.Toggles.Get('scroll').classList[value ? 'remove' : 'add']('sidebar__selector-mode--scroll-off');
});
if(window.innerWidth < 768)
Umi.UI.Toggles.Get('menu-toggle').click();
Umi.UI.Toggles.Add('audio', {
'click': function() {
settings.toggle('soundEnable');
}
}, 'Sounds');
settings.watch('soundEnable', function(value) {
Umi.UI.Toggles.Get('audio').classList[value ? 'remove' : 'add']('sidebar__selector-mode--audio-off');
});
Umi.UI.Toggles.Add('unembed', {
'click': function() {
const buttons = $qa('[data-embed="1"]');
for(const button of buttons)
button.click();
}
}, 'Unembed any embedded media');
Umi.UI.Toggles.Add('clear', {
'click': function() {
if(confirm('ARE YOU SURE ABOUT THAT???')) {
const limit = settings.get('explosionRadius');
const explode = $e({
tag: 'img',
attrs: {
src: '//static.flash.moe/images/explode.gif',
alt: '',
style: {
position: 'absolute',
zIndex: 9001,
bottom: 0,
right: 0,
pointerEvents: 'none',
},
onLoad: function() {
setTimeout(function(){
$r(explode);
}, 1700);
soundCtx.library.play('misc:explode');
},
},
});
document.body.appendChild(explode);
let backLog = Umi.Messages.All();
backLog = backLog.slice(Math.max(backLog.length - limit, 0));
Umi.Messages.Clear();
for(const blMsg of backLog)
Umi.Messages.Add(blMsg);
}
}
}, 'Clear Logs');
if(eeprom !== undefined) {
Umi.UI.Menus.Add('uploads', 'Upload History', !FUTAMI_DEBUG);
const doUpload = async file => {
const uploadEntry = Umi.UI.Uploads.create(file.name);
const uploadTask = eeprom.create(file);
uploadTask.onProgress(prog => uploadEntry.setProgress(prog.progress));
uploadEntry.addOption('Cancel', () => uploadTask.abort());
try {
const fileInfo = await uploadTask.start();
uploadEntry.hideOptions();
uploadEntry.clearOptions();
uploadEntry.removeProgress();
uploadEntry.addOption('Open', fileInfo.url);
uploadEntry.addOption('Insert', () => Umi.UI.Markup.InsertRaw(insertText, ''));
uploadEntry.addOption('Delete', () => {
eeprom.delete(fileInfo)
.then(() => uploadEntry.remove())
.catch(ex => {
console.error(ex);
alert(ex);
});
});
let insertText;
if(fileInfo.isImage()) {
insertText = `[img]${fileInfo.url}[/img]`;
uploadEntry.setThumbnail(fileInfo.thumb);
} else if(fileInfo.isAudio()) {
insertText = `[audio]${fileInfo.url}[/audio]`;
uploadEntry.setThumbnail(fileInfo.thumb);
} else if(fileInfo.isVideo()) {
insertText = `[video]${fileInfo.url}[/video]`;
uploadEntry.setThumbnail(fileInfo.thumb);
} else
insertText = location.protocol + fileInfo.url;
if(settings.get('eepromAutoInsert'))
Umi.UI.Markup.InsertRaw(insertText, '');
} catch(ex) {
if(!ex.aborted) {
console.error(ex);
alert(ex);
}
uploadEntry.remove();
}
};
const uploadForm = $e({
tag: 'input',
attrs: {
type: 'file',
multiple: true,
style: { display: 'none' },
onchange: ev => {
for(const file of ev.target.files)
doUpload(file);
},
},
});
document.body.appendChild(uploadForm);
Umi.UI.InputMenus.AddButton('upload', 'Upload', () => uploadForm.click());
$i('umi-msg-text').onpaste = ev => {
if(ev.clipboardData && ev.clipboardData.files.length > 0)
for(const file of ev.clipboardData.files)
doUpload(file);
};
document.body.ondragenter = ev => {
ev.preventDefault();
ev.stopPropagation();
};
document.body.ondragover = ev => {
ev.preventDefault();
ev.stopPropagation();
};
document.body.ondragleave = ev => {
ev.preventDefault();
ev.stopPropagation();
};
document.body.ondrop = ev => {
ev.preventDefault();
ev.stopPropagation();
if(ev.dataTransfer && ev.dataTransfer.files.length > 0)
for(const file of ev.dataTransfer.files) {
if(file.name.slice(-5) === '.mami'
&& confirm('This file appears to be a settings export. Do you want to import it? This will overwrite your existing settings!')) {
(new MamiSettingsBackup(settings)).importFile(file);
return;
}
doUpload(file);
}
};
}
Umi.UI.InputMenus.Add('markup', 'BB Code');
Umi.UI.InputMenus.Add('emotes', 'Emoticons');
window.addEventListener('beforeunload', function(ev) {
if(settings.get('closeTabConfirm')) {
ev.preventDefault();
return ev.returnValue = 'Are you sure you want to close the tab?';
}
});
// really not sure about all the watchers for the protocol just kinda being Listed here but we'll see i guess
loadingOverlay.setMessage('Connecting...');
const getLoadingOverlay = async (icon, header, message) => {
const currentView = views.current();
if('setIcon' in currentView) {
currentView.setIcon(icon);
currentView.setHeader(header);
currentView.setMessage(message);
return currentView;
}
const loading = new Umi.UI.LoadingOverlay(icon, header, message);
await views.push(loading);
return loading;
};
const wsCloseReasons = {
'_1000': 'The connection has been ended.',
'_1001': 'Something went wrong on the server side.',
'_1002': 'Your client sent broken data to the server.',
'_1003': 'Your client sent data to the server that it does not understand.',
'_1005': 'No additional information was provided.',
'_1006': 'You lost connection unexpectedly!',
'_1007': 'Your client sent broken data to the server.',
'_1008': 'Your client did something the server did not agree with.',
'_1009': 'Your client sent too much data to the server at once.',
'_1011': 'Something went wrong on the server side.',
'_1012': 'The server is restarting, reconnecting soon...',
'_1013': 'You cannot connect to the server right now, try again later.',
'_1015': 'Your client and the server could not establish a secure connection.',
};
const sessFailReasons = {
'authfail': 'Authentication failed.',
'sockfail': 'Too many active connections.',
'userfail': 'Name in use.',
'joinfail': 'You are banned.',
};
let dumpEvents = false;
settings.watch('dumpEvents', value => dumpEvents = value);
settings.watch('dumpPackets', value => Umi.Server.setDumpPackets(value));
Umi.Server.watch('conn:init', init => {
if(dumpEvents) console.log('conn:init', init);
let message = 'Connecting to server...';
if(init.attempt > 2)
message += ` (Attempt ${connectAttempts})`;
getLoadingOverlay('spinner', 'Loading...', message);
});
Umi.Server.watch('conn:ready', ready => {
if(dumpEvents) console.log('conn:ready', ready);
getLoadingOverlay('spinner', 'Loading...', 'Authenticating...');
});
Umi.Server.watch('conn:lost', lost => {
if(dumpEvents) console.log('conn:lost', lost);
getLoadingOverlay(
'unlink', 'Disconnected!',
wsCloseReasons[`_${lost.code}`] ?? `Something caused an unexpected connection loss. (${lost.code})`
);
});
Umi.Server.watch('conn:error', error => {
console.error('conn:error', error);
});
Umi.Server.watch('ping:send', send => {
if(dumpEvents) console.log('ping:send', send);
});
Umi.Server.watch('ping:long', long => {
if(dumpEvents) console.log('ping:long', long);
});
Umi.Server.watch('ping:recv', recv => {
if(dumpEvents) console.log('ping:recv', recv);
});
Umi.Server.watch('session:start', start => {
if(dumpEvents) console.log('session:start', start);
const userInfo = new Umi.User(start.user.id, start.user.name, start.user.colour, start.user.permsRaw);
Umi.User.setCurrentUser(userInfo);
Umi.Users.Add(userInfo);
Umi.UI.Markup.Reset();
Umi.UI.Emoticons.Init();
Umi.Parsing.Init();
views.pop(ctx => MamiAnimate({
async: true,
duration: 120,
easing: 'inOutSine',
start: () => {
ctx.toElem.style.zIndex = '100';
ctx.fromElem.style.pointerEvents = 'none';
ctx.fromElem.style.zIndex = '200';
},
update: t => {
ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`;
ctx.fromElem.style.opacity = 1 - (1 * t).toString();
},
end: () => {
ctx.toElem.style.zIndex = null;
},
}));
});
Umi.Server.watch('session:fail', fail => {
if(dumpEvents) console.log('session:fail', fail);
if(fail.baka !== undefined) {
new MamiForceDisconnectNotice(fail.baka).pushOn(views);
return;
}
getLoadingOverlay(
'cross', 'Failed!',
sessFailReasons[fail.session.reason] ?? `Unknown reason: ${fail.session.reason}`
);
if(fail.session.needsAuth)
setTimeout(() => location.assign(futami.get('login')), 1000);
});
Umi.Server.watch('session:term', term => {
if(dumpEvents) console.log('session:term', term);
new MamiForceDisconnectNotice(term.baka).pushOn(views);
});
Umi.Server.watch('user:add', add => {
if(dumpEvents) console.log('user:add', add);
if(add.user.self)
return;
const userInfo = new Umi.User(add.user.id, add.user.name, add.user.colour, add.user.permsRaw);
Umi.Users.Add(userInfo);
if(add.msg !== undefined)
Umi.Messages.Add(new Umi.Message(
add.msg.id, add.msg.time, undefined, '', add.msg.channel, false,
{
isError: false,
type: add.msg.botInfo.type,
args: add.msg.botInfo.args,
target: userInfo,
}
));
});
Umi.Server.watch('user:remove', remove => {
if(dumpEvents) console.log('user:remove', remove);
const userInfo = Umi.Users.Get(remove.user.id);
if(userInfo === null)
return;
if(remove.msg !== undefined)
Umi.Messages.Add(new Umi.Message(
remove.msg.id,
remove.msg.time,
undefined,
'',
remove.msg.channel,
false,
{
isError: false,
type: remove.msg.botInfo.type,
args: remove.msg.botInfo.args,
target: userInfo,
},
));
Umi.Users.Remove(userInfo);
});
Umi.Server.watch('user:update', update => {
if(dumpEvents) console.log('user:update', update);
const userInfo = Umi.Users.Get(update.user.id);
userInfo.setName(update.user.name);
userInfo.setColour(update.user.colour);
userInfo.setPermissions(update.user.permsRaw);
Umi.Users.Update(userInfo.getId(), userInfo);
});
Umi.Server.watch('user:clear', () => {
if(dumpEvents) console.log('user:clear');
const self = Umi.User.currentUser;
Umi.Users.Clear();
if(self !== undefined)
Umi.Users.Add(self);
});
Umi.Server.watch('chan:add', add => {
if(dumpEvents) console.log('chan:add', add);
Umi.Channels.Add(new Umi.Channel(
add.channel.name,
add.channel.hasPassword,
add.channel.isTemporary,
));
});
Umi.Server.watch('chan:remove', remove => {
if(dumpEvents) console.log('chan:remove', remove);
Umi.Channels.Remove(Umi.Channels.Get(remove.channel.name));
});
Umi.Server.watch('chan:update', update => {
if(dumpEvents) console.log('chan:update', update);
const chanInfo = Umi.Channels.Get(update.channel.previousName);
chanInfo.setName(update.channel.name);
chanInfo.setHasPassword(update.channel.hasPassword);
chanInfo.setTemporary(update.channel.isTemporary);
Umi.Channels.Update(update.channel.previousName, chanInfo);
});
Umi.Server.watch('chan:clear', () => {
if(dumpEvents) console.log('chan:clear');
Umi.Channels.Clear();
});
Umi.Server.watch('chan:focus', focus => {
if(dumpEvents) console.log('chan:focus', focus);
Umi.Channels.Switch(Umi.Channels.Get(focus.channel.name));
});
Umi.Server.watch('chan:join', join => {
if(dumpEvents) console.log('chan:join', join);
const userInfo = new Umi.User(join.user.id, join.user.name, join.user.colour, join.user.permsRaw);
Umi.Users.Add(userInfo);
if(join.msg !== undefined)
Umi.Messages.Add(new Umi.Message(
join.msg.id, null, undefined, '', join.msg.channel, false,
{
isError: false,
type: leave.msg.botInfo.type,
args: [ userInfo.getName() ],
target: userInfo,
},
));
});
Umi.Server.watch('chan:leave', leave => {
if(dumpEvents) console.log('chan:leave', leave);
if(leave.user.self)
return;
const userInfo = Umi.Users.Get(leave.user.id);
if(userInfo === null)
return;
if(leave.msg !== undefined)
Umi.Messages.Add(new Umi.Message(
leave.msg.id, null, undefined, '', leave.msg.channel, false,
{
isError: false,
type: leave.msg.botInfo.type,
args: [ userInfo.getName() ],
target: userInfo,
},
));
Umi.Users.Remove(userInfo);
});
Umi.Server.watch('msg:add', add => {
if(dumpEvents) console.log('msg:add', add);
const senderInfo = add.msg.sender;
const userInfo = senderInfo.name === undefined
? Umi.Users.Get(senderInfo.id)
: new Umi.User(senderInfo.id, senderInfo.name, senderInfo.colour, senderInfo.permsRaw);
// hack
let channelName = add.msg.channel;
if(channelName !== undefined && channelName.startsWith('@~')) {
const chanUserInfo = Umi.Users.Get(channelName.substring(2));
if(chanUserInfo !== null)
channelName = `@${chanUserInfo.getName()}`;
}
// also hack
if(add.msg.flags.isPM) {
if(Umi.Channels.Get(channelName) === null)
Umi.Channels.Add(new Umi.Channel(channelName, false, true, true));
// this should be raised for other channels too, but that is not possible yet
Umi.UI.Menus.Attention('channels');
}
Umi.Messages.Add(new Umi.Message(
add.msg.id,
add.msg.time,
userInfo,
add.msg.text,
channelName,
false,
add.msg.botInfo,
add.msg.flags.isAction,
add.msg.silent,
));
});
Umi.Server.watch('msg:remove', remove => {
if(dumpEvents) console.log('msg:remove', remove);
Umi.Messages.Remove(Umi.Messages.Get(remove.msg.id));
});
Umi.Server.watch('msg:clear', () => {
if(dumpEvents) console.log('msg:clear');
Umi.UI.Messages.RemoveAll();
});
Umi.Server.open();
if(window.dispatchEvent)
window.dispatchEvent(new Event('umi:connect'));
})();
#include compat.js