mami/src/mami.js/main.js

1000 lines
38 KiB
JavaScript

window.Umi = { UI: {} };
#include array.js
#include html.js
#include uniqstr.js
#include xhr.js
#include animate.js
#include args.js
#include awaitable.js
#include common.js
#include compat.js
#include conman.js
#include context.js
#include emotes.js
#include events.js
#include mobile.js
#include mszauth.js
#include parsing.js
#include themes.js
#include txtrigs.js
#include users.js
#include weeb.js
#include audio/autoplay.js
#include chatform/form.jsx
#include colpick/picker.jsx
#include controls/msgbox.jsx
#include controls/ping.jsx
#include controls/views.js
#include eeprom/eeprom.js
#include eeprom/eepromv2.js
#include emotes/picker.jsx
#include notices/baka.jsx
#include notices/youare.jsx
#include settings/backup.js
#include settings/settings.js
#include sidebar/act-clear-backlog.jsx
#include sidebar/act-collapse-all.jsx
#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
#include sidebar/pan-users.jsx
#include sidebar/sidebar.jsx
#include sockchat/client.js
#include sockchat/handlers.js
#include sound/context.js
#include sound/osukeys.js
#include sound/sndtest.jsx
#include ui/chat-layout.js
#include ui/emotes.js
#include ui/loading-overlay.jsx
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();
});
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: 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.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;
}
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();
settings.define('compactView').default(false).create();
settings.define('autoScroll').default(true).create();
settings.define('closeTabConfirm').default(false).create();
settings.define('autoCloseUserContext').default(true).create();
settings.define('preventOverflow').default(false).create();
settings.define('expandTextBox').default(false).create();
settings.define('eepromAutoInsert').default(true).create();
settings.define('autoEmbedV1').default(false).create();
settings.define('autoEmbedPlay').default(false).create();
settings.define('soundEnable').default(true).critical().create();
settings.define('soundPack').default('').create();
settings.define('soundVolume').default(80).min(0).max(100).create();
settings.define('soundEnableJoin').default(true).create();
settings.define('soundEnableLeave').default(true).create();
settings.define('soundEnableError').default(true).create();
settings.define('soundEnableServer').default(true).create();
settings.define('soundEnableIncoming').default(true).create();
settings.define('onlySoundOnMention').default(false).create();
settings.define('soundEnableOutgoing').default(true).create();
settings.define('soundEnablePrivate').default(true).create();
settings.define('soundEnableForceLeave').default(true).create();
settings.define('minecraft').type(['no', 'yes', 'old']).default('no').create();
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();
settings.define('motivationalVideos').default(false).create();
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(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();
settings.define('showMarkupSelector').type(['always', 'focus', 'never']).default('always').create();
settings.define('dbgEEPROMv2').default(false).create();
const noNotifSupport = !('Notification' in window);
settings.define('enableNotifications').default(false).immutable(noNotifSupport).critical().create();
settings.define('notificationShowMessage').default(false).immutable(noNotifSupport).create();
settings.define('notificationTriggers').default('').immutable(noNotifSupport).create();
loadingOverlay.message = 'Loading sounds...';
const soundCtx = new MamiSoundContext;
ctx.sound = soundCtx;
futami.getJson('sounds2')
.catch(ex => { console.error('Failed to load sound library and packs.', ex); })
.then(sounds => {
if(Array.isArray(sounds.library))
soundCtx.library.register(sounds.library, true);
if(Array.isArray(sounds.packs)) {
soundCtx.packs.register(sounds.packs, true);
settings.touch('soundPack', true);
}
});
MamiDetectAutoPlay()
.then(canAutoPlay => {
if(canAutoPlay) return;
settings.set('soundEnable', false);
settings.virtualise('soundEnable');
});
settings.watch('soundEnable', ev => {
if(ev.detail.value) {
if(!soundCtx.ready)
soundCtx.reset();
settings.touch('soundVolume');
settings.touch('soundPack', true);
// do we need to do this?
if(!ev.detail.initial && !ev.detail.silent && ev.detail.local)
soundCtx.library.play(soundCtx.pack.getEventSound('server'));
}
soundCtx.muted = !ev.detail.value;
});
settings.watch('soundPack', ev => {
const packs = soundCtx.packs;
let packName = ev.detail.value;
if(packName === '') {
const names = packs.names;
if(names.length < 1)
return;
packName = names[0];
} else if(!packs.has(packName))
return;
soundCtx.pack = packs.get(packName);
if(!ev.detail.initial && !ev.detail.silent && ev.detail.local)
soundCtx.library.play(soundCtx.pack.getEventSound('server'));
});
settings.watch('soundVolume', ev => {
soundCtx.volume = ev.detail.value / 100;
})
// loading these asynchronously makes them not show up in the backlog
// revisit when emote reparsing is implemented
loadingOverlay.message = 'Loading emoticons...';
try {
MamiEmotes.load(await futami.getApiJson('/v1/emotes'));
} catch(ex) {
console.error('Failed to load emoticons.', 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'))
location.hash = 'reset';
});
loadingOverlay.message = 'Preparing UI...';
ctx.textTriggers = new MamiTextTriggers;
const sidebar = new MamiSidebar;
MamiCompat('Umi.UI.Menus.Add', {
value: (baseId, title) => {
sidebar.createPanel({
name: `compat:${baseId}`,
text: title,
createdButton: button => {
button.element.id = `umi-menu-icons-${baseId}`;
button.element.append($element('div', { className: `sidebar__selector-mode--${baseId}` }));
},
element: $element('div', { 'class': `sidebar__menu--${baseId}`, id: `umi-menus-${baseId}` }),
});
}
});
MamiCompat('Umi.UI.Menus.Get', {
value: (baseId, icon) => {
const info = sidebar[icon ? 'getButton' : 'getPanel'](`compat:${baseId}`);
if(info === undefined)
return null;
return icon ? info.element : info.element.firstElementChild;
},
});
MamiCompat('Umi.UI.Menus.Attention', {
value: baseId => {
sidebar.getButton(`compat:${baseId}`)?.attention();
},
});
const chatForm = new MamiChatForm(ctx.events.scopeTo('form'));
window.addEventListener('keydown', ev => {
if((ev.ctrlKey && ev.key !== 'v') || ev.altKey)
return;
if(!ev.target.matches('input, textarea, select, button'))
chatForm.focus();
});
settings.watch('showMarkupSelector', ev => {
chatForm.markup.visible = ev.detail.value !== 'never';
Umi.UI.Messages.ScrollIfNeeded(chatForm.markup.height);
});
let colourPicker, colourPickerVisible = false;
for(const bbCode of UmiBBCodes)
if(bbCode.text !== undefined)
chatForm.markup.createButton({
title: bbCode.text,
style: bbCode.style,
onclick: ev => {
if(bbCode.tag === 'color') {
if(colourPicker === undefined) {
colourPicker = new MamiColourPicker({ presets: futami.get('colours') });
layout.element.appendChild(colourPicker.element);
}
if(colourPickerVisible) {
colourPicker.close();
} else {
colourPickerVisible = true;
colourPicker.dialog(ev)
.then(colour => { chatForm.input.insertAroundSelection(`[${bbCode.tag}=${MamiColour.hex(colour)}]`, `[/${bbCode.tag}]`) })
.catch(() => {}) // noop so the console stops screaming
.finally(() => colourPickerVisible = false);
}
} else
chatForm.input.insertAroundSelection(`[${bbCode.tag}${(bbCode.arg ? '=' : '')}]`, `[/${bbCode.tag}]`);
},
});
const layout = new Umi.UI.ChatLayout(chatForm, sidebar);
await ctx.views.unshift(layout);
ctx.events.watch('form:resize', ev => {
Umi.UI.Messages.ScrollIfNeeded(ev.detail.diffHeight);
});
settings.watch('style', ev => {
for(const className of layout.element.classList)
if(className.startsWith('umi--'))
layout.element.classList.remove(className);
layout.element.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);
});
settings.watch('preventOverflow', ev => { args.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('newLineOnEnter', ev => { chatForm.input.newLineOnEnter = ev.detail.value; });
settings.watch('expandTextBox', ev => { chatForm.input.growInputField = ev.detail.value; });
settings.watch('minecraft', ev => {
if(ev.detail.initial && ev.detail.value === 'no')
return;
soundCtx.library.play((() => {
if(ev.detail.initial)
return 'minecraft:nether:enter';
if(ev.detail.value === 'yes')
return 'minecraft:door:open';
if(ev.detail.value === 'old')
return 'minecraft:door:open-old';
return soundCtx.pack.getEventSound('join');
})());
});
settings.watch('enableNotifications', ev => {
if(!ev.detail.value || !('Notification' in window)
|| (Notification.permission === 'granted' && Notification.permission !== 'denied'))
return;
Notification.requestPermission()
.then(perm => {
if(perm !== 'granted')
settings.set('enableNotifications', false);
});
});
settings.watch('playJokeSounds', ev => {
if(!ev.detail.value) return;
if(!ctx.textTriggers.hasTriggers)
futami.getJson('texttriggers').then(trigInfos => ctx.textTriggers.addTriggers(trigInfos));
});
settings.watch('weeaboo', ev => {
if(ev.detail.value) Weeaboo.init();
});
settings.watch('osuKeysV2', ev => {
// migrate old value
if(ev.detail.initial) {
if(settings.has('osuKeys')) {
settings.set('osuKeysV2', settings.get('osuKeys') ? 'yes' : 'no');
settings.delete('osuKeys');
return;
}
}
OsuKeys.enable = ev.detail.value !== 'no';
OsuKeys.randomRate = ev.detail.value === 'rng';
});
loadingOverlay.message = 'Building menus...';
MamiCompat('Umi.Parser.SockChatBBcode.EmbedStub', { value: () => {} }); // intentionally a no-op
MamiCompat('Umi.UI.View.SetText', { value: text => { chatForm.input.setText(text); } });
const sbUsers = new MamiSidebarPanelUsers;
sidebar.createPanel(sbUsers);
sbUsers.addOption({
name: 'profile',
text: 'View profile',
onclick: entry => window.open(futami.get('profile').replace('{user:id}', entry.id), '_blank'),
});
sbUsers.addOption({
name: 'action',
text: 'Describe action',
condition: entry => Umi.User.getCurrentUser()?.id === entry.id,
onclick: entry => { chatForm.input.setText('/me '); },
});
sbUsers.addOption({
name: 'nick',
text: 'Set nickname',
condition: entry => Umi.User.getCurrentUser()?.id === entry.id && Umi.User.getCurrentUser().perms.canSetNick,
onclick: entry => { chatForm.input.setText('/nick '); },
});
sbUsers.addOption({
name: 'bans',
text: 'View bans',
condition: entry => Umi.User.getCurrentUser()?.id === entry.id && Umi.User.getCurrentUser().perms.canKick,
onclick: entry => { Umi.Server.sendMessage('/bans'); },
});
sbUsers.addOption({
name: 'dm',
text: 'Send direct message',
condition: entry => Umi.User.getCurrentUser()?.id !== entry.id,
onclick: entry => { chatForm.input.setText(`/msg ${entry.name} `); },
});
sbUsers.addOption({
name: 'kick',
text: 'Kick from chat',
condition: entry => Umi.User.hasCurrentUser() && Umi.User.getCurrentUser().id !== entry.id && Umi.User.getCurrentUser().perms.canKick,
onclick: entry => { chatForm.input.setText(`/kick ${entry.name} `); },
});
sbUsers.addOption({
name: 'ipaddr',
text: 'View IP address',
condition: entry => Umi.User.hasCurrentUser() && Umi.User.getCurrentUser().id !== entry.id && Umi.User.getCurrentUser().perms.canKick,
onclick: entry => { Umi.Server.sendMessage(`/ip ${entry.name}`); },
});
const sbChannels = new MamiSidebarPanelChannels;
sidebar.createPanel(sbChannels);
const sbSettings = new MamiSidebarPanelSettings(settings);
sidebar.createPanel(sbSettings);
sbSettings.category(category => {
category.header('Interface');
category.setting('style').title('Theme').type('select').options(() => {
const themes = {};
for(const theme of UmiThemes)
themes[theme.id] = theme.name;
return themes;
}).done();
category.setting('compactView').title('Use compact view').done();
category.setting('autoScroll').title('Enable auto scroll').done();
category.setting('closeTabConfirm').title('Confirm tab close').done();
category.setting('autoCloseUserContext').title('Auto-close user menus').done();
category.setting('doNotMarkLinksAsVisited').title("Don't mark links as visited").done();
});
sbSettings.category(category => {
category.header('Text');
category.setting('preventOverflow').title('Prevent overflow').done();
category.setting('expandTextBox').title('Grow input box while typing').done();
category.setting('eepromAutoInsert').title('Auto-insert uploads').done();
category.setting('autoEmbedV1').title('Auto-embed media').done();
category.setting('autoEmbedPlay').title('Auto-play embedded media').done();
category.setting('keepEmotePickerOpen').title('Keep emoticon picker open').done();
category.setting('newLineOnEnter').title('Swap Enter and Shift+Enter behaviour').done();
category.setting('showMarkupSelector').title('Show markup buttons').type('checkbox').on('always').off('never').done();
});
sbSettings.category(category => {
category.header('Notifications');
category.setting('flashTitle').title('Strobe title on new message').done();
category.setting('enableNotifications').title('Show notifications').done();
category.setting('notificationShowMessage').title('Show contents of message').done();
category.setting('notificationTriggers').title('Triggers').done();
});
sbSettings.category(category => {
category.header('Sound');
category.setting('soundEnable').title('Enable sound').done();
category.setting('soundPack').title('Sound pack').type('select').options(() => {
const options = { '': 'Default' };
for(const name of soundCtx.packs.names)
options[name] = soundCtx.packs.info(name).title;
return options;
}).done();
category.setting('soundVolume').title('Sound volume').type('range').done();
category.setting('soundEnableJoin').title('Play join sound').done();
category.setting('soundEnableLeave').title('Play leave sound').done();
category.setting('soundEnableError').title('Play error sound').done();
category.setting('soundEnableServer').title('Play server message sound').done();
category.setting('soundEnableIncoming').title('Play receive message sound').done();
category.setting('onlySoundOnMention').title('Only plays sounds when you are mentioned').done();
category.setting('soundEnableOutgoing').title('Play send message sound').done();
category.setting('soundEnablePrivate').title('Play private message sound').done();
category.setting('soundEnableForceLeave').title('Play kick sound').done();
category.setting('minecraft').title('Minecraft').type('select').options(() => {
return {
'no': 'No Minecraft',
'yes': 'Yes Minecraft',
'old': 'Old Minecraft',
};
}).done();
category.setting('windowsLiveMessenger').title('Windows Live Messenger').done();
category.setting('seinfeld').title('Seinfeld').done();
});
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();
category.setting('motivationalVideos').title('Make videos motivational').done();
category.setting('osuKeysV2').title('osu! keyboard sounds').type('select').options(() => {
return {
'no': 'Off',
'yes': 'On',
'rng': 'On, random pitch',
};
}).done();
category.setting('explosionRadius').title('Messages to keep on clear').done();
});
sbSettings.category(category => {
category.header('Actions');
category.button('Open compatibility client', () => { window.open(window.AMI_URL, '_blank', 'noopener'); });
category.button('Manual reconnect', async button => {
const textOrig = button.textContent;
let lock = 10;
button.disabled = true;
button.textContent = 'Reconnecting...';
try {
await ctx.conMan.start();
while(--lock > 0) {
button.textContent = textOrig + ` (${lock}s)`;
await MamiSleep(1000);
}
} finally {
button.textContent = textOrig;
button.disabled = false;
}
});
category.button('Reload emoticons', async button => {
const textOrig = button.textContent;
button.disabled = true;
button.textContent = 'Reloading emoticons...';
try {
const emotes = await futami.getApiJson('/v1/emotes', true);
MamiEmotes.clear();
MamiEmotes.load(emotes);
} finally {
button.textContent = textOrig;
button.disabled = false;
}
});
category.button('Reload sound library', async button => {
const textOrig = button.textContent;
button.disabled = true;
button.textContent = 'Reloading sound library...';
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);
settings.touch('soundPack', true);
}
} finally {
button.textContent = textOrig;
button.disabled = false;
}
});
category.button('Reload joke triggers', async button => {
const textOrig = button.textContent;
button.disabled = true;
button.textContent = 'Reloading joke triggers...';
try {
const triggers = await futami.getJson('texttriggers', true);
ctx.textTriggers.clearTriggers();
ctx.textTriggers.addTriggers(triggers)
} finally {
button.textContent = textOrig;
button.disabled = false;
}
});
});
sbSettings.category(category => {
category.header('Settings');
category.button('Import settings', () => {
(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();
let fileName;
if(user !== null)
fileName = `${user.name}'s settings.mami`;
(new MamiSettingsBackup(settings)).exportDownload(args.parent, fileName);
});
category.button('Reset settings', () => {
settings.clear();
}, ['This will reset all your settings to their defaults values.', 'Are you sure you want to do this?']);
});
sbSettings.category(category => {
category.header('Debug');
category.collapse();
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('dbgEEPROMv2').title('Use EEPROM v2 API (requires reload)').done();
category.setting('marqueeAllNames').title('Apply marquee on everyone (requires reload)').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' }));
await MamiSleep(5000);
await ctx.views.pop();
button.disabled = false;
});
category.button('You are an idiot!', async button => {
button.disabled = true;
await ctx.views.push(new MamiYouAreAnIdiot(soundCtx.library, ctx.views));
button.disabled = false;
});
category.button('Sound test', async (button, ev) => {
button.disabled = true;
await ctx.views.push(new MamiSoundTest(
settings,
soundCtx.audio,
soundCtx.manager,
soundCtx.library,
[ev.clientX, ev.clientY],
));
button.disabled = false;
});
category.button('Reset audio context', () => {
soundCtx.reset();
});
});
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));
const pingIndicator = new MamiPingIndicator;
const sbActPing = new MamiSidebarActionPing(pingIndicator, ctx.msgbox);
sidebar.createAction(sbActPing);
let emotePicker, emotePickerVisible = false;
chatForm.input.createButton({
title: 'Emoticons',
icon: 'fas fa-smile',
onclick: (ev, button) => {
if(emotePicker === undefined) {
emotePicker = new MamiEmotePicker({
onClose: () => { emotePickerVisible = false; },
onPick: emote => {
chatForm.input.insertAtCursor(`:${emote.strings[0]}:`);
},
getEmotes: () => MamiEmotes.all(Umi.User.getCurrentUser().perms.rank),
setKeepOpenOnPick: value => { settings.set('keepEmotePickerOpen', value); },
});
layout.element.appendChild(emotePicker.element);
settings.watch('keepEmotePickerOpen', ev => { emotePicker.keepOpenOnPick = ev.detail.value; });
}
if(emotePickerVisible) {
emotePicker.close();
} else {
emotePickerVisible = true;
emotePicker.dialog(button);
}
},
});
ctx.eeprom = settings.get('dbgEEPROMv2')
? new MamiEEPROMv2(futami.get('api'), MamiMisuzuAuth.getLine, 'chat')
: new MamiEEPROM(futami.get('eeprom2'), MamiMisuzuAuth.getLine);
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;
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);
},
});
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] });
}
},
});
const doUpload = async file => {
const entry = sbUploads.createEntry(file);
const task = ctx.eeprom.create(file);
entry.addOption({
name: 'cancel',
text: 'Cancel upload',
onclick: () => { task.abort(); },
});
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);
},
});
args.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
//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;
ev.preventDefault();
for(const file of ev.dataTransfer.files) {
if(file.name.slice(-5) === '.mami') {
ctx.msgbox.show({
body: [
'This file appears to be a settings export.',
'Do you want to import it? This will overwrite your existing settings!',
],
yes: true,
no: true,
})
.then(() => {
(new MamiSettingsBackup(settings)).importFile(file);
})
.catch(() => {
if(doUpload !== undefined)
doUpload(file);
});
} else if(doUpload !== undefined)
doUpload(file);
}
});
window.addEventListener('beforeunload', function(ev) {
if(settings.get('closeTabConfirm')) {
ev.preventDefault();
return ev.returnValue = 'Are you sure you want to close the tab?';
}
ctx.isUnloading = true;
});
loadingOverlay.message = 'Connecting...';
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, header, message);
await ctx.views.push(loading);
}
};
const sockChat = new MamiSockChat(ctx.events.scopeTo('sockchat'));
const conMan = new MamiConnectionManager(sockChat, settings, futami.get('servers'), ctx.events.scopeTo('conn'));
ctx.conMan = conMan;
ctx.events.watch('form:send', ev => {
sockChat.client.sendMessage(ev.detail.text);
});
sbChannels.onClickEntry = async info => {
await sockChat.client.switchChannel(info);
// hack for DM channels
if(info.isUserChannel) {
sbChannels.setActiveEntry(info.name);
Umi.UI.Messages.SwitchChannel(info);
}
};
let sockChatRestarting;
const sockChatReconnect = () => {
if(conMan.isActive)
return;
sbActPing.pingMs = -1;
pingIndicator.strength = -1;
const reconManAttempt = ev => {
if(sockChatRestarting || ev.detail.delay > 2000)
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', 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);
conMan.unwatch('attempt', reconManAttempt);
conMan.unwatch('fail', reconManFail);
};
conMan.watch('attempt', reconManAttempt);
conMan.watch('fail', reconManFail);
conMan.watch('success', reconManSuccess);
conMan.start();
};
const sockChatHandlers = new MamiSockChatHandlers(
ctx, sockChat, setLoadingOverlay, sockChatReconnect, pingIndicator,
sbActPing, sbChannels, sbUsers
);
settings.watch('dumpEvents', ev => sockChatHandlers.setDumpEvents(ev.detail.value));
settings.watch('dumpPackets', ev => sockChat.setDumpPackets(ev.detail.value));
sockChatHandlers.register();
const conManAttempt = ev => {
let message = ev.detail.attempt > 2 ? `Attempt ${ev.detail.attempt}...` : 'Connecting to server...';
setLoadingOverlay('spinner', 'Connecting...', message);
};
const conManFail = ev => {
setLoadingOverlay('cross', 'Failed to connect', `Retrying in ${ev.detail.delay / 1000} seconds...`);
};
const conManSuccess = () => {
conMan.unwatch('success', conManSuccess);
conMan.unwatch('attempt', conManAttempt);
conMan.unwatch('fail', conManFail);
};
conMan.watch('success', conManSuccess);
conMan.watch('attempt', conManAttempt);
conMan.watch('fail', conManFail);
await sockChat.create();
conMan.client = sockChat;
await conMan.start();
return ctx;
};
(() => {
const eventTarget = new MamiEventTargetWindow;
Object.defineProperty(window, 'mamiEventTarget', { enumerable: true, value: eventTarget });
MamiInit({
eventTarget: eventTarget,
}).then(mami => {
//Object.defineProperty(window, 'mami', { enumerable: true, value: mami });
});
})();
const MamiDbgCreateFloatingInstance = async () => {
if(!FUTAMI_DEBUG)
return;
const prefix = $rngs(8);
const parent = $element('div', {
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),
});
};