Work in progress message list rewrite.

This commit is contained in:
flash 2024-08-28 21:10:30 +00:00
parent 46549845b9
commit fa6ced4565
28 changed files with 1009 additions and 963 deletions

View file

@ -12,6 +12,7 @@
height: 40px; height: 40px;
max-height: 140px; max-height: 140px;
resize: none; resize: none;
width: 0;
} }
.input__text, .input__text,
@ -32,9 +33,6 @@
cursor: pointer cursor: pointer
} }
.input__menus {
}
.input__menu { .input__menu {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View file

@ -40,6 +40,10 @@ a:hover {
visibility: none !important; visibility: none !important;
} }
.invisible {
visibility: none !important;
}
.sjis { .sjis {
font-family: IPAMonaPGothic, 'IPA モナー Pゴシック', Monapo, Mona, 'MS PGothic', ' Pゴシック', monospace; font-family: IPAMonaPGothic, 'IPA モナー Pゴシック', Monapo, Mona, 'MS PGothic', ' Pゴシック', monospace;
font-size: 16px; font-size: 16px;

11
src/mami.js/avatar.js Normal file
View file

@ -0,0 +1,11 @@
const MamiFormatUserAvatarUrl = (userId, res = 0, changeTime = null) => {
const template = futami.get('avatar') ?? '';
if(template.length < 1)
return '';
changeTime ??= Date.now();
return template.replace('{user:id}', userId)
.replace('{resolution}', res)
.replace('{user:avatar_change}', changeTime);
};

View file

@ -1,8 +1,8 @@
#include chatform/input.jsx #include chat/input.jsx
#include chatform/markup.jsx #include chat/markup.jsx
const MamiChatForm = function(eventTarget) { const MamiChatForm = function(findUserInfosByName, findEmoteByName, eventTarget) {
const input = new MamiChatFormInput(eventTarget); const input = new MamiChatFormInput(findUserInfosByName, findEmoteByName, eventTarget);
const markup = new MamiChatFormMarkup; const markup = new MamiChatFormMarkup;
const html = <form class="input" onsubmit={ev => { const html = <form class="input" onsubmit={ev => {

View file

@ -0,0 +1,37 @@
#include parsing.js
#include url.js
#include weeb.js
#include ui/emotes.js
// replace this with better parsing, read the fucking book already
const MamiMolestMessageContents = (info, textElem, userNameElem) => {
const hasTextElem = textElem instanceof Element;
if(hasTextElem) {
Umi.UI.Emoticons.Parse(textElem, info.sender);
Umi.Parsing.Parse(textElem, info);
const textSplit = textElem.innerText.split(' ');
for(const textPart of textSplit) {
const uri = Umi.URI.Parse(textPart);
if(uri !== null && uri.Slashes !== null) {
const anchorElem = <a class="markup__link" href={textPart} target="_blank" rel="nofollow noreferrer noopener">{textPart}</a>;
textElem.innerHTML = textElem.innerHTML.replace(textPart.replace(/&/g, '&amp;'), anchorElem.outerHTML);
}
}
}
if(mami.settings.get('weeaboo')) {
if(userNameElem instanceof Element)
userNameElem.appendChild($t(Weeaboo.getNameSuffix(info.sender)));
if(hasTextElem) {
textElem.appendChild($t(Weeaboo.getTextSuffix(info.sender)));
const kaomoji = Weeaboo.getRandomKaomoji(true, info);
if(kaomoji)
textElem.append(` ${kaomoji}`);
}
}
};

View file

@ -1,7 +1,4 @@
#include emotes.js const MamiChatFormInput = function(findUserInfosByName, findEmoteByName, eventTarget) {
#include users.js
const MamiChatFormInput = function(eventTarget) {
const textElem = <textarea class="input__text" name="text" autofocus="autofocus"/>; const textElem = <textarea class="input__text" name="text" autofocus="autofocus"/>;
let submitButton; let submitButton;
@ -74,25 +71,24 @@ const MamiChatFormInput = function(eventTarget) {
snippet = text.charAt(position) + snippet; snippet = text.charAt(position) + snippet;
} }
let insertText; const insertText = portion => {
const nextPos = start - snippet.length + portion.length;
textElem.value = text.slice(0, start - snippet.length) + portion + text.slice(start);
textElem.selectionEnd = textElem.selectionStart = nextPos;
};
if(snippet.indexOf(':') === 0) { if(snippet.indexOf(':') === 0) {
let emoteRank = 0; findEmoteByName(snippet.substring(1))
if(Umi.User.hasCurrentUser()) .then(emotes => {
emoteRank = Umi.User.getCurrentUser().perms.rank; if(emotes.length > 0)
const emotes = MamiEmotes.findByName(emoteRank, snippet.substring(1), true); insertText(`:${emotes[0]}:`);
if(emotes.length > 0) });
insertText = `:${emotes[0]}:`;
} else { } else {
const users = Umi.Users.Find(snippet); findUserInfosByName(snippet)
if(users.length === 1) .then(userInfos => {
insertText = users[0].name; if(userInfos.length === 1)
} insertText(userInfos[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; return;

View file

@ -0,0 +1,117 @@
#include uniqstr.js
#include chat/msg-act.jsx
#include chat/msg-text.jsx
const MamiChatMessages = function(globalEvents) {
const html = <div class="chat"/>;
const broadcasts = [];
const channels = new Map;
let msgs = [];
let autoScroll = false;
let autoEmbed = false;
// so like how the fuck am i going to do this wtf
// some msgs may not have an id, just gen a rando for those then?
const doAutoScroll = () => {
if(autoScroll)
html.lastElementChild?.scrollIntoView({ block: 'start', inline: 'end' });
};
return {
get element() { return html; },
get autoScroll() { return autoScroll; },
set autoScroll(value) {
autoScroll = !!value;
},
get autoEmbed() { return autoEmbed; },
set autoEmbed(value) {
autoEmbed = !!value;
},
addMessage: info => {
console.info('addMessage()', info);
const msgId = info.id ?? `internal-${MamiUniqueStr(8)}`;
let msg;
if('element' in info) {
msg = info;
} else {
if(info.type === 'msg:text' || info.type.startsWith('legacy:'))
msg = new MamiChatMessageText(info);
else
msg = new MamiChatMessageAction(info);
}
if('first' in msg) {
const prev = msgs[msgs.length - 1];
msg.first = prev === undefined || !('first' in prev) || !('info' in prev) || info.sender.id !== prev.info.sender.id;
}
msgs.push(msg);
html.appendChild(msg.element);
// only run when not hidden/current channel
if(autoEmbed) {
const callEmbedOn = msg.element.querySelectorAll('a[onclick^="Umi.Parser.SockChatBBcode.Embed"]');
for(const embedElem of callEmbedOn)
if(embedElem.dataset.embed !== '1')
embedElem.click();
}
// ^^ if(!eBase.classList.contains('hidden'))
doAutoScroll();
if(globalEvents)
globalEvents.dispatch('umi:ui:message_add', { element: msg.element });
},
removeMessage: msgId => {
console.info('removeMessage()', msgId);
for(const i in msgs) {
const msg = msgs[i];
if(msg.matches(msgId)) {
// TODO: i want login, logout, etc. actions to be collapsed as a single messages
// some kinda flow to allow for that should exists (probably just check if the msg obj has certain functions and then handle deletion differently)
if(html.contains(msg.element))
html.removeChild(msg.element);
msgs.splice(i, 1);
break;
}
}
doAutoScroll();
},
clearMessages: keepAmount => {
console.info('clearMessages()', keepAmount);
msgs = [];
html.replaceChildren();
},
scrollIfNeeded: offsetOrForce => {
console.info('scrollIfNeeded()', offsetOrForce);
if(typeof offsetOrForce === 'boolean' && offsetOrForce !== true)
return;
if(typeof offsetOrForce === 'number' && html.scrollTop < (html.scrollHeight - html.offsetHeight - offsetOrForce))
return;
doAutoScroll();
},
// probably not the way i want this to work going forward
switchChannel: channelName => {
console.info('switchChannel()', channelName);
//
doAutoScroll();
},
};
};

View file

@ -0,0 +1,82 @@
#include avatar.js
#include chat/garbparse.jsx
const MamiChatMessageAction = function(info) {
const avatarClasses = ['message__avatar'];
// TODO: make this not hardcoded probably (should be in its own object for the thing mentioned in messages.jsx)
let text;
let senderId = info.sender?.id ?? '-1';
let senderName = info.sender?.name ?? '';
let senderColour = info.sender?.colour ?? 'inherit';
if(info.type === 'msg:action') {
text = info.text;
} else if(info.type === 'user:add') {
text = 'has joined';
} else if(info.type === 'user:remove') {
if(info.detail.reason === 'kick') {
avatarClasses.push('avatar-filter-invert');
text = 'got bludgeoned to death';
} else if(info.detail.reason === 'flood') {
avatarClasses.push('avatar-filter-invert');
text = 'got kicked for flood protection';
} else if(info.detail.reason === 'timeout') {
avatarClasses.push('avatar-filter-greyscale');
text = 'exploded';
} else {
avatarClasses.push('avatar-filter-greyscale');
text = 'has disconnected';
}
} else if(info.type === 'chan:join') {
text = 'has joined the channel';
} else if(info.type === 'chan:leave') {
avatarClasses.push('avatar-filter-greyscale');
text = 'has left the channel';
} else if(info.type === 'user:nick') {
if(senderId === '-1') {
senderId = '0';
senderName = info.detail.previousName;
senderColour = 'inherit';
}
text = `changed their name to ${info.detail.name}`;
} else {
text = `unknown action type: ${info.type}!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`;
}
if(text.indexOf("'") !== 0 || (text.match(/\'/g).length % 2) === 0)
text = "\xA0" + text;
let textElem, userNameElem;
const html = <div class={`message message-tiny message--user-${senderId} message--first`}>
<div class={avatarClasses} style={`background-image: url('${MamiFormatUserAvatarUrl(senderId, 80)}');`} />
<div class="message__container">
<div class="message__meta">
{userNameElem = <div class="message__user" style={`color: ${senderColour}`}>
{senderName}
</div>}
{textElem = <div class="message-tiny-text">
{text}
</div>}
<div class="message__time">
{info.time.getHours().toString().padStart(2, '0')}:{info.time.getMinutes().toString().padStart(2, '0')}:{info.time.getSeconds().toString().padStart(2, '0')}
</div>
</div>
</div>
</div>;
MamiMolestMessageContents(info, info.type === 'msg:action' ? textElem : undefined, userNameElem);
return {
get element() { return html; },
matches: other => {
if(typeof other === 'object' && other !== null && 'id' in other)
other = other.id;
if(typeof other !== 'string')
return false;
return other === info.id;
},
};
};

View file

@ -0,0 +1,89 @@
#include avatar.js
#include chat/garbparse.jsx
const MamiChatMessageText = function(info) {
let text;
if(info.type === 'msg:text') {
text = info.text;
} else if(info.type.startsWith('legacy:')) {
const bi = info.botInfo;
if(bi.type === 'banlist') {
const bans = bi.args[0].split(', ');
for(const i in bans)
bans[i] = bans[i].slice(92, -4);
text = `Banned: ${bans.join(', ')}`;
} else if(bi.type === 'who') {
const users = bi.args[0].split(', ');
for(const i in users) {
const isSelf = users[i].includes(' style="font-weight: bold;"');
users[i] = users[i].slice(isSelf ? 102 : 75, -4);
if(isSelf) users[i] += ' (You)';
}
text = `Online: ${users.join(', ')}`;
} else if(bi.type === 'whochan') {
const users = bi.args[1].split(', ');
for(const i in users) {
const isSelf = users[i].includes(' style="font-weight: bold;"');
users[i] = users[i].slice(isSelf ? 102 : 75, -4);
if(isSelf) users[i] += ' (You)';
}
text = `Online in ${bi.args[0]}: ${users.join(', ')}`;
} else if(bi.type === 'say') {
text = bi.args[0];
} else if(bi.type === 'ipaddr') {
text = `IP address of ${bi.args[0]}: ${bi.args[1]}.`;
} else {
text = `unknown legacy text type: ${bi.type}!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`;
}
} else {
text = `unknown text type: ${info.type}!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`;
}
let textElem, userNameElem;
const html = <div class={`message message--user-${info.sender.id}`}>
<div class="message__avatar" style={`background-image: url('${MamiFormatUserAvatarUrl(info.sender.id, 80)}');`} />
<div class="message__container">
<div class="message__meta">
{userNameElem = <div class="message__user" style={`color: ${info.sender.colour}`}>
{info.sender.name}
</div>}
<div class="message__time">
{info.time.getHours().toString().padStart(2, '0')}:{info.time.getMinutes().toString().padStart(2, '0')}:{info.time.getSeconds().toString().padStart(2, '0')}
</div>
</div>
{textElem = <div class="message__text">
{text}
</div>}
</div>
</div>;
if(info.type === 'msg:text') {
if(info.sender.id === '136')
html.style.transform = 'scaleY(' + (0.76 + (0.01 * Math.max(0, Math.ceil(Date.now() / (7 * 24 * 60 * 60000)) - 2813))).toString() + ')';
MamiMolestMessageContents(info, textElem, userNameElem);
}
return {
get element() { return html; },
get info() { return info; },
get first() { return html.classList.contains('message--first'); },
set first(value) {
html.classList.toggle('message--first', value);
},
matches: other => {
if(typeof other === 'object' && other !== null && 'id' in other)
other = other.id;
if(typeof other !== 'string')
return false;
return other === info.id;
},
};
};

View file

@ -168,6 +168,7 @@ const MamiColourPicker = function(options) {
setPosition: setPosition, setPosition: setPosition,
close: runReject, close: runReject,
dialog: pos => { dialog: pos => {
html.classList.add('invisible');
html.classList.remove('hidden'); html.classList.remove('hidden');
if(pos instanceof MouseEvent) if(pos instanceof MouseEvent)
@ -191,6 +192,8 @@ const MamiColourPicker = function(options) {
if(pos !== undefined) if(pos !== undefined)
setPosition(pos); setPosition(pos);
html.classList.remove('invisible');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
promiseResolve = resolve; promiseResolve = resolve;
promiseReject = reject; promiseReject = reject;

View file

@ -111,38 +111,55 @@ const MamiEmotePicker = function(args) {
setPosition: setPosition, setPosition: setPosition,
close: close, close: close,
dialog: pos => { dialog: pos => {
emotes = args.getEmotes(); const dialogBody = () => {
buildList(); buildList();
html.classList.remove('hidden'); html.classList.add('invisible');
html.classList.remove('hidden');
if(pos instanceof MouseEvent) if(pos instanceof MouseEvent)
pos = pos.target; pos = pos.target;
if(pos instanceof Element) if(pos instanceof Element)
pos = pos.getBoundingClientRect(); pos = pos.getBoundingClientRect();
if(pos instanceof DOMRect) { if(pos instanceof DOMRect) {
const bbb = pos; const bbb = pos;
pos = {}; pos = {};
const mbb = html.getBoundingClientRect(); const mbb = html.getBoundingClientRect();
const pbb = html.parentNode.getBoundingClientRect(); const pbb = html.parentNode.getBoundingClientRect();
pos.right = pbb.width - bbb.left; pos.right = pbb.width - bbb.left;
pos.bottom = pbb.height - bbb.top; pos.bottom = pbb.height - bbb.top;
if(pos.right + mbb.width > pbb.width) if(pos.right + mbb.width > pbb.width)
pos.right = 0; pos.right = 0;
else if(pos.right > mbb.width) else if(pos.right > mbb.width)
pos.right -= mbb.width; pos.right -= mbb.width;
else else
pos.right -= bbb.width; pos.right -= bbb.width;
}
if(pos !== undefined)
setPosition(pos);
html.classList.remove('invisible');
searchElem.focus();
};
const result = args.getEmotes();
if(result instanceof Promise)
result.then(list => {
emotes = list;
}).catch(() => {
// uhhh just use the last version of the list LOL
}).finally(() => {
dialogBody();
});
else {
emotes = result;
dialogBody();
} }
if(pos !== undefined)
setPosition(pos);
searchElem.focus();
return new Promise(resolve => { promiseResolve = resolve; }); return new Promise(resolve => { promiseResolve = resolve; });
}, },
}; };

View file

@ -15,12 +15,12 @@ window.Umi = { UI: {} };
#include themes.js #include themes.js
#include txtrigs.js #include txtrigs.js
#include uniqstr.js #include uniqstr.js
#include users.js
#include utility.js #include utility.js
#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 chat/form.jsx
#include chat/messages.jsx
#include colpick/picker.jsx #include colpick/picker.jsx
#include controls/msgbox.jsx #include controls/msgbox.jsx
#include controls/ping.jsx #include controls/ping.jsx
@ -288,7 +288,15 @@ const MamiInit = async args => {
}); });
const chatForm = new MamiChatForm(ctx.events.scopeTo('form')); const messages = new MamiChatMessages(ctx.globalEvents);
settings.watch('autoScroll', ev => { messages.autoScroll = ev.detail.value; });
settings.watch('autoEmbedV1', ev => { messages.autoEmbed = ev.detail.value; });
const chatForm = new MamiChatForm(
async snippet => await sockChatClient.getUserInfosByName(snippet),
async snippet => MamiEmotes.findByName((await sockChatClient.getCurrentUserInfo()).perms.rank, snippet, true),
ctx.events.scopeTo('form')
);
window.addEventListener('keydown', ev => { window.addEventListener('keydown', ev => {
if((ev.ctrlKey && ev.key !== 'v') || ev.altKey) if((ev.ctrlKey && ev.key !== 'v') || ev.altKey)
@ -300,6 +308,7 @@ const MamiInit = async args => {
settings.watch('showMarkupSelector', ev => { settings.watch('showMarkupSelector', ev => {
chatForm.markup.visible = ev.detail.value !== 'never'; chatForm.markup.visible = ev.detail.value !== 'never';
messages.scrollIfNeeded(chatForm.markup.height);
Umi.UI.Messages.ScrollIfNeeded(chatForm.markup.height); Umi.UI.Messages.ScrollIfNeeded(chatForm.markup.height);
}); });
@ -331,10 +340,11 @@ const MamiInit = async args => {
}); });
const layout = new Umi.UI.ChatLayout(chatForm, sidebar); const layout = new Umi.UI.ChatLayout(messages, chatForm, sidebar);
await ctx.views.unshift(layout); await ctx.views.unshift(layout);
ctx.events.watch('form:resize', ev => { ctx.events.watch('form:resize', ev => {
messages.scrollIfNeeded(ev.detail.diffHeight);
Umi.UI.Messages.ScrollIfNeeded(ev.detail.diffHeight); Umi.UI.Messages.ScrollIfNeeded(ev.detail.diffHeight);
}); });
@ -410,39 +420,50 @@ 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: () => {} });
MamiCompat('Umi.UI.View.SetText', { value: text => { chatForm.input.setText(text); } }); MamiCompat('Umi.UI.View.SetText', { value: text => { chatForm.input.setText(text); } });
let sockChatClient;
const sbUsers = new MamiSidebarPanelUsers; const sbUsers = new MamiSidebarPanelUsers;
sidebar.createPanel(sbUsers); sidebar.createPanel(sbUsers);
sbUsers.addOption({ await sbUsers.addOption({
name: 'profile', name: 'profile',
text: 'View profile', text: 'View profile',
onclick: entry => window.open(futami.get('profile').replace('{user:id}', entry.id), '_blank'), onclick: entry => window.open(futami.get('profile').replace('{user:id}', entry.id), '_blank'),
}); });
sbUsers.addOption({ await sbUsers.addOption({
name: 'action', name: 'action',
text: 'Describe action', text: 'Describe action',
condition: entry => Umi.User.getCurrentUser()?.id === entry.id, condition: async entry => await sockChatClient.getCurrentUserId() === entry.id,
onclick: entry => { chatForm.input.setText('/me '); }, onclick: entry => { chatForm.input.setText('/me '); },
}); });
sbUsers.addOption({ await 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: async entry => {
const userInfo = await sockChatClient.getCurrentUserInfo();
return userInfo.id === entry.id && userInfo.perms.nick;
},
onclick: entry => { chatForm.input.setText('/nick '); }, onclick: entry => { chatForm.input.setText('/nick '); },
}); });
sbUsers.addOption({ await sbUsers.addOption({
name: 'bans', name: 'bans',
text: 'View bans', text: 'View bans',
condition: entry => Umi.User.getCurrentUser()?.id === entry.id && Umi.User.getCurrentUser().perms.canKick, condition: async entry => {
const userInfo = await sockChatClient.getCurrentUserInfo();
return userInfo.id === entry.id && userInfo.perms.kick;
},
onclick: entry => { Umi.Server.sendMessage('/bans'); }, onclick: entry => { Umi.Server.sendMessage('/bans'); },
}); });
sbUsers.addOption({ await sbUsers.addOption({
name: 'kfe', name: 'kfe',
text: 'Kick Fucking Everyone', text: 'Kick Fucking Everyone',
condition: entry => Umi.User.getCurrentUser()?.id === entry.id && Umi.User.getCurrentUser().perms.canKick, condition: async entry => {
const userInfo = await sockChatClient.getCurrentUserInfo();
return userInfo.id === entry.id && userInfo.perms.kick;
},
onclick: async entry => { onclick: async entry => {
try { try {
await ctx.msgbox.show({ await ctx.msgbox.show({
@ -451,30 +472,36 @@ const MamiInit = async args => {
no: { text: 'nah' }, no: { text: 'nah' },
}); });
const names = sbUsers.getAllUserNames(); const myInfo = await sockChatClient.getCurrentUserInfo();
for(const name of names) const userInfos = await sockChatClient.getUserInfos();
if(Umi.User.getCurrentUser()?.name !== name) for(const userInfo of userInfos)
// this shouldn't call it like this but will have to leave it for now if(myInfo.id !== userInfo.id)
Umi.Server.sendMessage(`/kick ${name}`); sockChatClient.sendMessage(`/kick ${userInfo.name}`);
} catch(ex) {} } catch(ex) {}
}, },
}); });
sbUsers.addOption({ await sbUsers.addOption({
name: 'dm', name: 'dm',
text: 'Send direct message', text: 'Send direct message',
condition: entry => Umi.User.getCurrentUser()?.id !== entry.id, condition: async entry => await sockChatClient.getCurrentUserId() !== entry.id,
onclick: entry => { chatForm.input.setText(`/msg ${entry.name} `); }, onclick: entry => { chatForm.input.setText(`/msg ${entry.name} `); },
}); });
sbUsers.addOption({ await 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: async entry => {
const userInfo = await sockChatClient.getCurrentUserInfo();
return userInfo.id !== entry.id && userInfo.perms.kick;
},
onclick: entry => { chatForm.input.setText(`/kick ${entry.name} `); }, onclick: entry => { chatForm.input.setText(`/kick ${entry.name} `); },
}); });
sbUsers.addOption({ await sbUsers.addOption({
name: 'ipaddr', name: 'ipaddr',
text: 'View IP address', text: 'View IP address',
condition: entry => Umi.User.hasCurrentUser() && Umi.User.getCurrentUser().id !== entry.id && Umi.User.getCurrentUser().perms.canKick, condition: async entry => {
const userInfo = await sockChatClient.getCurrentUserInfo();
return userInfo.id !== entry.id && userInfo.perms.kick;
},
onclick: entry => { Umi.Server.sendMessage(`/ip ${entry.name}`); }, onclick: entry => { Umi.Server.sendMessage(`/ip ${entry.name}`); },
}); });
@ -643,11 +670,11 @@ const MamiInit = async args => {
category.button('Import settings', () => { category.button('Import settings', () => {
(new MamiSettingsBackup(settings)).importUpload(args.parent); (new MamiSettingsBackup(settings)).importUpload(args.parent);
}, ['Your current settings will be replaced with the ones in the export.', 'Are you sure you want to continue?']); }, ['Your current settings will be replaced with the ones in the export.', 'Are you sure you want to continue?']);
category.button('Export settings', () => { category.button('Export settings', async () => {
const user = Umi.User.getCurrentUser(); const userInfo = await sockChatClient.getCurrentUserInfo();
let fileName; let fileName;
if(user !== null) if(userInfo !== undefined)
fileName = `${user.name}'s settings.mami`; fileName = `${userInfo.name}'s settings.mami`;
(new MamiSettingsBackup(settings)).exportDownload(args.parent, fileName); (new MamiSettingsBackup(settings)).exportDownload(args.parent, fileName);
}); });
@ -703,7 +730,7 @@ const MamiInit = async args => {
sidebar.createAction(new MamiSidebarActionScroll(settings)); sidebar.createAction(new MamiSidebarActionScroll(settings));
sidebar.createAction(new MamiSidebarActionSound(settings)); sidebar.createAction(new MamiSidebarActionSound(settings));
sidebar.createAction(new MamiSidebarActionCollapseAll); sidebar.createAction(new MamiSidebarActionCollapseAll);
sidebar.createAction(new MamiSidebarActionClearBacklog(settings, soundCtx.library, ctx.msgbox)); sidebar.createAction(new MamiSidebarActionClearBacklog(settings, soundCtx.library, ctx.msgbox, messages));
const pingIndicator = new MamiPingIndicator; const pingIndicator = new MamiPingIndicator;
const sbActPing = new MamiSidebarActionPing(pingIndicator, ctx.msgbox); const sbActPing = new MamiSidebarActionPing(pingIndicator, ctx.msgbox);
@ -721,7 +748,7 @@ const MamiInit = async args => {
onPick: emote => { onPick: emote => {
chatForm.input.insertAtCursor(`:${emote.strings[0]}:`); chatForm.input.insertAtCursor(`:${emote.strings[0]}:`);
}, },
getEmotes: () => MamiEmotes.all(Umi.User.getCurrentUser().perms.rank), getEmotes: async () => MamiEmotes.all((await sockChatClient.getCurrentUserInfo()).perms.rank),
setKeepOpenOnPick: value => { settings.set('keepEmotePickerOpen', value); }, setKeepOpenOnPick: value => { settings.set('keepEmotePickerOpen', value); },
}); });
layout.getElement().appendChild(emotePicker.element); layout.getElement().appendChild(emotePicker.element);
@ -742,14 +769,14 @@ const MamiInit = async args => {
ctx.eeprom = new MamiEEPROM(futami.get('eeprom2'), MamiMisuzuAuth.getLine); ctx.eeprom = new MamiEEPROM(futami.get('eeprom2'), MamiMisuzuAuth.getLine);
ctx.eeprom.init() ctx.eeprom.init()
.catch(ex => { console.error('Failed to initialise EEPROM.', ex); }) .catch(ex => { console.error('Failed to initialise EEPROM.', ex); })
.then(() => { .then(async () => {
sbUploads.addOption({ await sbUploads.addOption({
name: 'view', name: 'view',
text: 'View upload', text: 'View upload',
condition: entry => entry.uploadInfo !== undefined, condition: entry => entry.uploadInfo !== undefined,
onclick: entry => window.open(entry.uploadInfo.url), onclick: entry => window.open(entry.uploadInfo.url),
}); });
sbUploads.addOption({ await sbUploads.addOption({
name: 'insert', name: 'insert',
text: 'Insert into message', text: 'Insert into message',
condition: entry => entry.uploadInfo !== undefined, condition: entry => entry.uploadInfo !== undefined,
@ -769,7 +796,7 @@ const MamiInit = async args => {
chatForm.input.insertAtCursor(text); chatForm.input.insertAtCursor(text);
}, },
}); });
sbUploads.addOption({ await sbUploads.addOption({
name: 'delete', name: 'delete',
text: 'Delete upload', text: 'Delete upload',
condition: entry => entry.uploadInfo !== undefined, condition: entry => entry.uploadInfo !== undefined,
@ -791,20 +818,20 @@ const MamiInit = async args => {
task.onProgress(prog => { task.onProgress(prog => {
entry.progress = prog.progress; entry.progress = prog.progress;
}); });
entry.addOption({ await entry.addOption({
name: 'cancel', name: 'cancel',
text: 'Cancel upload', text: 'Cancel upload',
onclick: () => { task.abort(); }, onclick: () => { task.abort(); },
}); });
await entry.setOptionsVisible(true);
try { try {
const fileInfo = await task.start(); const fileInfo = await task.start();
entry.optionsVisible = false; await entry.setOptionsVisible(false);
entry.uploadInfo = fileInfo; entry.uploadInfo = fileInfo;
entry.removeOption('cancel'); await entry.removeOption('cancel');
entry.nukeProgress(); entry.nukeProgress();
sbUploads.reloadOptionsFor(entry);
if(settings.get('eepromAutoInsert')) if(settings.get('eepromAutoInsert'))
entry.clickOption('insert'); entry.clickOption('insert');
@ -839,7 +866,6 @@ const MamiInit = async args => {
}); });
ctx.events.watch('form:upload', ev => { ctx.events.watch('form:upload', ev => {
console.info(ev);
for(const file of ev.detail.files) for(const file of ev.detail.files)
doUpload(file); doUpload(file);
}); });
@ -922,6 +948,7 @@ const MamiInit = async args => {
// hack for DM channels // hack for DM channels
if(info.isUserChannel) { if(info.isUserChannel) {
sbChannels.setActiveEntry(info.name); sbChannels.setActiveEntry(info.name);
messages.switchChannel(info.name);
Umi.UI.Messages.SwitchChannel(info); Umi.UI.Messages.SwitchChannel(info);
} }
}; };
@ -959,7 +986,7 @@ const MamiInit = async args => {
const sockChatHandlers = new MamiSockChatHandlers( const sockChatHandlers = new MamiSockChatHandlers(
ctx, sockChat, setLoadingOverlay, sockChatReconnect, pingIndicator, ctx, sockChat, setLoadingOverlay, sockChatReconnect, pingIndicator,
sbActPing, sbChannels, sbUsers sbActPing, sbChannels, sbUsers, messages
); );
settings.watch('dumpEvents', ev => sockChatHandlers.setDumpEvents(ev.detail.value)); settings.watch('dumpEvents', ev => sockChatHandlers.setDumpEvents(ev.detail.value));
settings.watch('dumpPackets', ev => sockChat.setDumpPackets(ev.detail.value)); settings.watch('dumpPackets', ev => sockChat.setDumpPackets(ev.detail.value));
@ -995,6 +1022,7 @@ const MamiInit = async args => {
await protoWorker.connect(); await protoWorker.connect();
await sockChat.create(); await sockChat.create();
conMan.client = sockChat; conMan.client = sockChat;
sockChatClient = sockChat.client;
await conMan.start(); await conMan.start();
} finally { } finally {
workerStarting = false; workerStarting = false;

View file

@ -1,74 +0,0 @@
#include users.js
const MamiMessageAuthorInfo = function(self = false, user = null, id = null, name = null, colour = null, rank = null, avatar = null) {
if(typeof self !== 'boolean')
throw 'self must be a boolean';
if(user === null) {
id ??= '';
name ??= '';
colour ??= 'inherit';
rank ??= 0;
avatar ??= new MamiUserAvatarInfo(id);
} else {
if(typeof user !== 'object')
throw 'user must be an object or null';
id ??= user.id;
name ??= user.name;
colour ??= user.colour;
rank ??= user.perms.rank;
avatar ??= user.avatar;
}
if(typeof id !== 'string')
throw 'id must be a string';
if(typeof name !== 'string')
throw 'name must be a string';
if(typeof colour !== 'string')
throw 'colour must be a string';
if(typeof rank !== 'number')
throw 'rank must be a number';
if(typeof avatar !== 'object')
throw 'avatar must be an object';
return {
get self() { return self; },
get user() { return user; },
get hasUser() { return user !== null; },
get id() { return id; },
get name() { return name; },
get colour() { return colour; },
get rank() { return rank; },
get avatar() { return avatar; },
};
};
const MamiMessageInfo = function(type, created = null, detail = null, id = '', author = null, channel = '', silent = false) {
if(typeof type !== 'string')
throw 'type must be a string';
if(created === null)
created = new Date;
else if(!(created instanceof Date))
throw 'created must be an instance of window.Date or null';
if(typeof id !== 'string')
throw 'id must be a string';
if(typeof author !== 'object')
throw 'author must be an object';
if(typeof channel !== 'string')
throw 'channel must be a string';
if(typeof silent !== 'boolean')
throw 'silent must be a boolean';
return {
get type() { return type; },
get created() { return created; },
get detail() { return detail; },
get id() { return id; },
get author() { return author; },
get channel() { return channel; },
get silent() { return silent; },
};
};

View file

@ -1,7 +1,7 @@
#include awaitable.js #include awaitable.js
#include ui/messages.jsx #include ui/messages.jsx
const MamiSidebarActionClearBacklog = function(settings, sndLib, msgBox) { const MamiSidebarActionClearBacklog = function(settings, sndLib, msgBox, messages) {
return { return {
get name() { return 'act:clear-backlog'; }, get name() { return 'act:clear-backlog'; },
get text() { return 'Clear backlog'; }, get text() { return 'Clear backlog'; },
@ -20,6 +20,7 @@ const MamiSidebarActionClearBacklog = function(settings, sndLib, msgBox) {
sndLib.play('misc:explode'); sndLib.play('misc:explode');
messages.clearMessages(settings.get('explosionRadius'));
Umi.UI.Messages.Clear(settings.get('explosionRadius')); Umi.UI.Messages.Clear(settings.get('explosionRadius'));
await MamiSleep(1700); await MamiSleep(1700);

View file

@ -1,3 +1,4 @@
#include animate.js
#include utility.js #include utility.js
const MamiSidebarPanelUploadsEntry = function(fileInfo) { const MamiSidebarPanelUploadsEntry = function(fileInfo) {
@ -5,8 +6,8 @@ const MamiSidebarPanelUploadsEntry = function(fileInfo) {
let uploadInfo; let uploadInfo;
let detailsElem, thumbElem, nameElem, progElem, optsElem; let detailsElem, thumbElem, nameElem, progElem, optsElem;
const html = <div class="sidebar__user" style="background: linear-gradient(270deg, transparent 0, #111 40%) #222; margin-bottom: 1px;"> const html = <div class="sidebar__user" style="background: linear-gradient(270deg, transparent 0, var(--theme-colour-sidebar-background) 40%) var(--theme-colour-sidebar-background-highlight)">
{detailsElem = <div class="sidebar__user-details" title={fileInfo.name} style="transition: height .2s" onclick={() => { optsElem.classList.toggle('hidden'); }}> {detailsElem = <div class="sidebar__user-details" title={fileInfo.name} style="transition: height .2s" onclick={() => { toggleOptions(); }}>
{thumbElem = <div class="sidebar__user-avatar hidden" style="transition: width .2s, height .2s" onmouseover={() => { {thumbElem = <div class="sidebar__user-avatar hidden" style="transition: width .2s, height .2s" onmouseover={() => {
thumbElem.style.width = '100px'; thumbElem.style.width = '100px';
detailsElem.style.height = thumbElem.style.height = '100px'; detailsElem.style.height = thumbElem.style.height = '100px';
@ -17,9 +18,98 @@ const MamiSidebarPanelUploadsEntry = function(fileInfo) {
{nameElem = <div class="sidebar__user-name" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{fileInfo.name}</div>} {nameElem = <div class="sidebar__user-name" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{fileInfo.name}</div>}
</div>} </div>}
{progElem = <progress class="eeprom-item-progress" max="100" value="0"/>} {progElem = <progress class="eeprom-item-progress" max="100" value="0"/>}
{optsElem = <div class="sidebar__user-options"/>} {optsElem = <div class="sidebar__user-options hidden"/>}
</div>; </div>;
let optionsVisible = false, optionsAnim, optionsTimeout;
const toggleOptions = async state => {
if(state === undefined)
state = !optionsVisible;
else if(state === optionsVisible)
return;
if(optionsTimeout !== undefined) {
clearTimeout(optionsTimeout);
optionsTimeout = undefined;
}
if(optionsAnim !== undefined) {
optionsAnim.cancel();
optionsAnim = undefined;
}
let start, update, end, height;
if(state) {
await reloadOptions();
start = () => {
optsElem.classList.remove('hidden');
const curHeight = optsElem.style.height;
optsElem.style.height = null;
height = optsElem.clientHeight;
optsElem.style.height = curHeight;
};
update = function(t) {
optsElem.style.height = `${height * t}px`;
};
end = () => {
optsElem.style.height = null;
};
} else {
start = () => {
height = optsElem.clientHeight;
};
update = t => {
optsElem.style.height = `${height - (height * t)}px`;
};
end = () => {
optsElem.style.height = '0';
optsElem.classList.add('hidden');
$rc(optsElem);
};
}
optionsAnim = MamiAnimate({
async: true,
delayed: true,
duration: 500,
easing: 'outExpo',
start: start,
update: update,
end: end,
});
optionsVisible = state;
return optionsAnim.start();
};
const reloadOptions = async () => {
for(const option of options.values()) {
if(typeof option.info.condition === 'function' && !await option.info.condition(public)) {
if(optsElem.contains(option.element))
optsElem.removeChild(option.element);
continue;
}
if(option.element === undefined) {
let text;
if(typeof option.info.formatText === 'function')
text = option.info.formatText(public);
else
text = option.info.text ?? option.info.name;
option.element = <div class="sidebar__user-option" onclick={ev => {
if(typeof option.info.onclick === 'function')
option.info.onclick(public, option.element, ev);
}}>{text}</div>;
}
if(!optsElem.contains(option.element))
optsElem.appendChild(option.element);
}
};
const public = { const public = {
get element() { return html; }, get element() { return html; },
@ -56,10 +146,8 @@ const MamiSidebarPanelUploadsEntry = function(fileInfo) {
progElem.value = Math.ceil(value * progElem.max); progElem.value = Math.ceil(value * progElem.max);
}, },
get optionsVisible() { return !optsElem.classList.contains('hidden'); }, get optionsVisible() { return optionsVisible; },
set optionsVisible(value) { setOptionsVisible: toggleOptions,
optsElem.classList.toggle('hidden', !value);
},
nukeProgress: () => { nukeProgress: () => {
if(progElem === undefined) if(progElem === undefined)
@ -71,42 +159,41 @@ const MamiSidebarPanelUploadsEntry = function(fileInfo) {
hasOption: name => options.has(name), hasOption: name => options.has(name),
getOptionNames: () => Array.from(options.keys()), getOptionNames: () => Array.from(options.keys()),
addOption: option => { addOption: async option => {
if(options.has(option.name)) if(options.has(option.name))
throw 'option has already been defined'; throw 'option has already been defined';
let text;
if(typeof option.formatText === 'function')
text = option.formatText(public);
else
text = option.text ?? option.name;
const elem = <div class="sidebar__user-option" onclick={ev => {
if(typeof option.onclick === 'function')
option.onclick(public, elem, ev);
}}>{text}</div>;
options.set(option.name, { options.set(option.name, {
option: option, info: option,
element: elem, element: undefined,
}); });
optsElem.appendChild(elem);
if(!optionsVisible)
await reloadOptions();
}, },
removeOption: name => { removeOption: async name => {
const info = options.get(name); const info = options.get(name);
if(info === undefined) if(info === undefined)
return; return;
optsElem.removeChild(info.element); if(optsElem.contains(info.element))
optsElem.removeChild(info.element);
options.delete(name); options.delete(name);
if(!optionsVisible)
await reloadOptions();
}, },
clickOption: name => { clickOption: name => {
const info = options.get(name) ?? options.get(`:${name}`); const option = options.get(name) ?? options.get(`:${name}`);
if(info === undefined) if(option === undefined)
return; return;
info.element.click(); if(option.element !== undefined)
option.element.click();
else if(typeof option.info.onclick === 'function')
option.info.onclick(public);
}, },
reloadOptions: reloadOptions,
}; };
return public; return public;
@ -117,27 +204,24 @@ const MamiSidebarPanelUploads = function() {
const options = new Map; const options = new Map;
const entries = []; const entries = [];
const reloadOptionsFor = entry => { const reloadOptionsFor = async entry => {
const names = entry.getOptionNames(); const names = entry.getOptionNames();
for(const name of names) for(const name of names)
if(name.startsWith(':') && !options.has(name)) if(name.startsWith(':') && !options.has(name))
entry.removeOption(name); await entry.removeOption(name);
for(const [name, option] of options) { for(const [name, option] of options)
if(typeof option.condition !== 'function' || option.condition(entry)) { if(!names.includes(name))
if(!names.includes(name)) await entry.addOption(option);
entry.addOption(option);
} else { if(entry.optionsVisible)
if(names.includes(name)) await entry.reloadOptions();
entry.removeOption(name);
}
}
}; };
const reloadOptions = () => { const reloadOptions = async () => {
for(const entry of entries) for(const entry of entries)
reloadOptionsFor(entry); await reloadOptionsFor(entry);
}; };
return { return {
@ -163,20 +247,21 @@ const MamiSidebarPanelUploads = function() {
if(!entries.includes(entry)) if(!entries.includes(entry))
return; return;
html.removeChild(entry.element); if(html.contains(entry.element))
html.removeChild(entry.element);
$ari(entries, entry); $ari(entries, entry);
}, },
addOption: option => { addOption: async option => {
if(!option.name.startsWith(':')) if(!option.name.startsWith(':'))
option.name = `:${option.name}`; option.name = `:${option.name}`;
if(options.has(option.name)) if(options.has(option.name))
throw 'option has already been defined'; throw 'option has already been defined';
options.set(option.name, option); options.set(option.name, option);
reloadOptions(); await reloadOptions();
}, },
removeOption: name => { removeOption: async name => {
if(!name.startsWith(':')) if(!name.startsWith(':'))
name = `:${name}`; name = `:${name}`;
@ -184,7 +269,7 @@ const MamiSidebarPanelUploads = function() {
return; return;
options.delete(name); options.delete(name);
reloadOptions(); await reloadOptions();
}, },
reloadOptions: reloadOptions, reloadOptions: reloadOptions,
reloadOptionsFor: reloadOptionsFor, reloadOptionsFor: reloadOptionsFor,

View file

@ -1,5 +1,5 @@
#include animate.js #include animate.js
#include users.js #include avatar.js
#include utility.js #include utility.js
const MamiSidebarPanelUsersEntry = function(info) { const MamiSidebarPanelUsersEntry = function(info) {
@ -13,7 +13,7 @@ const MamiSidebarPanelUsersEntry = function(info) {
const options = new Map; const options = new Map;
let avatarElem, nameElem, nameWrapperElem, statusElem, optsElem; let avatarElem, nameElem, nameWrapperElem, statusElem, optsElem;
const html = <div class="sidebar__user"> const html = <div class="sidebar__user">
<div class="sidebar__user-details" onclick={() => { setOptionsVisible(); }}> <div class="sidebar__user-details" onclick={() => { toggleOptions(); }}>
{avatarElem = <div class="sidebar__user-avatar" />} {avatarElem = <div class="sidebar__user-avatar" />}
{nameElem = <div class="sidebar__user-name"/>} {nameElem = <div class="sidebar__user-name"/>}
</div> </div>
@ -63,7 +63,7 @@ const MamiSidebarPanelUsersEntry = function(info) {
setStatusMessage(statusMessage); setStatusMessage(statusMessage);
let optionsVisible = false, optionsAnim, optionsTimeout; let optionsVisible = false, optionsAnim, optionsTimeout;
const setOptionsVisible = state => { const toggleOptions = async state => {
if(state === undefined) if(state === undefined)
state = !optionsVisible; state = !optionsVisible;
else if(state === optionsVisible) else if(state === optionsVisible)
@ -85,9 +85,11 @@ const MamiSidebarPanelUsersEntry = function(info) {
if(mami.settings.get('autoCloseUserContext')) if(mami.settings.get('autoCloseUserContext'))
optionsTimeout = setTimeout(() => { optionsTimeout = setTimeout(() => {
if(mami.settings.get('autoCloseUserContext')) if(mami.settings.get('autoCloseUserContext'))
setOptionsVisible(false); toggleOptions(false);
}, 300000); }, 300000);
await reloadOptions();
start = () => { start = () => {
optsElem.classList.remove('hidden'); optsElem.classList.remove('hidden');
const curHeight = optsElem.style.height; const curHeight = optsElem.style.height;
@ -111,6 +113,7 @@ const MamiSidebarPanelUsersEntry = function(info) {
end = () => { end = () => {
optsElem.style.height = '0'; optsElem.style.height = '0';
optsElem.classList.add('hidden'); optsElem.classList.add('hidden');
$rc(optsElem);
}; };
} }
@ -129,10 +132,34 @@ const MamiSidebarPanelUsersEntry = function(info) {
return optionsAnim.start(); return optionsAnim.start();
}; };
let avatar; const reloadOptions = async () => {
for(const option of options.values()) {
if(typeof option.info.condition === 'function' && !await option.info.condition(public)) {
if(optsElem.contains(option.element))
optsElem.removeChild(option.element);
continue;
}
if(option.element === undefined) {
let text;
if(typeof option.info.formatText === 'function')
text = option.info.formatText(public);
else
text = option.info.text ?? option.info.name;
option.element = <div class="sidebar__user-option" onclick={ev => {
if(typeof option.info.onclick === 'function')
option.info.onclick(public, option.element, ev);
}}>{text}</div>;
}
if(!optsElem.contains(option.element))
optsElem.appendChild(option.element);
}
};
const updateAvatar = url => { const updateAvatar = url => {
avatar = new MamiUserAvatarInfo(id); avatarElem.style.backgroundImage = `url('${MamiFormatUserAvatarUrl(id, 60)}')`;
avatarElem.style.backgroundImage = `url('${avatar.x60}')`;
}; };
updateAvatar(); updateAvatar();
@ -160,48 +187,47 @@ const MamiSidebarPanelUsersEntry = function(info) {
set statusMessage(value) { setStatusMessage(value); }, set statusMessage(value) { setStatusMessage(value); },
get optionsVisible() { return optionsVisible; }, get optionsVisible() { return optionsVisible; },
setOptionsVisible: setOptionsVisible, setOptionsVisible: toggleOptions,
updateAvatar: updateAvatar, updateAvatar: updateAvatar,
hasOption: name => options.has(name), hasOption: name => options.has(name),
getOptionNames: () => Array.from(options.keys()), getOptionNames: () => Array.from(options.keys()),
addOption: option => { addOption: async option => {
if(options.has(option.name)) if(options.has(option.name))
throw 'option has already been defined'; throw 'option has already been defined';
let text;
if(typeof option.formatText === 'function')
text = option.formatText(public);
else
text = option.text ?? option.name;
const elem = <div class="sidebar__user-option" onclick={ev => {
if(typeof option.onclick === 'function')
option.onclick(public, elem, ev);
}}>{text}</div>;
options.set(option.name, { options.set(option.name, {
option: option, info: option,
element: elem, element: undefined,
}); });
optsElem.appendChild(elem);
if(!optionsVisible)
await reloadOptions();
}, },
removeOption: name => { removeOption: async name => {
const info = options.get(name); const info = options.get(name);
if(info === undefined) if(info === undefined)
return; return;
optsElem.removeChild(info.element); if(optsElem.contains(info.element))
optsElem.removeChild(info.element);
options.delete(name); options.delete(name);
if(!optionsVisible)
await reloadOptions();
}, },
clickOption: name => { clickOption: name => {
const info = options.get(name); const option = options.get(name) ?? options.get(`:${name}`);
if(info === undefined) if(option === undefined)
return; return;
info.element.click(); if(option.element !== undefined)
option.element.click();
else if(typeof option.info.onclick === 'function')
option.info.onclick(public);
}, },
reloadOptions: reloadOptions,
}; };
return public; return public;
@ -212,27 +238,24 @@ const MamiSidebarPanelUsers = function() {
const options = new Map; const options = new Map;
const entries = new Map; const entries = new Map;
const reloadOptionsFor = entry => { const reloadOptionsFor = async entry => {
const names = entry.getOptionNames(); const names = entry.getOptionNames();
for(const name of names) for(const name of names)
if(name.startsWith(':') && !options.has(name)) if(name.startsWith(':') && !options.has(name))
entry.removeOption(name); await entry.removeOption(name);
for(const [name, option] of options) { for(const [name, option] of options)
if(typeof option.condition !== 'function' || option.condition(entry)) { if(!names.includes(name))
if(!names.includes(name)) await entry.addOption(option);
entry.addOption(option);
} else { if(entry.optionsVisible)
if(names.includes(name)) await entry.reloadOptions();
entry.removeOption(name);
}
}
}; };
const reloadOptions = () => { const reloadOptions = async () => {
for(const entry of entries) for(const entry of entries)
reloadOptionsFor(entry); await reloadOptionsFor(entry);
}; };
return { return {
@ -245,14 +268,6 @@ const MamiSidebarPanelUsers = function() {
button.element.append(<i class="fas fa-users sidebar-gutter-font-icon"/>); button.element.append(<i class="fas fa-users sidebar-gutter-font-icon"/>);
}, },
// this method is cope (used for KFE exclusive please don't use it elsewhere cuz it will get nuked)
getAllUserNames: () => {
const names = [];
for(const [id, entry] of entries)
names.push(entry.name);
return names;
},
createEntry: info => { createEntry: info => {
if(entries.has(info.id)) if(entries.has(info.id))
throw 'this user has already been inserted'; throw 'this user has already been inserted';
@ -301,16 +316,16 @@ const MamiSidebarPanelUsers = function() {
} }
}, },
addOption: option => { addOption: async option => {
if(!option.name.startsWith(':')) if(!option.name.startsWith(':'))
option.name = `:${option.name}`; option.name = `:${option.name}`;
if(options.has(option.name)) if(options.has(option.name))
throw 'option has already been defined'; throw 'option has already been defined';
options.set(option.name, option); options.set(option.name, option);
reloadOptions(); await reloadOptions();
}, },
removeOption: name => { removeOption: async name => {
if(!name.startsWith(':')) if(!name.startsWith(':'))
name = `:${name}`; name = `:${name}`;
@ -318,7 +333,7 @@ const MamiSidebarPanelUsers = function() {
return; return;
options.delete(name); options.delete(name);
reloadOptions(); await reloadOptions();
}, },
reloadOptions: reloadOptions, reloadOptions: reloadOptions,
reloadOptionsFor: reloadOptionsFor, reloadOptionsFor: reloadOptionsFor,

View file

@ -1,7 +1,5 @@
#include animate.js #include animate.js
#include messages.js
#include parsing.js #include parsing.js
#include users.js
#include notices/baka.jsx #include notices/baka.jsx
#include sockchat/modal.js #include sockchat/modal.js
#include ui/emotes.js #include ui/emotes.js
@ -9,7 +7,7 @@
const MamiSockChatHandlers = function( const MamiSockChatHandlers = function(
ctx, client, setLoadingOverlay, sockChatReconnect, pingIndicator, ctx, client, setLoadingOverlay, sockChatReconnect, pingIndicator,
sbActPing, sbChannels, sbUsers sbActPing, sbChannels, sbUsers, messages
) { ) {
if(typeof ctx !== 'object' || ctx === null) if(typeof ctx !== 'object' || ctx === null)
throw 'ctx must be an non-null object'; throw 'ctx must be an non-null object';
@ -86,20 +84,6 @@ const MamiSockChatHandlers = function(
if(dumpEvents) console.log('session:start', ev.detail); if(dumpEvents) console.log('session:start', ev.detail);
sockChatRestarting = false; sockChatRestarting = false;
const userInfo = new MamiUserInfo(
ev.detail.user.id,
ev.detail.user.name,
ev.detail.user.colour,
new MamiUserStatusInfo(ev.detail.user.status.isAway, ev.detail.user.status.message),
new MamiUserPermsInfo(
ev.detail.user.perms.rank, ev.detail.user.perms.kick,
ev.detail.user.perms.nick, ev.detail.user.perms.chan,
),
);
Umi.User.setCurrentUser(userInfo);
Umi.Users.Add(userInfo);
sbUsers.createEntry(ev.detail.user); sbUsers.createEntry(ev.detail.user);
if(ctx.views.count > 1) if(ctx.views.count > 1)
@ -133,76 +117,32 @@ const MamiSockChatHandlers = function(
if(ev.detail.user.self) if(ev.detail.user.self)
return; return;
const userInfo = new MamiUserInfo(
ev.detail.user.id,
ev.detail.user.name,
ev.detail.user.colour,
new MamiUserStatusInfo(ev.detail.user.status.isAway, ev.detail.user.status.message),
new MamiUserPermsInfo(
ev.detail.user.perms.rank, ev.detail.user.perms.kick,
ev.detail.user.perms.nick, ev.detail.user.perms.chan,
),
);
Umi.Users.Add(userInfo);
sbUsers.createEntry(ev.detail.user); sbUsers.createEntry(ev.detail.user);
if(ev.detail.msg !== undefined) if(ev.detail.msg !== undefined) {
Umi.UI.Messages.Add(new MamiMessageInfo( messages.addMessage(ev.detail.msg);
'user:join', Umi.UI.Messages.Add(ev.detail.msg);
ev.detail.msg.time, }
null,
ev.detail.msg.id,
new MamiMessageAuthorInfo(ev.detail.user.self, userInfo),
ev.detail.msg.channel
));
}; };
handlers['user:remove'] = ev => { handlers['user:remove'] = ev => {
if(dumpEvents) console.log('user:remove', ev.detail); if(dumpEvents) console.log('user:remove', ev.detail);
sbUsers.deleteEntry(ev.detail.user.id); sbUsers.deleteEntry(ev.detail.user.id);
const userInfo = Umi.Users.Get(ev.detail.user.id); if(ev.detail.msg !== undefined) {
if(userInfo === null) messages.addMessage(ev.detail.msg);
return; Umi.UI.Messages.Add(ev.detail.msg);
}
if(ev.detail.msg !== undefined)
Umi.UI.Messages.Add(new MamiMessageInfo(
'user:leave',
ev.detail.msg.time,
{ reason: ev.detail.leave.type },
ev.detail.msg.id,
new MamiMessageAuthorInfo(ev.detail.user.self, userInfo),
ev.detail.msg.channel
));
Umi.Users.Remove(userInfo);
}; };
handlers['user:update'] = ev => { handlers['user:update'] = ev => {
if(dumpEvents) console.log('user:update', ev.detail); if(dumpEvents) console.log('user:update', ev.detail);
sbUsers.updateEntry(ev.detail.user.id, ev.detail.user); sbUsers.updateEntry(ev.detail.user.id, ev.detail.user);
const userInfo = Umi.Users.Get(ev.detail.user.id);
userInfo.name = ev.detail.user.name;
userInfo.colour = ev.detail.user.colour;
userInfo.avatar = new MamiUserAvatarInfo(ev.detail.user.id);
userInfo.status = new MamiUserStatusInfo(ev.detail.user.status.isAway, ev.detail.user.status.message);
userInfo.perms = new MamiUserPermsInfo(
ev.detail.user.perms.rank, ev.detail.user.perms.kick,
ev.detail.user.perms.nick, ev.detail.user.perms.chan,
);
Umi.Users.Update(userInfo.id, userInfo);
}; };
handlers['user:clear'] = () => { handlers['user:clear'] = () => {
if(dumpEvents) console.log('user:clear'); if(dumpEvents) console.log('user:clear');
sbUsers.clearEntries(); sbUsers.clearEntries();
const self = Umi.User.getCurrentUser();
Umi.Users.Clear();
if(self !== undefined)
Umi.Users.Add(self);
}; };
@ -230,33 +170,18 @@ const MamiSockChatHandlers = function(
if(dumpEvents) console.log('chan:focus', ev.detail); if(dumpEvents) console.log('chan:focus', ev.detail);
sbChannels.setActiveEntry(ev.detail.channel.name); sbChannels.setActiveEntry(ev.detail.channel.name);
messages.switchChannel(ev.detail.channel.name);
Umi.UI.Messages.SwitchChannel(ev.detail.channel); Umi.UI.Messages.SwitchChannel(ev.detail.channel);
}; };
handlers['chan:join'] = ev => { handlers['chan:join'] = ev => {
if(dumpEvents) console.log('chan:join', ev.detail); if(dumpEvents) console.log('chan:join', ev.detail);
const userInfo = new MamiUserInfo(
ev.detail.user.id,
ev.detail.user.name,
ev.detail.user.colour,
new MamiUserStatusInfo(ev.detail.user.status.isAway, ev.detail.user.status.message),
new MamiUserPermsInfo(
ev.detail.user.perms.rank, ev.detail.user.perms.kick,
ev.detail.user.perms.nick, ev.detail.user.perms.chan,
)
);
Umi.Users.Add(userInfo);
sbUsers.createEntry(ev.detail.user); sbUsers.createEntry(ev.detail.user);
if(ev.detail.msg !== undefined) if(ev.detail.msg !== undefined) {
Umi.UI.Messages.Add(new MamiMessageInfo( messages.addMessage(ev.detail.msg);
'channel:join', Umi.UI.Messages.Add(ev.detail.msg);
null, null, }
ev.detail.msg.id,
new MamiMessageAuthorInfo(ev.detail.user.self, userInfo),
ev.detail.msg.channel
));
}; };
handlers['chan:leave'] = ev => { handlers['chan:leave'] = ev => {
if(dumpEvents) console.log('chan:leave', ev.detail); if(dumpEvents) console.log('chan:leave', ev.detail);
@ -264,135 +189,29 @@ const MamiSockChatHandlers = function(
if(ev.detail.user.self) if(ev.detail.user.self)
return; return;
const userInfo = Umi.Users.Get(ev.detail.user.id); if(ev.detail.msg !== undefined) {
if(userInfo === null) messages.addMessage(ev.detail.msg);
return; Umi.UI.Messages.Add(ev.detail.msg);
}
if(ev.detail.msg !== undefined)
Umi.UI.Messages.Add(new MamiMessageInfo(
'channel:leave',
null, null,
ev.detail.msg.id,
new MamiMessageAuthorInfo(ev.detail.user.self, userInfo),
ev.detail.msg.channel
));
Umi.Users.Remove(userInfo);
}; };
handlers['msg:add'] = ev => { handlers['msg:add'] = ev => {
if(dumpEvents) console.log('msg:add', ev.detail); if(dumpEvents) console.log('msg:add', ev.detail);
const senderInfo = ev.detail.msg.sender; if(ev.detail.msg.type.startsWith('legacy:') && modals.handled(ev.detail.msg.botInfo.type)) {
const rawUserInfo = Umi.Users.Get(senderInfo.id); modals.show(ev.detail.msg.botInfo.type, ev.detail.msg.botInfo.args);
const userInfo = senderInfo.name === undefined return;
? rawUserInfo
: new MamiUserInfo(
senderInfo.id,
senderInfo.name,
senderInfo.colour,
new MamiUserStatusInfo(senderInfo.status.isAway, senderInfo.status.message),
new MamiUserPermsInfo(
senderInfo.perms.rank, senderInfo.perms.kick,
senderInfo.perms.nick, senderInfo.perms.chan,
)
);
// hack
let channelName = ev.detail.msg.channel;
if(channelName !== undefined && channelName.startsWith('@~')) {
const chanUserInfo = Umi.Users.Get(channelName.substring(2));
if(chanUserInfo !== null)
channelName = `@${chanUserInfo.name}`;
} }
// also hack sbChannels.setUnreadEntry(ev.detail.msg.channel);
if(ev.detail.msg.flags.isPM && !sbChannels.hasEntry(channelName)) messages.addMessage(ev.detail.msg);
sbChannels.createEntry({ Umi.UI.Messages.Add(ev.detail.msg);
name: channelName,
hasPassword: false,
isTemporary: true,
isUserChannel: true,
});
let type, detail, author;
if(ev.detail.msg.isBot) {
const botInfo = ev.detail.msg.botInfo;
let authorMethod;
if(botInfo.type === 'join') {
type = 'user:join';
authorMethod = 'nameArg';
} else if(['leave', 'kick', 'flood', 'timeout'].includes(botInfo.type)) {
type = 'user:leave';
authorMethod = 'nameArg';
detail = { reason: botInfo.type };
} else if(botInfo.type === 'jchan') {
type = 'channel:join';
authorMethod = 'nameArg';
} else if(botInfo.type === 'lchan') {
type = 'channel:leave';
authorMethod = 'nameArg';
}
if(authorMethod === 'nameArg') {
author = botInfo.args[0];
authorMethod = 'name';
}
if(authorMethod === 'name') {
const botUserInfo = Umi.Users.FindExact(author);
author = new MamiMessageAuthorInfo(
Umi.User.isCurrentUser(botUserInfo),
botUserInfo,
null,
author
);
}
if(typeof type !== 'string') {
if(modals.handled(botInfo.type)) {
modals.show(botInfo.type, botInfo.args);
return;
}
type = `legacy:${botInfo.type}`;
detail = {
error: botInfo.isError,
args: botInfo.args,
};
}
} else {
author = new MamiMessageAuthorInfo(
senderInfo.self,
rawUserInfo,
senderInfo.id ?? rawUserInfo?.id,
senderInfo.name ?? rawUserInfo?.name,
senderInfo.colour ?? rawUserInfo?.colour,
senderInfo.perms?.rank ?? rawUserInfo?.perms?.rank ?? 0,
senderInfo.avatar ?? rawUserInfo?.avatar ?? new MamiUserAvatarInfo(senderInfo.id ?? rawUserInfo?.id ?? '0'),
);
type = `message:${ev.detail.msg.flags.isAction ? 'action' : 'text'}`;
detail = { body: ev.detail.msg.text };
}
sbChannels.setUnreadEntry(channelName);
Umi.UI.Messages.Add(new MamiMessageInfo(
type,
ev.detail.msg.time,
detail,
ev.detail.msg.id,
author,
channelName,
ev.detail.msg.silent,
));
}; };
handlers['msg:remove'] = ev => { handlers['msg:remove'] = ev => {
if(dumpEvents) console.log('msg:remove', ev.detail); if(dumpEvents) console.log('msg:remove', ev.detail);
messages.removeMessage(ev.detail.msg.id);
Umi.UI.Messages.Remove(ev.detail.msg.id); Umi.UI.Messages.Remove(ev.detail.msg.id);
}; };

View file

@ -1,8 +1,8 @@
#include utility.js #include utility.js
#include ui/chat-message-list.js #include ui/chat-message-list.js
Umi.UI.ChatInterface = function(chatForm) { Umi.UI.ChatInterface = function(messages, chatForm) {
const messages = new Umi.UI.ChatMessageList; const messagesOld = new Umi.UI.ChatMessageList;
const html = $e({ const html = $e({
attrs: { attrs: {
@ -10,13 +10,14 @@ Umi.UI.ChatInterface = function(chatForm) {
}, },
child: [ child: [
messages, messages,
messagesOld,
chatForm, chatForm,
], ],
}); });
return { return {
getMessageList: function() { getMessageList: function() {
return messages; return messagesOld;
}, },
getElement: function() { getElement: function() {
return html; return html;

View file

@ -2,8 +2,8 @@
#include ui/chat-interface.js #include ui/chat-interface.js
// 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(chatForm, sideBar) { Umi.UI.ChatLayout = function(messages, chatForm, sideBar) {
const main = new Umi.UI.ChatInterface(chatForm); const main = new Umi.UI.ChatInterface(messages, chatForm);
const html = $e({ const html = $e({
attrs: { attrs: {

View file

@ -1,13 +1,9 @@
#include avatar.js
#include common.js #include common.js
#include parsing.js
#include title.js #include title.js
#include txtrigs.js #include txtrigs.js
#include url.js
#include users.js
#include utility.js #include utility.js
#include weeb.js
#include sound/umisound.js #include sound/umisound.js
#include ui/emotes.js
Umi.UI.Messages = (function() { Umi.UI.Messages = (function() {
let focusChannelName = ''; let focusChannelName = '';
@ -29,113 +25,35 @@ Umi.UI.Messages = (function() {
}; };
const botMsgs = { const botMsgs = {
'say': { text: '%0' }, 'join': { sound: 'join' },
'join': { text: '%0 has joined.', action: 'has joined', sound: 'join' }, 'leave': { sound: 'leave' },
'leave': { text: '%0 has disconnected.', action: 'has disconnected', avatar: 'greyscale', sound: 'leave' }, 'jchan': { sound: 'join' },
'jchan': { text: '%0 has joined the channel.', action: 'has joined the channel', sound: 'join' }, 'lchan': { sound: 'leave' },
'lchan': { text: '%0 has left the channel.', action: 'has left the channel', avatar: 'greyscale', sound: 'leave' }, 'kick': { sound: 'kick' },
'kick': { text: '%0 got bludgeoned to death.', action: 'got bludgeoned to death', avatar: 'invert', sound: 'kick' }, 'flood': { sound: 'flood' },
'flood': { text: '%0 got kicked for flood protection.', action: 'got kicked for flood protection', avatar: 'invert', sound: 'flood' }, 'timeout': { sound: 'timeout' },
'timeout': { text: '%0 exploded.', action: 'exploded', avatar: 'greyscale', sound: 'timeout' },
'nick': { text: '%0 changed their name to %1.', action: 'changed their name to %1' },
'ipaddr': { text: 'IP address of %0 is %1.' },
'banlist': {
text: 'Banned: %0',
filter: args => {
const bans = args[0].split(', ');
for(const i in bans)
bans[i] = bans[i].slice(92, -4);
args[0] = bans.join(', ');
return args;
},
},
'who': {
text: 'Online: %0',
filter: args => {
const users = args[0].split(', ');
for(const i in users) {
const isSelf = users[i].includes(' style="font-weight: bold;"');
users[i] = users[i].slice(isSelf ? 102 : 75, -4);
if(isSelf) users[i] += ' (You)';
}
args[0] = users.join(', ');
return args;
},
},
'whochan': {
text: 'Online in %0: %1',
filter: args => {
const users = args[1].split(', ');
for(const i in users) {
const isSelf = users[i].includes(' style="font-weight: bold;"');
users[i] = users[i].slice(isSelf ? 102 : 75, -4);
if(isSelf) users[i] += ' (You)';
}
args[1] = users.join(', ');
return args;
},
},
};
const formatTemplate = (template, args) => {
if(typeof template !== 'string')
template = '';
if(Array.isArray(args))
for(let i = 0; i < args.length; ++i) {
const arg = args[i] === undefined || args[i] === null ? '' : args[i].toString();
template = template.replace(new RegExp(`%${i}`, 'g'), arg);
}
return template;
}; };
return { return {
Add: function(msg) { Add: async function(msg) {
const elementId = `message-${msg.id}`; const elementId = `message-${msg.id}`;
if(msg.id !== '' && $i(elementId)) if(msg.id !== '' && $i(elementId))
return; return;
let isTiny = false;
let skipTextParsing = false;
let msgText = '';
let msgTextLong = '';
let msgAuthor = msg.author;
let soundIsLegacy = true; let soundIsLegacy = true;
let soundName; let soundName;
let soundVolume; let soundVolume;
let soundRate; let soundRate;
const eText = <div/>; const eBase = <div class="message">{msg.id ?? 'no id'}</div>;
const eAvatar = <div class="message__avatar"/>;
const eUser = <div class="message__user"/>;
const eMeta = <div class="message__meta">{eUser}</div>;
const eTime = <div class="message__time">
{msg.created.getHours().toString().padStart(2, '0')}
:{msg.created.getMinutes().toString().padStart(2, '0')}
:{msg.created.getSeconds().toString().padStart(2, '0')}
</div>;
const eContainer = <div class="message__container">{eMeta}</div>;
const eBase = <div class="message">
{eAvatar}
{eContainer}
</div>;
if(msg.type.startsWith('message:')) { if(msg.type.startsWith('msg:')) {
msgText = msgTextLong = msg.detail.body;
soundName = msg.author?.self === true ? 'outgoing' : 'incoming'; soundName = msg.author?.self === true ? 'outgoing' : 'incoming';
if(msg.type === 'message:action')
isTiny = true;
if(mami.settings.get('playJokeSounds')) if(mami.settings.get('playJokeSounds'))
try { try {
const trigger = mami.textTriggers.getTrigger(msgText); const trigger = mami.textTriggers.getTrigger(msg.text);
if(trigger.isSoundType) { if(trigger.isSoundType) {
soundIsLegacy = false; soundIsLegacy = false;
soundName = trigger.getRandomSoundName(); soundName = trigger.getRandomSoundName();
@ -146,128 +64,43 @@ Umi.UI.Messages = (function() {
} else { } else {
let bIsError = false; let bIsError = false;
let bType; let bType;
let bArgs;
if(msg.type === 'user:join') { if(msg.type === 'user:add') {
bType = 'join'; bType = 'join';
bArgs = [msgAuthor.name]; } else if(msg.type === 'user:remove') {
} else if(msg.type === 'user:leave') {
bType = msg.detail.reason; bType = msg.detail.reason;
bArgs = [msgAuthor.name]; } else if(msg.type === 'chan:join') {
} else if(msg.type === 'channel:join') {
bType = 'jchan'; bType = 'jchan';
bArgs = [msgAuthor.name]; } else if(msg.type === 'chan:leave') {
} else if(msg.type === 'channel:leave') {
bType = 'lchan'; bType = 'lchan';
bArgs = [msgAuthor.name];
} else if(msg.type.startsWith('legacy:')) { } else if(msg.type.startsWith('legacy:')) {
bType = msg.type.substring(7); bType = msg.botInfo.type;
bIsError = msg.detail.error; bIsError = msg.botInfo.isError;
bArgs = msg.detail.args;
} }
soundName = bIsError ? 'error' : 'server'; soundName = bIsError ? 'error' : 'server';
if(botMsgs.hasOwnProperty(bType)) { if(botMsgs.hasOwnProperty(bType)) {
const bmInfo = botMsgs[bType]; const bmInfo = botMsgs[bType];
if(typeof bmInfo.filter === 'function')
bArgs = bmInfo.filter(bArgs);
if(typeof bmInfo.sound === 'string') if(typeof bmInfo.sound === 'string')
soundName = bmInfo.sound; soundName = bmInfo.sound;
}
let actionSuccess = false;
if(typeof bmInfo.action === 'string')
if(msgAuthor) {
actionSuccess = true;
isTiny = true;
skipTextParsing = true;
msgText = formatTemplate(bmInfo.action, bArgs);
if(typeof bmInfo.avatar === 'string')
eAvatar.classList.add(`avatar-filter-${bmInfo.avatar}`);
}
msgTextLong = formatTemplate(bmInfo.text, bArgs);
if(!actionSuccess)
msgText = msgTextLong;
} else
msgText = msgTextLong = `!!! Received unsupported message type: ${msg.type} !!!`;
} }
if(msgAuthor !== null) { if(focusChannelName !== '' && msg.channel && msg.channel !== focusChannelName)
eUser.style.color = msgAuthor.colour;
eUser.textContent = msgAuthor.name;
}
if(isTiny) {
eText.classList.add('message-tiny-text');
eBase.classList.add('message-tiny');
if(msgText.indexOf("'") !== 0 || (msgText.match(/\'/g).length % 2) === 0)
msgText = "\xA0" + msgText;
eMeta.append(eText, eTime);
} else {
eText.classList.add('message__text');
eMeta.append(eTime);
eContainer.append(eText);
if(msgAuthor?.id === '136')
eBase.style.transform = 'scaleY(' + (0.76 + (0.01 * Math.max(0, Math.ceil(Date.now() / (7 * 24 * 60 * 60000)) - 2813))).toString() + ')';
}
eBase.classList.add(`message--user-${msgAuthor?.id ?? '-1'}`);
if(focusChannelName !== '' && msg.channel !== '' && msg.channel !== focusChannelName)
eBase.classList.add('hidden'); eBase.classList.add('hidden');
if(msg.id !== '') { if(msg.id) {
eBase.id = elementId; eBase.id = elementId;
eBase.dataset.id = msg.id; eBase.dataset.id = msg.id;
} }
if(msgAuthor) if(msg.sender)
eBase.dataset.author = msgAuthor.id; eBase.dataset.author = msg.sender.id;
if(msg.channel !== '') if(msg.channel)
eBase.dataset.channel = msg.channel; eBase.dataset.channel = msg.channel;
if(isTiny) eBase.dataset.created = msg.time.toISOString();
eBase.dataset.tiny = '1';
eBase.dataset.created = msg.created.toISOString();
eBase.dataset.body = msgText; eBase.dataset.body = msg.text;
eText.innerText = msgText;
if(!skipTextParsing) {
Umi.UI.Emoticons.Parse(eText, msgAuthor);
Umi.Parsing.Parse(eText, msg);
const textSplit = eText.innerText.split(' ');
for(const textPart of textSplit) {
const uri = Umi.URI.Parse(textPart);
if(uri !== null && uri.Slashes !== null) {
const anchorElem = <a class="markup__link" href={textPart} target="_blank" rel="nofollow noreferrer noopener">{textPart}</a>;
eText.innerHTML = eText.innerHTML.replace(textPart.replace(/&/g, '&amp;'), anchorElem.outerHTML);
}
}
if(mami.settings.get('weeaboo')) {
eUser.appendChild($t(Weeaboo.getNameSuffix(msgAuthor)));
eText.appendChild($t(Weeaboo.getTextSuffix(msgAuthor)));
const kaomoji = Weeaboo.getRandomKaomoji(true, msg);
if(kaomoji)
eText.append(` ${kaomoji}`);
}
}
const avatarUrl = msgAuthor?.avatar?.[isTiny ? 'x40' : 'x80'] ?? '';
if(avatarUrl.length > 0)
eAvatar.style.backgroundImage = `url('${avatarUrl}')`;
else
eAvatar.classList.add('message__avatar--disabled');
const msgsList = $i('umi-messages'); const msgsList = $i('umi-messages');
@ -282,38 +115,22 @@ Umi.UI.Messages = (function() {
eBase.classList.toggle('message--first', shouldDisplayAuthorInfo(eBase, insertAfter)); eBase.classList.toggle('message--first', shouldDisplayAuthorInfo(eBase, insertAfter));
if(eBase.dataset.tiny !== insertAfter.dataset.tiny)
eBase.classList.add(isTiny ? 'message-tiny-fix' : 'message-big-fix');
insertAfter.after(eBase); insertAfter.after(eBase);
if(eBase.nextElementSibling instanceof Element) if(eBase.nextElementSibling instanceof Element)
eBase.nextElementSibling.classList.toggle('message--first', shouldDisplayAuthorInfo(eBase.nextElementSibling, eBase)); eBase.nextElementSibling.classList.toggle('message--first', shouldDisplayAuthorInfo(eBase.nextElementSibling, eBase));
} else { } else {
eBase.classList.add('message--first'); eBase.classList.add('message--first');
if(isTiny) eBase.classList.add('message-tiny-fix');
msgsList.append(eBase); msgsList.append(eBase);
} }
if(!eBase.classList.contains('hidden')) {
if(mami.settings.get('autoEmbedV1')) {
const callEmbedOn = eBase.querySelectorAll('a[onclick^="Umi.Parser.SockChatBBcode.Embed"]');
for(const embedElem of callEmbedOn)
if(embedElem.dataset.embed !== '1')
embedElem.click();
}
if(mami.settings.get('autoScroll'))
eBase.scrollIntoView({ inline: 'end' });
}
let isMentioned = false; let isMentioned = false;
const mentionTriggers = mami.settings.get('notificationTriggers').toLowerCase().split(' '); const mentionTriggers = mami.settings.get('notificationTriggers').toLowerCase().split(' ');
const currentUser = Umi.User.getCurrentUser(); const currentUser = await Umi.Server.getCurrentUserInfo();
if(typeof currentUser === 'object' && typeof currentUser.name === 'string') if(typeof currentUser === 'object' && typeof currentUser.name === 'string')
mentionTriggers.push(currentUser.name.toLowerCase()); mentionTriggers.push(currentUser.name.toLowerCase());
const mentionText = ` ${msgTextLong} `.toLowerCase(); const mentionText = ` ${msg.text} `.toLowerCase();
for(const trigger of mentionTriggers) { for(const trigger of mentionTriggers) {
if(trigger.trim() === '') if(trigger.trim() === '')
continue; continue;
@ -329,7 +146,7 @@ Umi.UI.Messages = (function() {
if(document.hidden) { if(document.hidden) {
if(mami.settings.get('flashTitle')) { if(mami.settings.get('flashTitle')) {
let titleText = msgAuthor?.name ?? msgTextLong; let titleText = msg.sender?.name ?? msg.text;
if(focusChannelName !== '' && focusChannelName !== msg.channel) if(focusChannelName !== '' && focusChannelName !== msg.channel)
titleText += ` @ ${msg.channel}`; titleText += ` @ ${msg.channel}`;
@ -343,14 +160,12 @@ Umi.UI.Messages = (function() {
if(mami.settings.get('enableNotifications') && isMentioned) { if(mami.settings.get('enableNotifications') && isMentioned) {
const options = {}; const options = {};
options.icon = MamiFormatUserAvatarUrl(msg.sender.id, 100);
options.body = 'Click here to see what they said.'; options.body = 'Click here to see what they said.';
if(mami.settings.get('notificationShowMessage')) if(mami.settings.get('notificationShowMessage'))
options.body += "\n" + msgTextLong; options.body += `\n${msg.text}`;
if(avatarUrl.length > 0) const notif = new Notification(`${msg.sender.name} mentioned you!`, options);
options.icon = avatarUrl;
const notif = new Notification(`${msgAuthor.name} mentioned you!`, options);
notif.addEventListener('click', () => { notif.addEventListener('click', () => {
window.focus(); window.focus();
}); });
@ -368,12 +183,6 @@ Umi.UI.Messages = (function() {
mami.sound.library.play(soundName, soundVolume, soundRate); mami.sound.library.play(soundName, soundVolume, soundRate);
} }
mami.globalEvents.dispatch('umi:ui:message_add', { element: eBase });
},
IsScrolledToBottom: () => {
const msgsList = $i('umi-messages');
return msgsList.scrollTop === (msgsList.scrollHeight - msgsList.offsetHeight);
}, },
ScrollIfNeeded: (offsetOrForce = 0) => { ScrollIfNeeded: (offsetOrForce = 0) => {
const msgsList = $i('umi-messages'); const msgsList = $i('umi-messages');

View file

@ -1,183 +0,0 @@
const MamiUserPermsInfo = function(rank = 0, canKick = false, canSetNick = false, canCreateChannels = false) {
if(typeof rank !== 'number')
throw 'rank must be a number';
if(typeof canKick !== 'boolean')
throw 'canKick must be a boolean';
if(typeof canSetNick !== 'boolean')
throw 'canSetNick must be a boolean';
if(typeof canCreateChannels !== 'boolean')
throw 'canCreateChannels must be a boolean';
return {
get rank() { return rank; },
get canKick() { return canKick; },
get canSetNick() { return canSetNick; },
get canCreateChannels() { return canCreateChannels; },
};
};
const MamiUserStatusInfo = function(isAway = false, message = '') {
if(typeof isAway !== 'boolean')
throw 'isAway must be a boolean';
if(typeof message !== 'string')
throw 'message must be a string';
return {
get isAway() { return isAway; },
get message() { return message; },
};
};
const MamiUserAvatarInfo = function(userId = null) {
userId ??= '';
if(typeof userId !== 'string')
throw 'userId must be a string or null';
const template = futami.get('avatar') ?? '';
const changeTime = Date.now();
const getAvatar = res => {
return template.replace('{user:id}', userId)
.replace('{resolution}', res)
.replace('{user:avatar_change}', changeTime);
};
return {
get original() { return getAvatar('0'); },
get x80() { return getAvatar('80'); },
get x60() { return getAvatar('60'); },
get x40() { return getAvatar('40'); },
};
};
const MamiUserInfo = function(id, name, colour = 'inherit', status = null, perms = null, avatar = null) {
if(typeof id !== 'string')
throw 'id must be a string';
if(typeof name !== 'string')
throw 'name must be a string';
if(typeof colour !== 'string') // should be like, object or something maybe
throw 'colour must be a string';
if(status === null)
status = new MamiUserStatusInfo;
else if(typeof status !== 'object')
throw 'status must be an object';
if(perms === null)
perms = new MamiUserPermsInfo;
else if(typeof perms !== 'object')
throw 'perms must be an object';
if(avatar === null)
avatar = new MamiUserAvatarInfo(id);
else if(typeof avatar !== 'object')
throw 'avatar must be an object';
return {
get id() { return id; },
get name() { return name; },
set name(value) {
if(typeof value !== 'string')
throw 'value must be a string';
name = value;
},
get colour() { return colour; },
set colour(value) {
if(typeof value !== 'string') // ^
throw 'value must be a string';
colour = value;
},
get status() { return status; },
set status(value) {
if(typeof value !== 'object' || value === null)
throw 'value must be an object';
status = value;
},
get perms() { return perms; },
set perms(value) {
if(typeof value !== 'object' || value === null)
throw 'value must be an object';
perms = value;
},
get avatar() { return avatar; },
set avatar(value) {
if(typeof value !== 'object' || value === null)
throw 'value must be an object';
avatar = value;
},
};
};
Umi.User = (() => {
let userInfo;
return {
hasCurrentUser: () => userInfo !== undefined,
getCurrentUser: () => userInfo,
setCurrentUser: value => { userInfo = value; },
isCurrentUser: otherInfo => otherInfo !== null && typeof otherInfo === 'object' && typeof otherInfo.id === 'string'
&& userInfo !== null && typeof userInfo === 'object'
&& typeof userInfo.id === 'string'
&& (userInfo === otherInfo || userInfo.id === otherInfo.id),
};
})();
Umi.Users = (function() {
const users = new Map;
return {
Add: function(user) {
const userId = user.id;
if(!users.has(userId)) {
users.set(userId, user);
}
},
Remove: function(user) {
const userId = user.id;
if(users.has(userId)) {
users.delete(userId);
}
},
Clear: function() {
users.clear();
},
All: function() {
return Array.from(users.values());
},
Get: function(userId) {
userId = userId.toString();
if(users.has(userId))
return users.get(userId);
return null;
},
Find: function(userName) {
const found = [];
userName = userName.toLowerCase();
users.forEach(function(user) {
if(user.name.toLowerCase().includes(userName))
found.push(user);
});
return found;
},
FindExact: function(userName) {
if(typeof userName !== 'string')
return null;
userName = userName.toLowerCase();
for(const user of users.values())
if(user.name.toLowerCase() === userName)
return user;
return null;
},
Update: function(userId, user) {
userId = userId.toString();
users.set(userId, user);
},
};
})();

View file

@ -36,12 +36,12 @@ const Weeaboo = (function() {
}; };
pub.getNameSuffix = function(user) { pub.getNameSuffix = function(user) {
if(typeof user !== 'object' || user === null) if(typeof user !== 'object' || user === null || user === undefined)
return ''; return '';
if(user.rank >= 10) if(user.perms.rank >= 10)
return '-sama'; return '-sama';
if(user.rank >= 5) if(user.perms.rank >= 5)
return '-sensei'; return '-sensei';
if(user.colour.toLowerCase() === '#f02d7d') if(user.colour.toLowerCase() === '#f02d7d')
return '-san'; return '-san';
@ -59,7 +59,7 @@ const Weeaboo = (function() {
}; };
pub.getTextSuffix = function(user) { pub.getTextSuffix = function(user) {
if(typeof user !== 'object' || user === null) if(typeof user !== 'object' || user === null || user === undefined)
return ''; return '';
const userId = user.id; const userId = user.id;

View file

@ -37,11 +37,15 @@ const SockChatS2CContextClear = (ctx, mode) => {
if(mode === '0' || mode === '3' || mode === '4') if(mode === '0' || mode === '3' || mode === '4')
ctx.dispatch('msg:clear'); ctx.dispatch('msg:clear');
if(mode === '1' || mode === '3' || mode === '4') if(mode === '1' || mode === '3' || mode === '4') {
ctx.users.clear();
ctx.dispatch('user:clear'); ctx.dispatch('user:clear');
}
if(mode === '2' || mode === '4') if(mode === '2' || mode === '4') {
ctx.channels.clear();
ctx.dispatch('chan:clear'); ctx.dispatch('chan:clear');
}
}; };
const SockChatS2CChannelPopulate = (ctx, count, ...args) => { const SockChatS2CChannelPopulate = (ctx, count, ...args) => {
@ -50,13 +54,19 @@ const SockChatS2CChannelPopulate = (ctx, count, ...args) => {
for(let i = 0; i < count; ++i) { for(let i = 0; i < count; ++i) {
const offset = 3 * i; const offset = 3 * i;
const chanInfo = {
name: args[offset],
hasPassword: args[offset + 1] !== '0',
isTemporary: args[offset + 2] !== '0',
};
ctx.channels.set(chanInfo.name, chanInfo);
ctx.dispatch('chan:add', { ctx.dispatch('chan:add', {
channel: { channel: {
name: args[offset], name: chanInfo.name,
hasPassword: args[offset + 1] !== '0', hasPassword: chanInfo.hasPassword,
isTemporary: args[offset + 2] !== '0', isTemporary: chanInfo.name.isTemporary,
isCurrent: args[offset] === ctx.channelName, isCurrent: chanInfo.name === ctx.channelName,
}, },
}); });
} }
@ -67,27 +77,42 @@ const SockChatS2CChannelPopulate = (ctx, count, ...args) => {
}; };
const SockChatS2CChannelAdd = (ctx, name, hasPass, isTemp) => { const SockChatS2CChannelAdd = (ctx, name, hasPass, isTemp) => {
const chanInfo = {
name: name,
hasPassword: hasPass !== '0',
isTemporary: isTemp !== '0',
};
ctx.channels.set(chanInfo.name, chanInfo);
ctx.dispatch('chan:add', { ctx.dispatch('chan:add', {
channel: { channel: chanInfo,
name: name,
hasPassword: hasPass !== '0',
isTemporary: isTemp !== '0',
},
}); });
}; };
const SockChatS2CChannelUpdate = (ctx, prevName, name, hasPass, isTemp) => { const SockChatS2CChannelUpdate = (ctx, prevName, name, hasPass, isTemp) => {
const chanInfo = ctx.channels.get(prevName);
if(prevName !== name) {
chanInfo.name = name;
ctx.channels.delete(prevName);
ctx.channels.set(name, chanInfo);
}
chanInfo.hasPassword = hasPass !== '0';
chanInfo.isTemporary = isTemp !== '0';
ctx.dispatch('chan:update', { ctx.dispatch('chan:update', {
channel: { channel: {
previousName: prevName, previousName: prevName,
name: name, name: chanInfo.name,
hasPassword: hasPass !== '0', hasPassword: chanInfo.hasPassword,
isTemporary: isTemp !== '0', isTemporary: chanInfo.isTemporary,
}, },
}); });
}; };
const SockChatS2CChannelRemove = (ctx, name) => { const SockChatS2CChannelRemove = (ctx, name) => {
ctx.channels.delete(name);
ctx.dispatch('chan:remove', { ctx.dispatch('chan:remove', {
channel: { name: name }, channel: { name: name },
}); });
@ -100,103 +125,120 @@ const SockChatS2CUserPopulate = (ctx, count, ...args) => {
for(let i = 0; i < count; ++i) { for(let i = 0; i < count; ++i) {
const offset = 5 * i; const offset = 5 * i;
const statusInfo = SockChatParseStatusInfo(args[offset + 1]); const statusInfo = SockChatParseStatusInfo(args[offset + 1]);
const userInfo = {
id: args[offset],
self: args[offset] === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(args[offset + 2]),
perms: SockChatParseUserPerms(args[offset + 3]),
hidden: args[offset + 4] !== '0',
};
ctx.users.set(userInfo.id, userInfo);
ctx.dispatch('user:add', { ctx.dispatch('user:add', {
user: { user: userInfo,
id: args[offset],
self: args[offset] === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(args[offset + 2]),
perms: SockChatParseUserPerms(args[offset + 3]),
hidden: args[offset + 4] !== '0',
},
}); });
} }
}; };
const SockChatS2CUserAdd = (ctx, timeStamp, userId, userName, userColour, userPerms, msgId) => { const SockChatS2CUserAdd = (ctx, timeStamp, userId, userName, userColour, userPerms, msgId) => {
const statusInfo = SockChatParseStatusInfo(userName); const statusInfo = SockChatParseStatusInfo(userName);
const userInfo = {
id: userId,
self: userId === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(userColour),
perms: SockChatParseUserPerms(userPerms),
};
ctx.users.set(userInfo.id, userInfo);
ctx.dispatch('user:add', { ctx.dispatch('user:add', {
msg: { msg: {
type: 'user:add',
id: msgId, id: msgId,
time: new Date(parseInt(timeStamp) * 1000), time: new Date(parseInt(timeStamp) * 1000),
channel: ctx.channelName, channel: ctx.channelName,
sender: userInfo,
}, },
user: { user: userInfo,
id: userId,
self: userId === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(userColour),
perms: SockChatParseUserPerms(userPerms),
},
}); });
}; };
const SockChatS2CUserUpdate = (ctx, userId, userName, userColour, userPerms) => { const SockChatS2CUserUpdate = (ctx, userId, userName, userColour, userPerms) => {
const statusInfo = SockChatParseStatusInfo(userName); const statusInfo = SockChatParseStatusInfo(userName);
const userInfo = ctx.users.get(userId);
userInfo.self = userId === ctx.userId;
userInfo.name = statusInfo.name;
userInfo.status = statusInfo.status;
userInfo.colour = SockChatParseUserColour(userColour);
userInfo.perms = SockChatParseUserPerms(userPerms);
ctx.users.set(userInfo.id, userInfo);
ctx.dispatch('user:update', { ctx.dispatch('user:update', {
user: { user: userInfo,
id: userId,
self: userId === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(userColour),
perms: SockChatParseUserPerms(userPerms),
},
}); });
}; };
const SockChatS2CUserRemove = (ctx, userId, userName, reason, timeStamp, msgId) => { const SockChatS2CUserRemove = (ctx, userId, userName, reason, timeStamp, msgId) => {
const statusInfo = SockChatParseStatusInfo(userName); const statusInfo = SockChatParseStatusInfo(userName);
const userInfo = ctx.users.get(userId);
userInfo.name = statusInfo.name;
userInfo.status = statusInfo.status;
ctx.users.delete(userId);
ctx.dispatch('user:remove', { ctx.dispatch('user:remove', {
leave: { type: reason }, leave: { type: reason },
msg: { msg: {
type: 'user:remove',
id: msgId, id: msgId,
time: new Date(parseInt(timeStamp) * 1000), time: new Date(parseInt(timeStamp) * 1000),
channel: ctx.channelName, channel: ctx.channelName,
sender: userInfo,
detail: { reason: reason },
}, },
user: { user: userInfo,
id: userId,
self: userId === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
},
}); });
}; };
const SockChatS2CUserChannelJoin = (ctx, userId, userName, userColour, userPerms, msgId) => { const SockChatS2CUserChannelJoin = (ctx, userId, userName, userColour, userPerms, msgId) => {
const statusInfo = SockChatParseStatusInfo(userName); const statusInfo = SockChatParseStatusInfo(userName);
const userInfo = {
id: userId,
self: userId === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(userColour),
perms: SockChatParseUserPerms(userPerms),
};
ctx.users.set(userInfo.id, userInfo);
ctx.dispatch('chan:join', { ctx.dispatch('chan:join', {
user: { user: userInfo,
id: userId,
self: userId === ctx.userId,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(userColour),
perms: SockChatParseUserPerms(userPerms),
},
msg: { msg: {
type: 'chan:join',
id: msgId, id: msgId,
time: new Date,
channel: ctx.channelName, channel: ctx.channelName,
sender: userInfo,
}, },
}); });
}; };
const SockChatS2CUserChannelLeave = (ctx, userId, msgId) => { const SockChatS2CUserChannelLeave = (ctx, userId, msgId) => {
const userInfo = ctx.users.get(userId);
ctx.users.delete(userId); // should this happen? probably not!
ctx.dispatch('chan:leave', { ctx.dispatch('chan:leave', {
user: { user: userInfo,
id: userId,
self: userId === ctx.userId,
},
msg: { msg: {
type: 'chan:leave',
id: msgId, id: msgId,
time: new Date,
channel: ctx.channelName, channel: ctx.channelName,
sender: userInfo,
}, },
}); });
}; };
@ -214,6 +256,7 @@ const SockChatS2CMessagePopulate = (ctx, timeStamp, userId, userName, userColour
const statusInfo = SockChatParseStatusInfo(userName); const statusInfo = SockChatParseStatusInfo(userName);
const info = { const info = {
msg: { msg: {
type: `msg:${mFlags.isAction ? 'action' : 'text'}`,
time: new Date(parseInt(timeStamp) * 1000), time: new Date(parseInt(timeStamp) * 1000),
channel: ctx.channelName, channel: ctx.channelName,
sender: { sender: {
@ -227,7 +270,6 @@ const SockChatS2CMessagePopulate = (ctx, timeStamp, userId, userName, userColour
isBot: userId === '-1', isBot: userId === '-1',
silent: msgNotify === '0', silent: msgNotify === '0',
flags: mFlags, flags: mFlags,
flagsRaw: msgFlags,
text: SockChatUnfuckText(msgText, mFlags.isAction), text: SockChatUnfuckText(msgText, mFlags.isAction),
}, },
}; };
@ -243,8 +285,51 @@ const SockChatS2CMessagePopulate = (ctx, timeStamp, userId, userName, userColour
args: botParts.slice(2), args: botParts.slice(2),
}; };
if(info.msg.botInfo.type === 'say') let botUserNameIndex = 2;
info.msg.botInfo.args[0] = SockChatUnfuckText(info.msg.botInfo.args[0]); if(info.msg.botInfo.type === 'say') {
info.msg.botInfo.args[0] = info.msg.text = SockChatUnfuckText(info.msg.botInfo.args[0], mFlags.isAction);
} else if(info.msg.botInfo.type === 'join') {
info.msg.type = 'user:add';
} else if(['leave', 'kick', 'flood', 'timeout'].includes(info.msg.botInfo.type)) {
info.msg.type = 'user:remove';
info.msg.detail = { reason: info.msg.botInfo.type };
} else if(info.msg.botInfo.type === 'nick') {
info.msg.type = 'user:nick';
info.msg.detail = {
previousName: info.msg.botInfo.args[0],
name: info.msg.botInfo.args[1],
};
} else if(info.msg.botInfo.type === 'jchan') {
info.msg.type = 'chan:join';
} else if(info.msg.botInfo.type === 'lchan') {
info.msg.type = 'chan:leave';
} else {
info.msg.type = `legacy:${info.msg.botInfo.type}`;
info.msg.detail = {
error: info.msg.botInfo.isError,
args: info.msg.botInfo.args,
};
}
if(['join', 'leave', 'kick', 'flood', 'timeout', 'jchan', 'lchan', 'nick'].includes(botParts[1])) {
info.msg.sender = undefined;
const botUserTarget = botParts[botUserNameIndex].toLowerCase();
for(const botUserInfo of ctx.users.values())
if(botUserInfo.name.toLowerCase() === botUserTarget) {
info.msg.sender = botUserInfo;
break;
}
if(info.msg.sender === undefined)
info.msg.sender = {
id: '0',
self: false,
name: botParts[botUserNameIndex],
colour: 'inherit',
perms: { rank: 0 },
};
}
} }
ctx.dispatch('msg:add', info); ctx.dispatch('msg:add', info);
@ -255,27 +340,43 @@ const SockChatS2CMessageAdd = (ctx, timeStamp, userId, msgText, msgId, msgFlags)
let mText = SockChatUnfuckText(msgText, mFlags.isAction); let mText = SockChatUnfuckText(msgText, mFlags.isAction);
let mChannelName = ctx.channelName; let mChannelName = ctx.channelName;
const userInfo = userId === '-1' ? {
id: '-1',
self: false,
name: 'ChatBot',
colour: 'inherit',
perms: { rank: 0 },
} : ctx.users.get(userId);
if(msgFlags[4] !== '0') { if(msgFlags[4] !== '0') {
if(userId === ctx.userId) { if(userId === ctx.userId) {
const mTextParts = mText.split(' '); const mTextParts = mText.split(' ');
mChannelName = `@${mTextParts.shift()}`; mChannelName = `@${mTextParts.shift()}`;
mText = mTextParts.join(' '); mText = mTextParts.join(' ');
} else { } else
mChannelName = `@~${userId}`; mChannelName = `@${userInfo.name}`;
if(!ctx.channels.has(mChannelName)) {
const chanInfo = {
name: mChannelName,
hasPassword: false,
isTemporary: true,
isUserChannel: true,
};
ctx.channels.set(chanInfo.name, chanInfo);
ctx.dispatch('chan:add', { channel: chanInfo });
} }
} }
const msgInfo = { const msgInfo = {
msg: { msg: {
type: `msg:${mFlags.isAction ? 'action' : 'text'}`,
id: msgId, id: msgId,
time: new Date(parseInt(timeStamp) * 1000), time: new Date(parseInt(timeStamp) * 1000),
channel: mChannelName, channel: mChannelName,
sender: { sender: userInfo,
id: userId,
self: userId === ctx.userId,
},
flags: mFlags, flags: mFlags,
flagsRaw: msgFlags,
isBot: userId === '-1', isBot: userId === '-1',
text: mText, text: mText,
}, },
@ -288,6 +389,50 @@ const SockChatS2CMessageAdd = (ctx, timeStamp, userId, msgText, msgId, msgFlags)
type: botParts[1], type: botParts[1],
args: botParts.slice(2), args: botParts.slice(2),
}; };
let botUserNameIndex = 2;
if(msgInfo.msg.botInfo.type === 'join') {
msgInfo.msg.type = 'user:add';
} else if(['leave', 'kick', 'flood', 'timeout'].includes(msgInfo.msg.botInfo.type)) {
msgInfo.msg.type = 'user:remove';
msgInfo.msg.detail = { reason: msgInfo.msg.botInfo.type };
} else if(msgInfo.msg.botInfo.type === 'nick') {
msgInfo.msg.type = 'user:nick';
msgInfo.msg.detail = {
previousName: msgInfo.msg.botInfo.args[0],
name: msgInfo.msg.botInfo.args[1],
};
} else if(msgInfo.msg.botInfo.type === 'jchan') {
msgInfo.msg.type = 'chan:join';
} else if(msgInfo.msg.botInfo.type === 'lchan') {
msgInfo.msg.type = 'chan:leave';
} else {
msgInfo.msg.type = `legacy:${msgInfo.msg.botInfo.type}`;
msgInfo.msg.detail = {
error: msgInfo.msg.botInfo.isError,
args: msgInfo.msg.botInfo.args,
};
}
if(['join', 'leave', 'kick', 'flood', 'timeout', 'jchan', 'lchan', 'nick'].includes(botParts[1])) {
msgInfo.msg.sender = undefined;
const botUserTarget = botParts[botUserNameIndex].toLowerCase();
for(const botUserInfo of ctx.users.values())
if(botUserInfo.name.toLowerCase() === botUserTarget) {
msgInfo.msg.sender = botUserInfo;
break;
}
if(msgInfo.msg.sender === undefined)
msgInfo.msg.sender = {
id: '0',
self: false,
name: botParts[botUserNameIndex],
colour: 'inherit',
perms: { rank: 0 },
};
}
} }
ctx.dispatch('msg:add', msgInfo); ctx.dispatch('msg:add', msgInfo);
@ -296,6 +441,7 @@ const SockChatS2CMessageAdd = (ctx, timeStamp, userId, msgText, msgId, msgFlags)
const SockChatS2CMessageRemove = (ctx, msgId) => { const SockChatS2CMessageRemove = (ctx, msgId) => {
ctx.dispatch('msg:remove', { ctx.dispatch('msg:remove', {
msg: { msg: {
type: 'msg:remove',
id: msgId, id: msgId,
channel: ctx.channelName, channel: ctx.channelName,
}, },

View file

@ -12,6 +12,9 @@ const SockChatContext = function(dispatch, sendPing, pingDelay) {
get isAuthed() { return userId !== undefined; }, get isAuthed() { return userId !== undefined; },
users: new Map,
channels: new Map,
channelName: undefined, channelName: undefined,
pseudoChannelName: undefined, pseudoChannelName: undefined,

View file

@ -81,6 +81,8 @@ const SockChatProtocol = function(dispatch, options) {
ctx.userId = undefined; ctx.userId = undefined;
ctx.channelName = undefined; ctx.channelName = undefined;
ctx.pseudoChannelName = undefined; ctx.pseudoChannelName = undefined;
ctx.users.clear();
ctx.channels.clear();
if(ev.code === 1012) if(ev.code === 1012)
ctx.isRestarting = true; ctx.isRestarting = true;
@ -252,5 +254,42 @@ const SockChatProtocol = function(dispatch, options) {
await sendMessage(`/join ${name}`); await sendMessage(`/join ${name}`);
} }
}, },
getCurrentUserId: () => ctx.userId,
getCurrentUserInfo: () => ctx.users.get(ctx.userId),
getUserInfos: () => Array.from(ctx.users.values()),
getUserInfoById: userId => {
if(typeof userId === 'string')
userId = parseInt(userId);
if(typeof userId !== 'number')
throw 'userId must be a number';
return ctx.users.get(userId);
},
getUserInfoByName: (userName, exact = true) => {
if(typeof userName !== 'string')
throw 'userName must be a string';
const compare = exact ? (a, b) => a.toLowerCase() === b : (a, b) => a.toLowerCase().includes(b);
userName = userName.toLowerCase();
for(const info of ctx.users.values())
if(compare(info.name, userName))
return info;
return undefined;
},
getUserInfosByName: userName => {
if(typeof userName !== 'string')
throw 'userName must be a string';
const users = [];
userName = userName.toLowerCase();
for(const info of ctx.users.values())
if(info.name.toLowerCase().includes(userName))
users.push(info);
return users;
},
}; };
}; };

View file

@ -5,6 +5,15 @@ const SockChatS2CAuthSuccess = (ctx, userId, userName, userColour, userPerms, ch
ctx.channelName = chanName; ctx.channelName = chanName;
const statusInfo = SockChatParseStatusInfo(userName); const statusInfo = SockChatParseStatusInfo(userName);
const userInfo = {
id: ctx.userId,
self: true,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(userColour),
perms: SockChatParseUserPerms(userPerms),
};
ctx.users.set(userInfo.id, userInfo);
const info = { const info = {
wasConnected: ctx.wasConnected, wasConnected: ctx.wasConnected,
@ -12,14 +21,7 @@ const SockChatS2CAuthSuccess = (ctx, userId, userName, userColour, userPerms, ch
ctx: { ctx: {
maxMsgLength: parseInt(maxLength), maxMsgLength: parseInt(maxLength),
}, },
user: { user: userInfo,
id: ctx.userId,
self: true,
name: statusInfo.name,
status: statusInfo.status,
colour: SockChatParseUserColour(userColour),
perms: SockChatParseUserPerms(userPerms),
},
channel: { channel: {
name: ctx.channelName, name: ctx.channelName,
}, },

View file

@ -14,8 +14,9 @@ const SockChatParseUserColour = str => {
const SockChatParseUserPerms = str => { const SockChatParseUserPerms = str => {
const parts = str.split(str.includes("\f") ? "\f" : ' '); const parts = str.split(str.includes("\f") ? "\f" : ' ');
const rank = parseInt(parts[0] ?? '0');
return { return {
rank: parseInt(parts[0] ?? '0'), rank: isNaN(rank) ? 0 : rank,
kick: parts[1] !== undefined && parts[1] !== '0', kick: parts[1] !== undefined && parts[1] !== '0',
nick: parts[3] !== undefined && parts[3] !== '0', nick: parts[3] !== undefined && parts[3] !== '0',
chan: parts[4] !== undefined && parts[4] !== '0', chan: parts[4] !== undefined && parts[4] !== '0',