mami/src/mami.js/ui/messages.jsx

504 lines
20 KiB
JavaScript

#include channels.js
#include common.js
#include parsing.js
#include title.js
#include txtrigs.js
#include url.js
#include users.js
#include utility.js
#include weeb.js
#include sound/umisound.js
#include ui/emotes.js
Umi.UI.Messages = (function() {
let focusChannelName = '';
const title = new MamiWindowTitle({
getName: () => futami.get('title'),
});
window.addEventListener('focus', () => title.clear());
const shouldDisplayAuthorInfo = (target, ref) => {
if(!(target instanceof Element) || !(ref instanceof Element))
return true;
return target.dataset.tiny !== undefined
|| target.dataset.author !== ref.dataset.author
|| target.dataset.channel !== ref.dataset.channel
|| target.dataset.tiny !== ref.dataset.tiny;
};
const botMsgs = {
'say': { text: '%0' },
'generr': { text: 'Something unexpected happened.' },
'flwarn': { text: 'You are about to hit the flood limit! If you continue you will be kicked.' },
'unban': { text: '%0 is no longer banned.', sound: 'unban' },
'delerr': { text: 'You are not allowed to delete this message.' },
'notban': { text: '%0 is not banned.' },
'whoerr': { text: '%0 does not exist.' },
'join': { text: '%0 has joined.', action: 'has joined', sound: 'join' },
'leave': { text: '%0 has disconnected.', action: 'has disconnected', avatar: 'greyscale', sound: 'leave' },
'jchan': { text: '%0 has joined the channel.', action: 'has joined the channel', sound: 'join' },
'lchan': { text: '%0 has left the channel.', action: 'has left the channel', avatar: 'greyscale', sound: 'leave' },
'kick': { text: '%0 got bludgeoned to death.', action: 'got bludgeoned to death', avatar: 'invert', sound: 'kick' },
'flood': { text: '%0 got kicked for flood protection.', action: 'got kicked for flood protection', avatar: 'invert', sound: 'flood' },
'timeout': { text: '%0 exploded.', action: 'exploded', avatar: 'greyscale', sound: 'timeout' },
'nick': { text: '%0 changed their name to %1.', action: 'changed their name to %1' },
'crchan': { text: 'Channel %0 has been created.' },
'delchan': { text: 'Channel %0 has been deleted.' },
'cpwdchan': { text: 'Channel password has been changed.' },
'cprivchan': { text: 'Channel access level has been changed.' },
'ipaddr': { text: 'IP address of %0 is %1.' },
'cmdna': { text: 'You are not allowed to use %0.' },
'nocmd': { text: 'Command %0 does not exist.' },
'cmderr': { text: 'You did not use that command correctly.' },
'usernf': { text: '%0 is not logged in right now!' },
'kickna': { text: 'You are not allowed to kick %0.' },
'samechan': { text: 'You are already in channel %0.' },
'ipchan': { text: 'You are not allowed to join channel %0.' },
'nochan': { text: 'Channel %0 does not exist.' },
'nopwchan': { text: 'Channel %0 requires a password. Use /join %0 <password>' },
'ipwchan': { text: 'Wrong password for channel %0.' },
'inchan': { text: 'Channel name contains invalid characters.' },
'nischan': { text: 'A channel with the name %0 already exists.' },
'ndchan': { text: 'You are not allowed to deleted channel %0.' },
'namchan': { text: 'You are not allowed to edit channel %0.' },
'nameinuse': { text: 'Someone else is already using the name %0.' },
'rankerr': { text: 'You cannot set the access level of a channel higher than that of your own.' },
'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 {
Add: function(msg) {
mami.globalEvents.dispatch('umi:message_add', msg);
const msgId = msg.getId();
const elementId = `message-${msgId}`;
if(msgId !== '' && $i(elementId))
return;
const channelName = msg.getChannel();
const sender = msg.getUserV2();
const isBot = sender.id === '-1';
const isOutgoing = Umi.User.isCurrentUser(sender);
const hasSeen = msg.hasSeen();
const displayMessage = focusChannelName === '' || channelName === '' || channelName === focusChannelName;
const notifyPM = !displayMessage && !isOutgoing && !hasSeen && channelName.startsWith('@');
let isTiny = false;
let skipTextParsing = false;
let msgText = msg.getText();
let msgTextLong = msgText;
let eBase;
let eAvatar;
let eText;
let eMeta;
let eUser;
let avatarUser = sender;
let avatarSize = '80';
let soundIsLegacy = true;
let soundName = isOutgoing ? 'outgoing' : 'incoming';
let soundVolume;
let soundRate;
const userClass = `message--user-${sender.id}`;
const classes = ['message', userClass];
const styles = {};
const avatarClasses = ['message__avatar'];
if(msg.isAction()) {
isTiny = true;
classes.push('message-action');
}
if(!displayMessage)
classes.push('hidden');
if(sender.id === "136")
styles.transform = 'scaleY(' + (0.76 + (0.01 * Math.max(0, Math.ceil(Date.now() / (7 * 24 * 60 * 60000)) - 2813))).toString() + ')';
const msgCreated = msg.getTime();
const msgDateTime = msgCreated.getHours().toString().padStart(2, '0')
+ ':' + msgCreated.getMinutes().toString().padStart(2, '0')
+ ':' + msgCreated.getSeconds().toString().padStart(2, '0');
if(isBot) {
const botInfo = msg.getBotInfo();
soundName = botInfo.isError ? 'error' : 'server';
if(botMsgs.hasOwnProperty(botInfo.type)) {
const bmInfo = botMsgs[botInfo.type];
let bArgs = botInfo.args;
if(typeof bmInfo.filter === 'function')
bArgs = bmInfo.filter(bArgs);
if(typeof bmInfo.sound === 'string')
soundName = bmInfo.sound;
let actionSuccess = false;
if(typeof bmInfo.action === 'string' && mami.settings.get('fancyInfo')) {
const target = botInfo.target ?? Umi.Users.FindExact(bArgs[0]) ?? Umi.Users.FindExact('~' + bArgs[0]); // shitty fix for server sending invalid data
if(target) {
actionSuccess = true;
isTiny = true;
skipTextParsing = true;
avatarUser = target;
$ari(classes, userClass);
msgText = formatTemplate(bmInfo.action, bArgs);
if(typeof bmInfo.avatar === 'string')
avatarClasses.push(`avatar-filter-${bmInfo.avatar}`);
}
}
msgTextLong = formatTemplate(bmInfo.text, bArgs);
if(!actionSuccess)
msgText = msgTextLong;
} else
msgText = msgTextLong = `!!! Received unsupported message type: ${botInfo.type} !!!`;
} else {
if(mami.settings.get('playJokeSounds'))
try {
const trigger = mami.textTriggers.getTrigger(msgText);
if(trigger.isSoundType()) {
soundIsLegacy = false;
soundName = trigger.getRandomSoundName();
soundVolume = trigger.getVolume();
soundRate = trigger.getRate();
}
} catch(ex) {}
}
let avatarUrl = futami.get('avatar');
if(typeof avatarUrl !== 'string' || avatarUrl.length < 1)
avatarUrl = undefined;
else
avatarUrl = avatarUrl.replace('{user:id}', avatarUser.id)
.replace('{resolution}', avatarSize)
.replace('{user:avatar_change}', avatarUser.avatarChangeTime);
eAvatar = <div class={avatarClasses}/>;
eUser = <div class="message__user" style={{ color: avatarUser.colour }}>{avatarUser.name}</div>;
if(isTiny) {
classes.push('message-tiny');
avatarSize = '40';
if(msgText.indexOf("'") !== 0 || (msgText.match(/\'/g).length % 2) === 0)
msgText = "\xA0" + msgText;
eBase = <div class={classes} style={styles}>
{eAvatar}
<div class="message__container">
{eMeta = <div class="message__meta">
{eUser}
{eText = <div class="message-tiny-text"/>}
<div class="message__time">{msgDateTime}</div>
</div>}
</div>
</div>;
} else {
eBase = <div class={classes} style={styles}>
{eAvatar}
<div class="message__container">
{eMeta = <div class="message__meta">
{eUser}
<div class="message__time">{msgDateTime}</div>
</div>}
{eText = <div class="message__text"/>}
</div>
</div>;
}
if(msgId !== '') {
eBase.id = elementId;
eBase.dataset.id = msgId;
}
if(!isBot)
eBase.dataset.author = sender.id;
if(channelName !== '')
eBase.dataset.channel = channelName;
if(isTiny)
eBase.dataset.tiny = '1';
eBase.dataset.created = msgCreated.toISOString();
eBase.dataset.body = msgText;
eText.innerText = msgText;
if(!skipTextParsing) {
eText = Umi.UI.Emoticons.Parse(eText, msg);
eText = Umi.Parsing.Parse(eText, msg);
const urls = [];
if(mami.settings.get('autoParseUrls')) {
const textSplit = eText.innerText.split(' ');
for(const textPart of textSplit) {
const uri = Umi.URI.Parse(textPart);
if(uri !== null && uri.Slashes !== null) {
urls.push(textPart);
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')) {
eText.appendChild($t(Weeaboo.getTextSuffix(sender)));
const kaomoji = Weeaboo.getRandomKaomoji(true, msg);
if(kaomoji) {
eText.appendChild($t(' '));
eText.appendChild($t(kaomoji));
}
}
if(mami.settings.get('weeaboo'))
eUser.appendChild($t(Weeaboo.getNameSuffix(sender)));
}
if(avatarUrl === undefined)
eAvatar.classList.add('message__avatar--disabled');
else
eAvatar.style.backgroundImage = `url(${avatarUrl})`;
const msgsList = $i('umi-messages');
let insertAfter = msgsList.lastElementChild;
if(insertAfter instanceof Element) {
while(insertAfter.dataset.created > eBase.dataset.created) {
if(!insertAfter.previousElementSibling || !insertAfter.previousElementSibling.dataset.channel)
break;
insertAfter = insertAfter.previousElementSibling;
}
eBase.classList.toggle('message--first', shouldDisplayAuthorInfo(eBase, insertAfter));
if(eBase.dataset.tiny !== insertAfter.dataset.tiny)
eBase.classList.add(isTiny ? 'message-tiny-fix' : 'message-big-fix');
insertAfter.after(eBase);
if(eBase.nextElementSibling instanceof Element)
eBase.nextElementSibling.classList.toggle('message--first', shouldDisplayAuthorInfo(eBase.nextElementSibling, eBase));
} else {
eBase.classList.add('message--first');
if(isTiny) eBase.classList.add('message-tiny-fix');
msgsList.append(eBase);
}
if(displayMessage) {
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'))
msgsList.scrollTop = msgsList.scrollHeight;
}
let isMentioned = false;
const mentionTriggers = mami.settings.get('notificationTriggers').toLowerCase().split(' ');
const currentUser = Umi.User.getCurrentUser();
if(typeof currentUser === 'object' && typeof currentUser.name === 'string')
mentionTriggers.push(currentUser.name.toLowerCase());
const mentionText = ` ${msgTextLong} `.toLowerCase();
for(const trigger of mentionTriggers) {
if(trigger.trim() === '')
continue;
if(mentionText.includes(` ${trigger} `)) {
isMentioned = true;
break;
}
}
if(!isMentioned && mami.settings.get('onlySoundOnMention'))
soundName = undefined;
if(document.hidden) {
if(mami.settings.get('flashTitle')) {
let titleText = isBot && mami.settings.get('showServerMsgInTitle')
? ` ${msgTextLong}`
: ` ${sender.name}`;
if(focusChannelName !== '' && focusChannelName !== channelName)
titleText += ` @ ${channelName}`;
title.strobe([
`[ @] ${titleText}`,
`[@ ] ${titleText}`,
]);
}
if(!hasSeen) {
Umi.UI.Channels.Unread(channelName);
if(mami.settings.get('enableNotifications') && isMentioned) {
const options = {};
options.body = 'Click here to see what they said.';
if(mami.settings.get('notificationShowMessage'))
options.body += "\n" + msgTextLong;
if(avatarUrl !== undefined)
options.icon = avatarUrl;
const notif = new Notification(`${sender.name} mentioned you!`, options);
notif.addEventListener('click', () => {
window.focus();
});
document.addEventListener('visibilitychange', () => {
if(document.visibilityState === 'visible')
notif.close();
});
}
}
}
if(soundName !== undefined && !hasSeen) {
if(soundIsLegacy)
soundName = Umi.Sound.Convert(soundName);
mami.sound.library.play(soundName, soundVolume, soundRate);
}
mami.globalEvents.dispatch('umi:ui:message_add', {
element: eBase,
message: msg,
});
msg.markSeen();
},
SwitchChannel: channel => {
if(typeof channel === 'object' && channel !== null && 'name' in channel)
channel = channel.name;
if(typeof channel !== 'string')
return;
focusChannelName = channel;
const root = $i('umi-messages');
for(const elem of root.children)
elem.classList.toggle('hidden', elem.dataset.channel !== undefined && elem.dataset.channel !== focusChannelName);
},
Clear: retain => {
if(typeof retain === 'string' && !isNaN(retain))
retain = parseInt(retain);
if(typeof retain !== 'number')
return;
const root = $i('umi-messages');
// remove messages
if(root.childElementCount > retain)
for(let i = root.childElementCount - 1; i >= 0; --i) {
const elem = root.children[i];
if(!elem.dataset.channel || elem.classList.contains('hidden') || --retain > 0)
continue;
$r(elem);
}
// fix author display
for(const elem of root.children) {
elem.classList.toggle('message--first', shouldDisplayAuthorInfo(elem, elem.previousElementSibling));
lastAuthor = elem.dataset.author;
}
},
Remove: function(msgId) {
if(typeof msgId === 'object' && msgId !== null) {
if('getId' in msgId)
msgId = msgId.getId();
else if('id' in msgId)
msgId = msgId.id;
}
if(typeof msgId !== 'string')
msgId = msgId.toString();
if(msgId === '')
return;
const elem = $i(`message-${msgId}`);
if(!(elem instanceof Element))
return;
// todo: take channel into account
if(elem.nextElementSibling && elem.nextElementSibling.dataset.author === elem.dataset.author)
elem.nextElementSibling.classList.add('message--first');
$r(elem);
},
};
})();