Rewrote code for the input box.

This commit is contained in:
flash 2024-07-01 21:38:07 +00:00
parent cd16a5d1b5
commit 46549845b9
24 changed files with 483 additions and 584 deletions

View file

@ -5,12 +5,12 @@
.input__main { .input__main {
display: flex; display: flex;
height: 40px;
max-height: 140px;
} }
.input__text { .input__text {
flex-grow: 1; flex-grow: 1;
height: 40px;
max-height: 140px;
resize: none; resize: none;
} }
@ -32,37 +32,15 @@
cursor: pointer cursor: pointer
} }
.input__button:before {
font-family: "Font Awesome 5 Free";
font-weight: 900
}
.input__button--markup:before {
content: "\f121"
}
.input__button--emotes:before {
content: "\f118"
}
.input__button--upload:before {
content: "\f574"
}
.input__button--send:before {
content: "\f1d8"
}
.input__menus { .input__menus {
max-height: 100px;
overflow: auto
} }
.input__menu { .input__menu {
display: flex;
flex-wrap: wrap;
gap: 2px;
padding: 2px;
}
.input__menu:empty {
display: none; display: none;
padding: 1px
}
.input__menu--active {
display: block
} }

View file

@ -37,6 +37,7 @@ a:hover {
.hidden { .hidden {
display: none !important; display: none !important;
visibility: none !important;
} }
.sjis { .sjis {

View file

@ -1,21 +1,22 @@
.markup__button { .markup__button {
border: 0; border: 0;
background: transparent; background: transparent;
font-family: Verdana, Tahoma, Geneva, Arial, Helvetica, sans-serif; font-family: inherit;
cursor: pointer; cursor: pointer;
padding: 4px 8px; padding: 4px 8px;
margin: 2px; transition: background .1s;
transition: background .1s min-width: 26px;
text-align: center;
} }
.markup__link { .markup__link {
color: #1e90ff; color: #1e90ff;
text-decoration: none text-decoration: none;
} }
.markup__link:hover, .markup__link:hover,
.markup__link:focus { .markup__link:focus {
text-decoration: underline text-decoration: underline;
} }
.chat:not(.mami-do-not-mark-links-as-visited) .markup__link--visited, .chat:not(.mami-do-not-mark-links-as-visited) .markup__link--visited,

View file

@ -0,0 +1,30 @@
#include chatform/input.jsx
#include chatform/markup.jsx
const MamiChatForm = function(eventTarget) {
const input = new MamiChatFormInput(eventTarget);
const markup = new MamiChatFormMarkup;
const html = <form class="input" onsubmit={ev => {
ev.preventDefault();
const text = input.text;
input.text = '';
input.updateGrowHeight();
eventTarget.dispatch('send', { text: text });
}}>
{markup}
{input}
</form>;
return {
get element() { return html; },
get markup() { return markup; },
get input() { return input; },
focus: () => {
input.focus();
},
};
};

View file

@ -0,0 +1,174 @@
#include emotes.js
#include users.js
const MamiChatFormInput = function(eventTarget) {
const textElem = <textarea class="input__text" name="text" autofocus="autofocus"/>;
let submitButton;
const html = <div class="input__main">
{textElem}
</div>;
const createButton = (info, submit) => {
const button = <button class="input__button" title={info.title}>
<i class={info.icon} />
</button>;
if(submit) {
button.type = 'submit';
button.classList.add('input__button--send');
} else {
button.type = 'button';
button.onclick = ev => { info.onclick(ev, button, info); };
}
html.insertBefore(button, submitButton);
if(submit)
submitButton = button;
};
createButton({
title: 'Send',
icon: 'fas fa-paper-plane',
}, true);
let growInputField = false;
let growLastHeight, growBaseHeight;
const updateGrowHeight = () => {
if(growBaseHeight === undefined)
growBaseHeight = textElem.getBoundingClientRect().height;
let height = growBaseHeight;
if(growInputField && textElem.scrollHeight > textElem.clientHeight)
height = textElem.scrollHeight;
textElem.style.height = height > growBaseHeight ? `${height}px` : null;
height = textElem.getBoundingClientRect().height;
if(growLastHeight !== height) {
growLastHeight = height;
eventTarget.dispatch('resize', {
height: height,
baseHeight: growBaseHeight,
diffHeight: height - growBaseHeight,
});
}
};
textElem.addEventListener('keyup', () => { updateGrowHeight(); });
let newLineOnEnter = false;
textElem.addEventListener('keydown', ev => {
if(ev.key === 'Tab' && !ev.shiftKey && !ev.ctrlKey) {
ev.preventDefault();
const text = textElem.value;
if(text.length < 1)
return;
const start = textElem.selectionStart;
let position = start;
let snippet = '';
while(position >= 0 && text.charAt(position - 1) !== ' ' && text.charAt(position - 1) !== "\n") {
--position;
snippet = text.charAt(position) + snippet;
}
let insertText;
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]}:`;
} 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;
}
return;
}
if(ev.key === 'Enter' && ev.shiftKey === newLineOnEnter) {
ev.preventDefault();
textElem.form.requestSubmit();
return;
}
});
textElem.addEventListener('paste', ev => {
if(ev.clipboardData?.files.length > 0)
eventTarget.dispatch('upload', { files: ev.clipboardData.files });
});
return {
get element() { return html; },
get text() { return textElem.value; },
set text(value) {
textElem.value = value;
},
get newLineOnEnter() { return newLineOnEnter; },
set newLineOnEnter(value) {
newLineOnEnter = !!value;
},
get growInputField() { return growInputField; },
set growInputField(value) {
growInputField = !!value;
},
createButton: info => createButton(info),
updateGrowHeight: updateGrowHeight,
setText: text => { // this one also focusses!!
if(typeof text !== 'string')
throw 'text must be string';
textElem.value = text;
textElem.focus();
},
insertAtCursor: text => {
if(typeof text !== 'string')
throw 'text must be string';
const value = textElem.value;
let start = textElem.selectionStart;
let end = textElem.selectionEnd;
textElem.value = value.slice(0, start) + text + value.slice(end);
textElem.selectionEnd = textElem.selectionStart = start + text.length;
textElem.focus();
},
insertAroundSelection: (before = '', after = '') => {
if(typeof before !== 'string')
throw 'before must be string';
if(typeof after !== 'string')
throw 'after must be string';
const start = textElem.selectionStart;
const end = textElem.selectionEnd;
const length = end - start;
const value = textElem.value;
textElem.value = value.slice(0, start) + before + value.slice(start, end) + after + value.slice(end);
textElem.selectionStart = start + before.length;
textElem.selectionEnd = end + before.length;
textElem.focus();
},
focus: () => {
textElem.focus();
},
};
};

View file

@ -0,0 +1,19 @@
const MamiChatFormMarkup = function(input) {
const buttons = <div class="input__menu"/>;
const html = <div class="input__menus">{buttons}</div>;
const createButton = info => {
buttons.appendChild(<button class="markup__button" type="button" style={info.style} onclick={ev => { info.onclick(ev, info); }}>{info.title}</button>);
};
return {
get element() { return html; },
get visible() { return !html.classList.contains('hidden'); },
set visible(state) { html.classList.toggle('hidden', !state); },
get height() { return html.getBoundingClientRect().height; },
createButton: createButton,
};
};

View file

@ -170,12 +170,15 @@ const MamiColourPicker = function(options) {
dialog: pos => { dialog: pos => {
html.classList.remove('hidden'); html.classList.remove('hidden');
if(pos instanceof MouseEvent) { if(pos instanceof MouseEvent)
const ev = pos; pos = pos.target;
if(pos instanceof Element)
pos = pos.getBoundingClientRect();
if(pos instanceof DOMRect) {
const bbb = pos;
pos = {}; pos = {};
const mbb = html.getBoundingClientRect(); const mbb = html.getBoundingClientRect();
const bbb = ev.target.getBoundingClientRect();
const pbb = html.parentNode.getBoundingClientRect(); const pbb = html.parentNode.getBoundingClientRect();
pos.left = bbb.left; pos.left = bbb.left;

View file

@ -116,12 +116,15 @@ const MamiEmotePicker = function(args) {
html.classList.remove('hidden'); html.classList.remove('hidden');
if(pos instanceof MouseEvent) { if(pos instanceof MouseEvent)
const ev = pos; pos = pos.target;
if(pos instanceof Element)
pos = pos.getBoundingClientRect();
if(pos instanceof DOMRect) {
const bbb = pos;
pos = {}; pos = {};
const mbb = html.getBoundingClientRect(); const mbb = html.getBoundingClientRect();
const bbb = ev.target.getBoundingClientRect();
const pbb = html.parentNode.getBoundingClientRect(); const pbb = html.parentNode.getBoundingClientRect();
pos.right = pbb.width - bbb.left; pos.right = pbb.width - bbb.left;

View file

@ -11,6 +11,7 @@ window.Umi = { UI: {} };
#include events.js #include events.js
#include mobile.js #include mobile.js
#include mszauth.js #include mszauth.js
#include parsing.js
#include themes.js #include themes.js
#include txtrigs.js #include txtrigs.js
#include uniqstr.js #include uniqstr.js
@ -19,6 +20,8 @@ window.Umi = { UI: {} };
#include weeb.js #include weeb.js
#include worker.js #include worker.js
#include audio/autoplay.js #include audio/autoplay.js
#include chatform/form.jsx
#include colpick/picker.jsx
#include controls/msgbox.jsx #include controls/msgbox.jsx
#include controls/ping.jsx #include controls/ping.jsx
#include controls/views.js #include controls/views.js
@ -46,11 +49,7 @@ window.Umi = { UI: {} };
#include sound/sndtest.jsx #include sound/sndtest.jsx
#include ui/chat-layout.js #include ui/chat-layout.js
#include ui/emotes.js #include ui/emotes.js
#include ui/hooks.js
#include ui/input-menus.js
#include ui/loading-overlay.jsx #include ui/loading-overlay.jsx
#include ui/markup.js
#include ui/view.js
const MamiInit = async args => { const MamiInit = async args => {
args = MamiArgs('args', args, define => { args = MamiArgs('args', args, define => {
@ -152,6 +151,7 @@ const MamiInit = async args => {
settings.define('newLineOnEnter').default(false).create(); settings.define('newLineOnEnter').default(false).create();
settings.define('keepEmotePickerOpen').default(true).create(); settings.define('keepEmotePickerOpen').default(true).create();
settings.define('doNotMarkLinksAsVisited').default(false).create(); settings.define('doNotMarkLinksAsVisited').default(false).create();
settings.define('showMarkupSelector').type(['always', 'focus', 'never']).default('always').create();
const noNotifSupport = !('Notification' in window); const noNotifSupport = !('Notification' in window);
settings.define('enableNotifications').default(false).immutable(noNotifSupport).critical().create(); settings.define('enableNotifications').default(false).immutable(noNotifSupport).critical().create();
@ -287,10 +287,56 @@ const MamiInit = async args => {
}, },
}); });
const layout = new Umi.UI.ChatLayout(sidebar);
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.getElement().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); await ctx.views.unshift(layout);
Umi.UI.Hooks.AddHooks(); ctx.events.watch('form:resize', ev => {
Umi.UI.Messages.ScrollIfNeeded(ev.detail.diffHeight);
});
settings.watch('style', ev => { settings.watch('style', ev => {
for(const className of layout.getElement().classList) for(const className of layout.getElement().classList)
@ -306,6 +352,8 @@ const MamiInit = async args => {
}); });
settings.watch('preventOverflow', ev => { args.parent.classList.toggle('prevent-overflow', 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('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; });
settings.watch('minecraft', ev => { settings.watch('minecraft', ev => {
if(ev.detail.initial && ev.detail.value === 'no') if(ev.detail.initial && ev.detail.value === 'no')
@ -363,7 +411,7 @@ const MamiInit = async args => {
loadingOverlay.message = 'Building menus...'; loadingOverlay.message = 'Building menus...';
MamiCompat('Umi.Parser.SockChatBBcode.EmbedStub', { value: () => {} }); // intentionally a no-op MamiCompat('Umi.Parser.SockChatBBcode.EmbedStub', { value: () => {} }); // intentionally a no-op
MamiCompat('Umi.UI.View.SetText', { value: text => console.log(`Umi.UI.View.SetText(text: ${text})`) }); MamiCompat('Umi.UI.View.SetText', { value: text => { chatForm.input.setText(text); } });
const sbUsers = new MamiSidebarPanelUsers; const sbUsers = new MamiSidebarPanelUsers;
sidebar.createPanel(sbUsers); sidebar.createPanel(sbUsers);
@ -377,13 +425,13 @@ const MamiInit = async args => {
name: 'action', name: 'action',
text: 'Describe action', text: 'Describe action',
condition: entry => Umi.User.getCurrentUser()?.id === entry.id, condition: entry => Umi.User.getCurrentUser()?.id === entry.id,
onclick: entry => { Umi.UI.View.SetText('/me '); }, onclick: entry => { chatForm.input.setText('/me '); },
}); });
sbUsers.addOption({ sbUsers.addOption({
name: 'nick', name: 'nick',
text: 'Set nickname', text: 'Set nickname',
condition: entry => Umi.User.getCurrentUser()?.id === entry.id && Umi.User.getCurrentUser().perms.canSetNick, condition: entry => Umi.User.getCurrentUser()?.id === entry.id && Umi.User.getCurrentUser().perms.canSetNick,
onclick: entry => { Umi.UI.View.SetText('/nick '); }, onclick: entry => { chatForm.input.setText('/nick '); },
}); });
sbUsers.addOption({ sbUsers.addOption({
name: 'bans', name: 'bans',
@ -415,13 +463,13 @@ const MamiInit = async args => {
name: 'dm', name: 'dm',
text: 'Send direct message', text: 'Send direct message',
condition: entry => Umi.User.getCurrentUser()?.id !== entry.id, condition: entry => Umi.User.getCurrentUser()?.id !== entry.id,
onclick: entry => { Umi.UI.View.SetText(`/msg ${entry.name} `); }, onclick: entry => { chatForm.input.setText(`/msg ${entry.name} `); },
}); });
sbUsers.addOption({ sbUsers.addOption({
name: 'kick', name: 'kick',
text: 'Kick from chat', text: 'Kick from chat',
condition: entry => Umi.User.hasCurrentUser() && Umi.User.getCurrentUser().id !== entry.id && Umi.User.getCurrentUser().perms.canKick, condition: entry => Umi.User.hasCurrentUser() && Umi.User.getCurrentUser().id !== entry.id && Umi.User.getCurrentUser().perms.canKick,
onclick: entry => { Umi.UI.View.SetText(`/kick ${entry.name} `); }, onclick: entry => { chatForm.input.setText(`/kick ${entry.name} `); },
}); });
sbUsers.addOption({ sbUsers.addOption({
name: 'ipaddr', name: 'ipaddr',
@ -462,6 +510,7 @@ const MamiInit = async args => {
category.setting('autoEmbedPlay').title('Auto-play embedded media').done(); category.setting('autoEmbedPlay').title('Auto-play embedded media').done();
category.setting('keepEmotePickerOpen').title('Keep emoticon picker open').done(); category.setting('keepEmotePickerOpen').title('Keep emoticon picker open').done();
category.setting('newLineOnEnter').title('Swap Enter and Shift+Enter behaviour').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 => { sbSettings.category(category => {
category.header('Notifications'); category.header('Notifications');
@ -660,36 +709,35 @@ const MamiInit = async args => {
const sbActPing = new MamiSidebarActionPing(pingIndicator, ctx.msgbox); const sbActPing = new MamiSidebarActionPing(pingIndicator, ctx.msgbox);
sidebar.createAction(sbActPing); sidebar.createAction(sbActPing);
Umi.UI.InputMenus.Add('markup');
Umi.UI.Markup.SetPickerTarget(layout.getElement());
let emotePicker, emotePickerVisible = false; let emotePicker, emotePickerVisible = false;
Umi.UI.InputMenus.AddButton('emotes', 'Emoticons', ev => { chatForm.input.createButton({
if(emotePicker === undefined) { title: 'Emoticons',
emotePicker = new MamiEmotePicker({ icon: 'fas fa-smile',
onClose: () => { emotePickerVisible = false; }, onclick: (ev, button) => {
onPick: emote => { if(emotePicker === undefined) {
const emoteStr = `:${emote.strings[0]}:`; emotePicker = new MamiEmotePicker({
Umi.UI.View.EnterAtCursor(emoteStr); onClose: () => { emotePickerVisible = false; },
Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + emoteStr.length); onPick: emote => {
Umi.UI.View.SetPosition(Umi.UI.View.GetPosition(), true); chatForm.input.insertAtCursor(`:${emote.strings[0]}:`);
Umi.UI.View.Focus(); },
}, getEmotes: () => MamiEmotes.all(Umi.User.getCurrentUser().perms.rank),
getEmotes: () => MamiEmotes.all(Umi.User.getCurrentUser().perms.rank), setKeepOpenOnPick: value => { settings.set('keepEmotePickerOpen', value); },
setKeepOpenOnPick: value => { settings.set('keepEmotePickerOpen', value); }, });
}); layout.getElement().appendChild(emotePicker.element);
layout.getElement().appendChild(emotePicker.element); settings.watch('keepEmotePickerOpen', ev => { emotePicker.keepOpenOnPick = ev.detail.value; });
settings.watch('keepEmotePickerOpen', ev => { emotePicker.keepOpenOnPick = ev.detail.value; }); }
}
if(emotePickerVisible) { if(emotePickerVisible) {
emotePicker.close(); emotePicker.close();
} else { } else {
emotePickerVisible = true; emotePickerVisible = true;
emotePicker.dialog(ev); emotePicker.dialog(button);
} }
},
}); });
let doUpload; let doUpload;
ctx.eeprom = new MamiEEPROM(futami.get('eeprom2'), MamiMisuzuAuth.getLine); ctx.eeprom = new MamiEEPROM(futami.get('eeprom2'), MamiMisuzuAuth.getLine);
ctx.eeprom.init() ctx.eeprom.init()
@ -718,7 +766,7 @@ const MamiInit = async args => {
} else } else
text = location.protocol + upload.url; text = location.protocol + upload.url;
Umi.UI.Markup.InsertRaw(text, '') chatForm.input.insertAtCursor(text);
}, },
}); });
sbUploads.addOption({ sbUploads.addOption({
@ -784,13 +832,17 @@ const MamiInit = async args => {
}); });
args.parent.appendChild(uploadForm); args.parent.appendChild(uploadForm);
Umi.UI.InputMenus.AddButton('upload', 'Upload', () => uploadForm.click(), 'markup'); chatForm.input.createButton({
title: 'Upload',
icon: 'fas fa-file-upload',
onclick: () => { uploadForm.click(); },
});
$i('umi-msg-text').onpaste = ev => { ctx.events.watch('form:upload', ev => {
if(ev.clipboardData && ev.clipboardData.files.length > 0) console.info(ev);
for(const file of ev.clipboardData.files) for(const file of ev.detail.files)
doUpload(file); doUpload(file);
}; });
}); });
// figure out how to display a UI for this someday // figure out how to display a UI for this someday
@ -860,6 +912,10 @@ const MamiInit = async args => {
const conMan = new MamiConnectionManager(sockChat, settings, futami.get('servers'), ctx.events.scopeTo('conn')); const conMan = new MamiConnectionManager(sockChat, settings, futami.get('servers'), ctx.events.scopeTo('conn'));
ctx.conMan = conMan; ctx.conMan = conMan;
ctx.events.watch('form:send', ev => {
sockChat.client.sendMessage(ev.detail.text);
});
sbChannels.onClickEntry = async info => { sbChannels.onClickEntry = async info => {
await sockChat.client.switchChannel(info); await sockChat.client.switchChannel(info);

View file

@ -1,91 +1,93 @@
#include utility.js #include utility.js
#include ui/markup.js
if(!Umi.Parser) Umi.Parser = {}; if(!Umi.Parser) Umi.Parser = {};
if(!Umi.Parser.SockChatBBcode) Umi.Parser.SockChatBBcode = {}; if(!Umi.Parser.SockChatBBcode) Umi.Parser.SockChatBBcode = {};
Umi.Parsing = (function() { const UmiBBCodes = [
const bbCodes = [ {
{ tag: 'b',
tag: 'b', text: 'B',
replace: '<b>{0}</b>', style: 'font-weight: 700',
button: true, replace: '<b>{0}</b>',
}, },
{ {
tag: 'i', tag: 'i',
replace: '<i>{0}</i>', text: 'I',
button: true, style: 'font-style: italic',
}, replace: '<i>{0}</i>',
{ },
tag: 'u', {
replace: '<u>{0}</u>', tag: 'u',
button: true, text: 'U',
}, style: 'text-decoration: underline',
{ replace: '<u>{0}</u>',
tag: 's', },
replace: '<del>{0}</del>', {
button: true, tag: 's',
}, text: 'S',
{ style: 'text-decoration: line-through',
tag: 'quote', replace: '<del>{0}</del>',
replace: '<q style="font-variant: small-caps;">{0}</q>', },
button: true, {
}, tag: 'quote',
{ text: 'Quote',
tag: 'code', replace: '<q style="font-variant: small-caps;">{0}</q>',
replace: '<span style="white-space: pre-wrap; font-family: monospace;">{0}</span>', },
button: true, {
}, tag: 'code',
{ text: 'Code',
tag: 'sjis', replace: '<span style="white-space: pre-wrap; font-family: monospace;">{0}</span>',
replace: '<span class="sjis" style="white-space: pre-wrap;">{0}</span>', },
}, {
{ tag: 'sjis',
tag: 'color', replace: '<span class="sjis" style="white-space: pre-wrap;">{0}</span>',
hasArg: true, },
stripArg: ';:{}<>&|\\/~\'"', {
replace: '<span style="color:{0};">{1}</span>', tag: 'color',
isToggle: true, text: 'Colour',
button: true, hasArg: true,
}, stripArg: ';:{}<>&|\\/~\'"',
{ replace: '<span style="color:{0};">{1}</span>',
tag: 'img', },
stripText: '"\'', {
replace: '<span title="{0}"><span title="link"><a class="markup__link" href="{0}" target="_blank" rel="nofollow noreferrer noopener">{0}</a></span>&nbsp;[<a href="#" onclick="Umi.Parser.SockChatBBcode.EmbedImage(this);return false;" class="markup__link">Embed</a>]</span>', tag: 'img',
button: true, text: 'Image',
}, stripText: '"\'',
{ replace: '<span title="{0}"><span title="link"><a class="markup__link" href="{0}" target="_blank" rel="nofollow noreferrer noopener">{0}</a></span>&nbsp;[<a href="#" onclick="Umi.Parser.SockChatBBcode.EmbedImage(this);return false;" class="markup__link">Embed</a>]</span>',
tag: 'url', },
stripText: '"\'', {
replace: '<a href="{0}" target="_blank" rel="nofollow noreferrer noopener" class="markup__link">{0}</a>', tag: 'url',
}, stripText: '"\'',
{ replace: '<a href="{0}" target="_blank" rel="nofollow noreferrer noopener" class="markup__link">{0}</a>',
tag: 'url', },
hasArg: true, {
stripArg: '"\'', tag: 'url',
replace: '<a href="{0}" target="_blank" rel="nofollow noreferrer noopener" class="markup__link">{1}</a>', text: 'URL',
button: true, hasArg: true,
}, stripArg: '"\'',
{ replace: '<a href="{0}" target="_blank" rel="nofollow noreferrer noopener" class="markup__link">{1}</a>',
tag: 'video', },
stripText: '"\'', {
replace: '<span title="{0}"><span title="link"><a class="markup__link" href="{0}" target="_blank" rel="nofollow noreferrer noopener">{0}</a></span>&nbsp;[<a href="#" onclick="Umi.Parser.SockChatBBcode.EmbedVideo(this);return false;" class="markup__link">Embed</a>]</span>', tag: 'video',
button: true, text: 'Video',
}, stripText: '"\'',
{ replace: '<span title="{0}"><span title="link"><a class="markup__link" href="{0}" target="_blank" rel="nofollow noreferrer noopener">{0}</a></span>&nbsp;[<a href="#" onclick="Umi.Parser.SockChatBBcode.EmbedVideo(this);return false;" class="markup__link">Embed</a>]</span>',
tag: 'audio', },
stripText: '"\'', {
replace: '<span title="{0}"><span title="link"><a class="markup__link" href="{0}" target="_blank" rel="nofollow noreferrer noopener">{0}</a></span>&nbsp;[<a href="#" onclick="Umi.Parser.SockChatBBcode.EmbedAudio(this);return false;" class="markup__link">Embed</a>]</span>', tag: 'audio',
button: true, text: 'Audio',
}, stripText: '"\'',
{ replace: '<span title="{0}"><span title="link"><a class="markup__link" href="{0}" target="_blank" rel="nofollow noreferrer noopener">{0}</a></span>&nbsp;[<a href="#" onclick="Umi.Parser.SockChatBBcode.EmbedAudio(this);return false;" class="markup__link">Embed</a>]</span>',
tag: 'spoiler', },
stripText: '"\'', {
replace: '<span data-shit="{0}"><span>*** HIDDEN ***</span>&nbsp;[<a href="#" onclick="Umi.Parser.SockChatBBcode.ToggleSpoiler(this);return false;" class="markup__link">Reveal</a>]</span>', tag: 'spoiler',
button: true, text: 'Spoiler',
} stripText: '"\'',
]; replace: '<span data-shit="{0}"><span>*** HIDDEN ***</span>&nbsp;[<a href="#" onclick="Umi.Parser.SockChatBBcode.ToggleSpoiler(this);return false;" class="markup__link">Reveal</a>]</span>',
},
];
Umi.Parsing = (function() {
const replaceAll = function(haystack, needle, replace, ignore) { const replaceAll = function(haystack, needle, replace, ignore) {
return haystack.replace( return haystack.replace(
new RegExp( new RegExp(
@ -362,21 +364,9 @@ Umi.Parsing = (function() {
Umi.Parser.SockChatBBcode.ToggleSpoiler = toggleSpoiler; Umi.Parser.SockChatBBcode.ToggleSpoiler = toggleSpoiler;
return { return {
Init: function() {
for (let i = 0; i < bbCodes.length; i++) {
const bbCode = bbCodes[i];
if(!bbCode.button)
continue;
const start = '[{0}]'.replace('{0}', bbCode.tag + (bbCode.arg ? '=' : '')), end = '[/{0}]'.replace('{0}', bbCode.tag);
const text = (bbCode.tag.length > 1 ? bbCode.tag.substring(0, 1).toUpperCase() + bbCode.tag.substring(1) : bbCode.tag).replace('Color', 'Colour');
Umi.UI.Markup.Add(bbCode.tag, text, start, end);
}
},
Parse: function(element, message) { Parse: function(element, message) {
for(let i = 0; i < bbCodes.length; i++) { for(let i = 0; i < UmiBBCodes.length; i++) {
const bbCode = bbCodes[i]; const bbCode = UmiBBCodes[i];
if(!bbCode.hasArg) { if(!bbCode.hasArg) {
let at = 0; let at = 0;

View file

@ -113,6 +113,8 @@ const MamiSidebarPanelSettings = function(settings, msgBox) {
options: undefined, options: undefined,
confirm: undefined, confirm: undefined,
disabled: info.immutable, disabled: info.immutable,
trueValue: true,
falseValue: false,
}; };
const detectType = () => { const detectType = () => {
@ -151,6 +153,20 @@ const MamiSidebarPanelSettings = function(settings, msgBox) {
setting.options = value; setting.options = value;
return pub; return pub;
}, },
on: value => {
if(typeof value === undefined)
value = true;
setting.trueValue = value;
return pub;
},
off: value => {
if(typeof value === undefined)
value = false;
setting.falseValue = value;
return pub;
},
confirm: value => { confirm: value => {
if(!Array.isArray(value) && typeof value !== 'string' && value !== undefined) if(!Array.isArray(value) && typeof value !== 'string' && value !== undefined)
throw 'value must be an array or a string'; throw 'value must be an array or a string';
@ -200,7 +216,15 @@ const MamiSidebarPanelSettings = function(settings, msgBox) {
label.append(title, input); label.append(title, input);
if(setting.type === 'checkbox') { if(setting.type === 'checkbox') {
settings.watch(info.name, ev => input.checked = ev.detail.value); settings.watch(info.name, ev => input.checked = ev.detail.value !== setting.falseValue);
const toggle = () => {
settings.set(
info.name,
settings.get(info.name) !== setting.falseValue ? setting.falseValue : setting.trueValue
);
};
input.addEventListener('change', () => { input.addEventListener('change', () => {
if(setting.confirm !== undefined && input.checked !== info.fallback) { if(setting.confirm !== undefined && input.checked !== info.fallback) {
msgBox.show({ msgBox.show({
@ -208,12 +232,12 @@ const MamiSidebarPanelSettings = function(settings, msgBox) {
yes: { primary: false }, yes: { primary: false },
no: { primary: true }, no: { primary: true },
}).then(() => { }).then(() => {
settings.toggle(info.name); toggle();
}).catch(() => { }).catch(() => {
input.checked = info.fallback; input.checked = info.fallback;
}); });
} else { } else {
settings.toggle(info.name); toggle();
} }
}); });
} else { } else {

View file

@ -1,6 +1,5 @@
#include compat.js #include compat.js
#include mszauth.js #include mszauth.js
#include ui/hooks.js
const MamiSockChat = function(protoWorker) { const MamiSockChat = function(protoWorker) {
const events = protoWorker.eventTarget('sockchat'); const events = protoWorker.eventTarget('sockchat');
@ -23,8 +22,6 @@ const MamiSockChat = function(protoWorker) {
client = await protoWorker.root.create('sockchat', { ping: futami.get('ping') }); client = await protoWorker.root.create('sockchat', { ping: futami.get('ping') });
await client.setDumpPackets(dumpPackets); await client.setDumpPackets(dumpPackets);
Umi.UI.Hooks.SetCallbacks(client.sendMessage);
MamiCompat('Umi.Server', { get: () => client, configurable: true }); MamiCompat('Umi.Server', { get: () => client, configurable: true });
MamiCompat('Umi.Server.SendMessage', { value: text => client.sendMessage(text), 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.SockChat.Protocol.Instance.SendMessage', { value: text => client.sendMessage(text), configurable: true });

View file

@ -5,7 +5,6 @@
#include notices/baka.jsx #include notices/baka.jsx
#include sockchat/modal.js #include sockchat/modal.js
#include ui/emotes.js #include ui/emotes.js
#include ui/markup.js
#include ui/messages.jsx #include ui/messages.jsx
const MamiSockChatHandlers = function( const MamiSockChatHandlers = function(
@ -103,9 +102,6 @@ const MamiSockChatHandlers = function(
sbUsers.createEntry(ev.detail.user); sbUsers.createEntry(ev.detail.user);
Umi.UI.Markup.Reset();
Umi.Parsing.Init();
if(ctx.views.count > 1) if(ctx.views.count > 1)
ctx.views.pop(); ctx.views.pop();
}; };

View file

@ -1,34 +0,0 @@
#include utility.js
Umi.UI.ChatInputMain = function() {
const html = $e({
attrs: {
id: 'umi-msg-container',
className: 'input__main',
},
child: [
{
tag: 'textarea',
attrs: {
id: 'umi-msg-text',
name: 'text',
className: 'input__text',
autofocus: true,
},
},
{
tag: 'button',
attrs: {
id: 'umi-msg-send',
className: 'input__button input__button--send',
},
},
],
});
return {
getElement: function() {
return html;
},
};
};

View file

@ -1,16 +0,0 @@
#include utility.js
Umi.UI.ChatInputMenus = function() {
const html = $e({
attrs: {
id: 'umi-msg-menu',
className: 'input__menus',
},
});
return {
getElement: function() {
return html;
},
};
};

View file

@ -1,32 +0,0 @@
#include utility.js
#include ui/chat-input-main.js
#include ui/chat-input-menus.js
Umi.UI.ChatInput = function() {
const menus = new Umi.UI.ChatInputMenus;
const main = new Umi.UI.ChatInputMain;
const html = $e({
tag: 'form',
attrs: {
id: 'umi-msg-form',
className: 'input',
},
child: [
menus,
main,
],
});
return {
getMenus: function() {
return menus;
},
getMain: function() {
return main;
},
getElement: function() {
return html;
},
};
};

View file

@ -1,10 +1,8 @@
#include utility.js #include utility.js
#include ui/chat-message-list.js #include ui/chat-message-list.js
#include ui/chat-input.js
Umi.UI.ChatInterface = function() { Umi.UI.ChatInterface = function(chatForm) {
const messages = new Umi.UI.ChatMessageList; const messages = new Umi.UI.ChatMessageList;
const input = new Umi.UI.ChatInput;
const html = $e({ const html = $e({
attrs: { attrs: {
@ -12,7 +10,7 @@ Umi.UI.ChatInterface = function() {
}, },
child: [ child: [
messages, messages,
input, chatForm,
], ],
}); });
@ -20,9 +18,6 @@ Umi.UI.ChatInterface = function() {
getMessageList: function() { getMessageList: function() {
return messages; return messages;
}, },
getInput: function() {
return input;
},
getElement: function() { getElement: function() {
return html; return html;
}, },

View file

@ -1,10 +1,9 @@
#include utility.js #include utility.js
#include ui/chat-interface.js #include ui/chat-interface.js
#include sidebar/sidebar.jsx
// this needs revising at some point but will suffice for now // this needs revising at some point but will suffice for now
Umi.UI.ChatLayout = function(sideBar) { Umi.UI.ChatLayout = function(chatForm, sideBar) {
const main = new Umi.UI.ChatInterface; const main = new Umi.UI.ChatInterface(chatForm);
const html = $e({ const html = $e({
attrs: { attrs: {
@ -18,9 +17,6 @@ Umi.UI.ChatLayout = function(sideBar) {
}); });
return { return {
getSideBar: function() {
return sideBar;
},
getInterface: function() { getInterface: function() {
return main; return main;
}, },

View file

@ -1,110 +0,0 @@
#include users.js
#include utility.js
#include ui/messages.jsx
#include ui/view.js
Umi.UI.Hooks = (function() {
let sendMessage;
return {
SetCallbacks: (sendMessageFunc) => {
sendMessage = sendMessageFunc;
},
AddHooks: function() {
const msgForm = $i('umi-msg-form');
const msgText = $i('umi-msg-text');
window.addEventListener('keydown', function(ev) {
if((ev.ctrlKey && ev.key !== 'v') || ev.altKey)
return;
if(!ev.target.matches('input, textarea, select, button'))
msgText.focus();
});
msgForm.addEventListener('submit', ev => {
ev.preventDefault();
if(typeof sendMessage !== 'function')
return;
const textField = ev.target.elements.namedItem('text');
if(textField instanceof HTMLTextAreaElement) {
let text = textField.value;
textField.value = '';
text = text.replace(/\t/g, ' ');
if(text.length > 0)
sendMessage(text);
}
});
msgText.addEventListener('keyup', function(ev) {
const isScrolledToBottom = Umi.UI.Messages.IsScrolledToBottom();
const elemParent = msgText.parentNode;
let height = 40;
if(mami.settings.get('expandTextBox') && msgText.scrollHeight > msgText.clientHeight)
height = msgText.scrollHeight;
if(height > 40)
elemParent.style.height = height.toString() + 'px';
else
elemParent.style.height = null;
if(isScrolledToBottom)
Umi.UI.Messages.ScrollIfNeeded();
});
msgText.addEventListener('keydown', function(ev) {
if(ev.key === 'Tab' && (!ev.shiftKey || !ev.ctrlKey)) {
ev.preventDefault();
const text = Umi.UI.View.GetText();
if(text.length < 1)
return;
const start = Umi.UI.View.GetPosition();
let position = start,
snippet = '';
while(position >= 0 && text.charAt(position - 1) !== ' ' && text.charAt(position - 1) !== "\n") {
--position;
snippet = text.charAt(position) + snippet;
}
let insertText = undefined;
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] + ':';
} else {
const users = Umi.Users.Find(snippet);
if(users.length === 1)
insertText = users[0].name;
}
if(insertText !== undefined) {
Umi.UI.View.SetText(text.slice(0, start - snippet.length) + text.slice(start));
Umi.UI.View.SetPosition(start - snippet.length);
Umi.UI.View.EnterAtCursor(insertText);
Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + insertText.length);
Umi.UI.View.SetPosition(Umi.UI.View.GetPosition(), true);
}
return;
}
if((ev.key === 'Enter' || ev.key === 'NumpadEnter') && ev.shiftKey === mami.settings.get('newLineOnEnter')) {
ev.preventDefault();
msgForm.requestSubmit();
return;
}
});
},
};
})();

View file

@ -1,68 +0,0 @@
#include utility.js
#include ui/input-menus.js
Umi.UI.InputMenus = (function() {
const ids = [];
const createButtonId = id => `umi-msg-menu-btn-${id}`;
const createButton = function(id, title, onClick) {
return $e({
tag: 'button',
attrs: {
type: 'button',
id: createButtonId(id),
classList: ['input__button', 'input__button--' + id],
title: title,
onclick: onClick,
},
});
};
return {
Add: function(baseId, title, beforeButtonId) {
if(baseId !== 'markup')
throw 'only baseId "markup" may be added';
if(ids.includes(baseId))
return;
ids.push(baseId);
let beforeButton;
if(typeof beforeButtonId === 'string')
beforeButton = $i(createButtonId(beforeButtonId))
if(!(beforeButton instanceof Element))
beforeButton = $i('umi-msg-send');
if(typeof title === 'string')
$i('umi-msg-container').insertBefore(createButton(baseId, title), beforeButton);
$i('umi-msg-menu').appendChild(
$e({
attrs: {
'class': ['input__menu', 'input__menu--' + baseId, 'input__menu--active'],
id: 'umi-msg-menu-sub-' + baseId,
tabindex: '0',
}
})
);
},
AddButton: function(baseId, title, onClick, beforeButtonId) {
if(ids.includes(baseId))
return;
ids.push(baseId);
let beforeButton;
if(typeof beforeButtonId === 'string')
beforeButton = $i(createButtonId(beforeButtonId))
if(!(beforeButton instanceof Element))
beforeButton = $i('umi-msg-send');
$i('umi-msg-container').insertBefore(createButton(baseId, title, onClick), beforeButton);
},
Get: function(baseId, button) {
const id = 'umi-msg-menu-' + (button ? 'btn' : 'sub') + '-' + baseId;
if(ids.indexOf(baseId) >= 0)
return $i(id);
return null;
},
};
})();

View file

@ -1,71 +0,0 @@
#include colour.js
#include common.js
#include utility.js
#include colpick/picker.jsx
#include ui/input-menus.js
#include ui/markup.js
#include ui/view.js
Umi.UI.Markup = (function() {
const insertRaw = function(start, end) {
const selectionLength = Umi.UI.View.GetSelectionLength();
Umi.UI.View.EnterAtCursor(start);
Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + selectionLength + start.length);
Umi.UI.View.EnterAtCursor(end);
Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() - selectionLength);
Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + selectionLength, true);
Umi.UI.View.Focus();
};
let pickerTarget = document.body;
let picker, pickerVisible = false;
const insert = function(ev) {
if(this.dataset.umiTagName === 'color') {
if(picker === undefined) {
picker = new MamiColourPicker({ presets: futami.get('colours') });
pickerTarget.appendChild(picker.element);
}
if(pickerVisible) {
picker.close();
} else {
pickerVisible = true;
picker.dialog(ev)
.then(colour => insertRaw(`[color=${MamiColour.hex(colour)}]`, '[/color]'))
.catch(() => {}) // noop so the console stops screaming
.finally(() => pickerVisible = false);
}
} else
insertRaw(
this.dataset.umiBeforeCursor,
this.dataset.umiAfterCursor
);
};
return {
SetPickerTarget: target => { pickerTarget = target; },
Add: function(name, text, beforeCursor, afterCursor) {
Umi.UI.InputMenus.Get('markup').appendChild($e({
tag: 'button',
attrs: {
type: 'button',
id: 'umi-msg-menu-markup-btn-' + name,
classList: ['markup__button', 'markup__button--' + name],
dataset: {
umiTagName: name,
umiBeforeCursor: beforeCursor,
umiAfterCursor: afterCursor,
},
onclick: insert,
},
child: text,
}));
},
Reset: function() {
Umi.UI.InputMenus.Get('markup').innerHTML = '';
},
Insert: insert,
InsertRaw: insertRaw,
};
})();

View file

@ -375,9 +375,19 @@ Umi.UI.Messages = (function() {
const msgsList = $i('umi-messages'); const msgsList = $i('umi-messages');
return msgsList.scrollTop === (msgsList.scrollHeight - msgsList.offsetHeight); return msgsList.scrollTop === (msgsList.scrollHeight - msgsList.offsetHeight);
}, },
ScrollIfNeeded: () => { ScrollIfNeeded: (offsetOrForce = 0) => {
const msgsList = $i('umi-messages');
if(!(msgsList instanceof Element))
return;
if(typeof offsetOrForce === 'boolean' && offsetOrForce !== true)
return;
if(typeof offsetOrForce === 'number' && msgsList.scrollTop < (msgsList.scrollHeight - msgsList.offsetHeight - offsetOrForce))
return;
if(mami.settings.get('autoScroll')) if(mami.settings.get('autoScroll'))
$i('umi-messages').lastElementChild?.scrollIntoView({ inline: 'end' }); msgsList.lastElementChild?.scrollIntoView({ inline: 'end' });
}, },
SwitchChannel: channel => { SwitchChannel: channel => {
if(typeof channel === 'object' && channel !== null && 'name' in channel) if(typeof channel === 'object' && channel !== null && 'name' in channel)
@ -385,15 +395,13 @@ Umi.UI.Messages = (function() {
if(typeof channel !== 'string') if(typeof channel !== 'string')
return; return;
const isScrolledToBottom = Umi.UI.Messages.IsScrolledToBottom();
focusChannelName = channel; focusChannelName = channel;
const root = $i('umi-messages'); const root = $i('umi-messages');
for(const elem of root.children) for(const elem of root.children)
elem.classList.toggle('hidden', elem.dataset.channel !== undefined && elem.dataset.channel !== focusChannelName); elem.classList.toggle('hidden', elem.dataset.channel !== undefined && elem.dataset.channel !== focusChannelName);
if(isScrolledToBottom) Umi.UI.Messages.ScrollIfNeeded();
Umi.UI.Messages.ScrollIfNeeded();
}, },
Clear: retain => { Clear: retain => {
if(typeof retain === 'string' && !isNaN(retain)) if(typeof retain === 'string' && !isNaN(retain))

View file

@ -1,45 +0,0 @@
#include utility.js
Umi.UI.View = (function() {
const getPosition = function(end) {
return $i('umi-msg-text')[end ? 'selectionEnd' : 'selectionStart'];
};
const setPosition = function(pos, end) {
$i('umi-msg-text')[end ? 'selectionEnd' : 'selectionStart'] = pos;
};
const getText = function() {
return $i('umi-msg-text').value;
};
const setText = function(text) {
$i('umi-msg-text').value = text;
};
return {
Focus: function() {
$i('umi-msg-text').focus();
},
SetPosition: setPosition,
GetPosition: getPosition,
GetSelectionLength: function() {
let length = getPosition(true) - getPosition();
if(length < 0)
length = getPosition() - getPosition(true);
return length;
},
EnterAtCursor: function(text, overwrite) {
const value = getText();
const current = getPosition();
let out = '';
out += value.slice(0, current);
out += text;
out += value.slice(current + (overwrite ? text.length : 0));
setText(out);
setPosition(current);
},
GetText: getText,
SetText: setText,
};
})();

View file

@ -166,6 +166,10 @@ const SockChatProtocol = function(dispatch, options) {
if(!ctx.isAuthed) if(!ctx.isAuthed)
throw 'must be authenticated'; throw 'must be authenticated';
text = text.replace(/\t/g, ' ');
if(text.length < 1)
return;
// there's actually a pretty big bug here lol // there's actually a pretty big bug here lol
// any unsupported command is gonna fall through to the actual channel you're in // any unsupported command is gonna fall through to the actual channel you're in
if(!text.startsWith('/') && ctx.pseudoChannelName !== undefined) if(!text.startsWith('/') && ctx.pseudoChannelName !== undefined)