You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

347 lines
12 KiB

// site-chat.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const DTP_COMPONENT = { name: 'Site Chat', slug: 'site-chat' };
const dtp = window.dtp = window.dtp || { }; // jshint ignore:line
const EMOJI_EXPLOSION_DURATION = 8000;
const EMOJI_EXPLOSION_INTERVAL = 100;
import DtpLog from 'dtp/dtp-log.js';
import SiteReactions from './site-reactions.js';
export default class SiteChat {
constructor (app) {
this.app = app;
this.log = new DtpLog(DTP_COMPONENT);
this.ui = {
form: document.querySelector('#chat-input-form'),
messageList: document.querySelector('#chat-message-list'),
messages: [ ],
messageMenu: document.querySelector('.chat-message-menu'),
input: document.querySelector('#chat-input-text'),
isAtBottom: true,
isModifying: false,
};
if (this.ui.messageList) {
this.ui.messageList.addEventListener('scroll', this.onChatMessageListScroll.bind(this));
this.updateTimestamps();
setTimeout(( ) => {
this.log.info('constructor', 'scrolling chat', { top: this.ui.messageList.scrollHeight });
this.ui.messageList.scrollTo({ top: this.ui.messageList.scrollHeight, behavior: 'instant' });
}, 100);
this.ui.reactions = new SiteReactions();
this.lastReaction = new Date();
}
if (this.ui.input) {
this.ui.input.addEventListener('keydown', this.onChatInputKeyDown.bind(this));
}
this.lastReaction = new Date();
if (window.localStorage) {
this.mutedUsers = window.localStorage.mutedUsers ? JSON.parse(window.localStorage.mutedUsers) : [ ];
this.filterChatView();
}
}
async filterChatView ( ) {
this.mutedUsers.forEach((block) => {
document.querySelectorAll(`.chat-message[data-author-id="${block.userId}"]`).forEach((message) => {
message.parentElement.removeChild(message);
});
});
}
async toggleChatInput (event) {
event.preventDefault();
event.stopPropagation();
this.ui.input.toggleAttribute('hidden');
if (this.ui.input.getAttribute('hidden')) {
this.ui.input.focus();
}
return true;
}
async openChatInput ( ) {
if (this.ui.input.hasAttribute('hidden')) {
this.ui.input.removeAttribute('hidden');
}
return true;
}
async onChatInputKeyDown (event) {
if (event.key === 'Enter' && !event.shiftKey) {
return this.sendUserChat(event);
}
}
async onChatMessageListScroll (/* event */) {
const prevBottom = this.ui.isAtBottom;
const scrollPos = this.ui.messageList.scrollTop + this.ui.messageList.clientHeight;
this.ui.isAtBottom = scrollPos >= (this.ui.messageList.scrollHeight - 8);
if (this.ui.isAtBottom !== prevBottom) {
this.log.info('onChatMessageListScroll', 'at-bottom status change', { atBottom: this.ui.isAtBottom });
if (this.ui.isAtBottom) {
this.ui.messageMenu.classList.remove('chat-menu-visible');
} else {
this.ui.messageMenu.classList.add('chat-menu-visible');
}
}
}
async resumeChatScroll ( ) {
this.ui.messageList.scrollTop = this.ui.messageList.scrollHeight;
}
async sendUserChat (event) {
event.preventDefault();
if (!dtp.room || !dtp.room._id) {
UIkit.modal.alert('There is a problem with Chat. Please refresh the page.');
return;
}
const roomId = dtp.room._id;
const content = this.ui.input.value;
this.ui.input.value = '';
if (content.length === 0) {
return true;
}
this.log.info('sendUserChat', 'sending chat message', { roomId, content });
this.app.socket.emit('user-chat', {
channelType: 'ChatRoom',
channel: roomId,
content,
});
// set focus back to chat input
this.ui.input.focus();
return true;
}
async sendReaction (event) {
const NOW = new Date();
if (NOW - this.lastReaction < 1000) {
return;
}
this.lastReaction = NOW;
const target = event.currentTarget || event.target;
if (!target) {
return;
}
const reaction = target.getAttribute('data-reaction');
this.log.info('sendReaction', 'sending user reaction', { reaction });
this.app.socket.emit('user-react', {
subjectType: 'ChatRoom',
subject: dtp.room._id,
reaction,
});
}
async appendUserChat (message) {
const isAtBottom = this.ui.isAtBottom;
this.log.info('appendUserChat', 'message received', { user: message.user, content: message.content });
if (this.mutedUsers.find((block) => block.userId === message.user._id)) {
this.log.info('appendUserChat', 'message is from blocked user', {
_id: message.user._id,
username: message.user.username,
});
return; // sender is blocked by local user on this device
}
const chatMessage = document.createElement('div');
chatMessage.setAttribute('data-message-id', message._id);
chatMessage.setAttribute('data-author-id', message.user._id);
chatMessage.classList.add('uk-margin-small');
chatMessage.classList.add('chat-message');
const userGrid = document.createElement('div');
userGrid.setAttribute('uk-grid', '');
userGrid.classList.add('uk-grid-small');
userGrid.classList.add('uk-flex-middle');
chatMessage.appendChild(userGrid);
const usernameColumn = document.createElement('div');
usernameColumn.classList.add('uk-width-expand');
userGrid.appendChild(usernameColumn);
const chatUser = document.createElement('div');
const authorName = message.user.displayName || message.user.username;
chatUser.classList.add('uk-text-small');
chatUser.classList.add('chat-username');
chatUser.textContent = authorName;
usernameColumn.appendChild(chatUser);
if (message.user.picture && message.user.picture.small) {
const chatUserPictureColumn = document.createElement('div');
chatUserPictureColumn.classList.add('uk-width-auto');
userGrid.appendChild(chatUserPictureColumn);
const chatUserPicture = document.createElement('img');
chatUserPicture.classList.add('chat-author-image');
chatUserPicture.setAttribute('src', `/image/${message.user.picture.small._id}`);
chatUserPicture.setAttribute('alt', `${authorName}'s profile picture`);
chatUserPictureColumn.appendChild(chatUserPicture);
}
if (dtp.user && (dtp.user._id !== message.user._id)) {
const menuColumn = document.createElement('div');
menuColumn.classList.add('uk-width-auto');
menuColumn.classList.add('chat-user-menu');
userGrid.appendChild(menuColumn);
const menuButton = document.createElement('button');
menuButton.setAttribute('type', 'button');
menuButton.classList.add('uk-button');
menuButton.classList.add('uk-button-link');
menuButton.classList.add('uk-button-small');
menuColumn.appendChild(menuButton);
const menuIcon = document.createElement('i');
menuIcon.classList.add('fas');
menuIcon.classList.add('fa-ellipsis-h');
menuButton.appendChild(menuIcon);
const menuDropdown = document.createElement('div');
menuDropdown.setAttribute('data-message-id', message._id);
menuDropdown.setAttribute('uk-dropdown', 'mode: click');
menuColumn.appendChild(menuDropdown);
const dropdownList = document.createElement('ul');
dropdownList.classList.add('uk-nav');
dropdownList.classList.add('uk-dropdown-nav');
menuDropdown.appendChild(dropdownList);
let dropdownListItem = document.createElement('li');
dropdownList.appendChild(dropdownListItem);
let link = document.createElement('a');
link.setAttribute('href', '');
link.setAttribute('data-message-id', message._id);
link.setAttribute('data-user-id', message.user._id);
link.setAttribute('data-username', message.user.username);
link.setAttribute('onclick', "return dtp.app.muteChatUser(event);");
link.textContent = `Mute ${message.user.displayName || message.user.username}`;
dropdownListItem.appendChild(link);
}
const chatContent = document.createElement('div');
chatContent.classList.add('chat-content');
chatContent.classList.add('uk-text-break');
chatContent.innerHTML = message.content;
chatMessage.appendChild(chatContent);
const chatTimestamp = document.createElement('div');
chatTimestamp.classList.add('chat-timestamp');
chatTimestamp.classList.add('uk-text-small');
chatTimestamp.textContent = moment(message.created).format('hh:mm:ss a');
chatMessage.appendChild(chatTimestamp);
if (Array.isArray(message.stickers) && message.stickers.length) {
message.stickers.forEach((sticker) => {
const chatContent = document.createElement('div');
chatContent.classList.add('chat-sticker');
chatContent.setAttribute('title', `:${sticker.slug}:`);
chatContent.setAttribute('data-sticker-id', sticker._id);
switch (sticker.encoded.type) {
case 'video/mp4':
chatContent.innerHTML = `<video playsinline autoplay muted loop preload="auto"><source src="/sticker/${sticker._id}/media"></source></video>`;
break;
case 'image/png':
chatContent.innerHTML = `<img src="/sticker/${sticker._id}/media" height="100" />`;
break;
case 'image/jpeg':
chatContent.innerHTML = `<img src="/sticker/${sticker._id}/media" height="100" />`;
break;
}
chatMessage.appendChild(chatContent);
});
}
this.ui.isModifying = true;
this.ui.messageList.appendChild(chatMessage);
this.ui.messages.push(chatMessage);
while (this.ui.messages.length > 50) {
const message = this.ui.messages.shift();
this.ui.messageList.removeChild(message);
}
if (isAtBottom) {
/*
* This is jank. I don't know why I had to add this jank, but it is jank.
* The browser started emitting a scroll event *after* I issue this scroll
* command to return to the bottom of the view. So, I have to issue the
* scroll, let it fuck up, and issue the scroll again. I don't care why.
*/
this.ui.messageList.scrollTo(0, this.ui.messageList.scrollHeight);
setTimeout(( ) => {
this.ui.isAtBottom = true;
this.ui.messageList.scrollTo(0, this.ui.messageList.scrollHeight);
this.ui.isModifying = false;
}, 25);
}
}
updateTimestamps ( ) {
const timestamps = document.querySelectorAll('div.chat-timestamp[data-created]');
timestamps.forEach((timestamp) => {
const created = timestamp.getAttribute('data-created');
timestamp.textContent = moment(created).format('hh:mm:ss a');
});
}
createEmojiReact (message) {
this.ui.reactions.create(message.reaction);
}
triggerEmojiExplosion ( ) {
const reactions = ['happy', 'angry', 'honk', 'clap', 'fire', 'laugh'];
const stopHandler = this.stopEmojiExplosion.bind(this);
if (this.emojiExplosionTimeout && this.emojiExplosionInterval) {
clearTimeout(this.emojiExplosionTimeout);
this.emojiExplosionTimeout = setTimeout(stopHandler, EMOJI_EXPLOSION_DURATION);
return;
}
// spawn 10 emoji reacts per second until told to stop
this.emojiExplosionInterval = setInterval(( ) => {
// choose a random reaction from the list of available reactions and
// spawn it.
const reaction = reactions[Math.floor(Math.random() * reactions.length)];
this.ui.reactions.create({ reaction });
}, EMOJI_EXPLOSION_INTERVAL);
// set a timeout to stop the explosion
this.emojiExplosionTimeout = setTimeout(stopHandler, EMOJI_EXPLOSION_DURATION);
}
stopEmojiExplosion ( ) {
if (!this.emojiExplosionTimeout || !this.emojiExplosionInterval) {
return;
}
clearTimeout(this.emojiExplosionTimeout);
delete this.emojiExplosionTimeout;
clearInterval(this.emojiExplosionInterval);
delete this.emojiExplosionInterval;
}
}