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.
926 lines
30 KiB
926 lines
30 KiB
// chat-client.js
|
|
// Copyright (C) 2024 DTP Technologies, LLC
|
|
// All Rights Reserved
|
|
|
|
'use strict';
|
|
|
|
const DTP_COMPONENT_NAME = 'DtpChatApp';
|
|
const dtp = window.dtp = window.dtp || { };
|
|
|
|
import DtpApp from 'lib/dtp-app.js';
|
|
import ChatAudio from './chat-audio.js';
|
|
|
|
import QRCode from 'qrcode';
|
|
import Cropper from 'cropperjs';
|
|
|
|
import dayjs from 'dayjs';
|
|
import dayjsRelativeTime from 'dayjs/plugin/relativeTime.js';
|
|
dayjs.extend(dayjsRelativeTime);
|
|
|
|
import hljs from 'highlight.js';
|
|
|
|
export class ChatApp extends DtpApp {
|
|
|
|
static get SFX_CHAT_ROOM_CONNECT ( ) { return 'chat-room-connect'; }
|
|
static get SFX_CHAT_MESSAGE ( ) { return 'chat-message'; }
|
|
static get SFX_CHAT_REACTION ( ) { return 'reaction'; }
|
|
static get SFX_CHAT_REACTION_REMOVE ( ) { return 'reaction-remove'; }
|
|
static get SFX_CHAT_MESSAGE_REMOVE ( ) { return 'message-remove'; }
|
|
|
|
constructor (user) {
|
|
super(DTP_COMPONENT_NAME, user);
|
|
this.loadSettings();
|
|
this.log.info('constructor', 'DTP app client online');
|
|
|
|
this.notificationPermission = 'default';
|
|
this.haveFocus = true; // hard to load the app w/o also being the focused app
|
|
|
|
this.chat = {
|
|
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'),
|
|
sendButton: document.querySelector('#chat-send-btn'),
|
|
isAtBottom: true,
|
|
};
|
|
|
|
if (this.chat.input) {
|
|
this.chat.input.addEventListener('keydown', this.onChatInputKeyDown.bind(this));
|
|
this.observer = new MutationObserver(this.onChatMessageListChanged.bind(this));
|
|
this.observer.observe(this.chat.messageList, { childList: true });
|
|
hljs.highlightAll();
|
|
}
|
|
|
|
this.emojiPickerDisplay = document.querySelector('.emoji-picker-display');
|
|
if (this.emojiPickerDisplay) {
|
|
this.emojiPickerDisplay.addEventListener('click', this.onEmojiPickerClose.bind(this));
|
|
}
|
|
this.emojiPicker = document.querySelector('emoji-picker');
|
|
if (this.emojiPicker) {
|
|
this.emojiPicker.addEventListener('emoji-click', this.onEmojiPicked.bind(this));
|
|
}
|
|
|
|
this.dragFeedback = document.querySelector('.dtp-drop-feedback');
|
|
|
|
window.addEventListener('dtp-load', this.onDtpLoad.bind(this));
|
|
window.addEventListener('unload', this.onDtpUnload.bind(this));
|
|
|
|
window.addEventListener('focus', this.onWindowFocus.bind(this));
|
|
window.addEventListener('blur', this.onWindowBlur.bind(this));
|
|
|
|
this.updateTimestamps();
|
|
}
|
|
|
|
async onWindowFocus (event) {
|
|
this.log.debug('onWindowFocus', 'window has received focus', { event });
|
|
this.haveFocus = true;
|
|
}
|
|
|
|
async onWindowBlur (event) {
|
|
this.log.debug('onWindowBlur', 'window has lost focus', { event });
|
|
this.haveFocus = false;
|
|
}
|
|
|
|
async startAudio ( ) {
|
|
this.log.info('startAudio', 'starting ChatAudio');
|
|
this.audio = new ChatAudio();
|
|
this.audio.start();
|
|
try {
|
|
await Promise.all([
|
|
this.audio.loadSound(ChatApp.SFX_CHAT_ROOM_CONNECT, '/static/sfx/room-connect.mp3'),
|
|
this.audio.loadSound(ChatApp.SFX_CHAT_MESSAGE, '/static/sfx/chat-message.mp3'),
|
|
this.audio.loadSound(ChatApp.SFX_CHAT_REACTION, '/static/sfx/reaction.mp3'),
|
|
this.audio.loadSound(ChatApp.SFX_CHAT_REACTION_REMOVE, '/static/sfx/reaction-remove.mp3'),
|
|
this.audio.loadSound(ChatApp.SFX_CHAT_MESSAGE_REMOVE, '/static/sfx/message-deleted.mp3'),
|
|
]);
|
|
} catch (error) {
|
|
this.log.error('startAudio', 'failed to load sound', { error });
|
|
// fall through
|
|
}
|
|
}
|
|
|
|
async onChatInputKeyDown (event) {
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
if (dtp.room) {
|
|
return this.sendChatRoomMessage(event);
|
|
}
|
|
if (dtp.thread) {
|
|
return this.sendPrivateMessage(event);
|
|
}
|
|
return this.sendUserChat(event);
|
|
}
|
|
}
|
|
|
|
async onDragEnter (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
this.log.info('onDragEnter', 'something being dragged has entered the stage', { event });
|
|
this.dragFeedback.classList.add('feedback-active');
|
|
}
|
|
|
|
async onDragLeave (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
this.log.info('onDragLeave', 'something being dragged has left the stage', { event });
|
|
this.dragFeedback.classList.remove('feedback-active');
|
|
}
|
|
|
|
async onDragOver (event) {
|
|
/*
|
|
* Inform that we want "copy" as a drop effect and prevent all default
|
|
* processing so we'll actually get the files in the drop event. If this
|
|
* isn't done, you simply won't get the files in the drop.
|
|
*/
|
|
event.preventDefault();
|
|
event.stopPropagation(); // this ends now!
|
|
event.dataTransfer.dropEffect = 'copy';
|
|
|
|
// this.log.info('onDragOver', 'something was dragged over the stage', { event });
|
|
this.dragFeedback.classList.add('feedback-active');
|
|
}
|
|
|
|
async onDrop (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
for (const file of event.dataTransfer.files) {
|
|
this.log.info('onDrop', 'a file has been dropped', { file });
|
|
}
|
|
this.log.info('onFileDrop', 'something was dropped on the stage', { event, files: event.files });
|
|
this.dragFeedback.classList.remove('feedback-active');
|
|
}
|
|
|
|
async onChatMessageListChanged (mutationList) {
|
|
this.log.info('onMutation', 'DOM mutation received', { mutationList });
|
|
if (!Array.isArray(mutationList) || (mutationList.length === 0)) {
|
|
return;
|
|
}
|
|
for (const mutation of mutationList) {
|
|
for (const node of mutation.addedNodes) {
|
|
if (typeof node.querySelectorAll !== 'function') {
|
|
continue;
|
|
}
|
|
const timestamps = node.querySelectorAll("[data-dtp-timestamp]");
|
|
this.updateTimestamps(timestamps);
|
|
}
|
|
}
|
|
hljs.highlightAll();
|
|
}
|
|
|
|
updateTimestamps ( ) {
|
|
const nodeList = document.querySelectorAll("[data-dtp-timestamp]");
|
|
this.log.debug('updateTimestamps', 'updating timestamps', { count: nodeList.length });
|
|
for (const ts of nodeList) {
|
|
const date = ts.getAttribute('data-dtp-timestamp');
|
|
const format = ts.getAttribute('data-dtp-timestamp-format');
|
|
if (!date) { continue; }
|
|
switch (format) {
|
|
case 'date':
|
|
ts.textContent = dayjs(date).format('MMM DD, YYYY');
|
|
break;
|
|
case 'time':
|
|
ts.textContent = dayjs(date).format('h:mm a');
|
|
break;
|
|
case 'datetime':
|
|
ts.textContent = dayjs(date).format('MMM D [at] h:mm a');
|
|
break;
|
|
case 'fuzzy':
|
|
ts.textContent = dayjs(date).fromNow();
|
|
break;
|
|
|
|
case 'timestamp':
|
|
default:
|
|
ts.textContent = dayjs(date).format('hh:mm:ss a');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
async sendUserChat (event) {
|
|
event.preventDefault();
|
|
|
|
if (!dtp.user) {
|
|
UIkit.modal.alert('There is a problem with Chat. Please refresh the page.');
|
|
return;
|
|
}
|
|
|
|
if (this.chatTimeout) {
|
|
return;
|
|
}
|
|
|
|
const channelId = dtp.user._id;
|
|
const content = this.chat.input.value;
|
|
this.chat.input.value = '';
|
|
|
|
if (content.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
this.log.debug('sendUserChat', 'sending chat message', { channel: this.user._id, content });
|
|
this.socket.sendUserChat(channelId, content);
|
|
|
|
// set focus back to chat input
|
|
this.chat.input.focus();
|
|
|
|
const isFreeMember = false;
|
|
this.chat.sendButton.setAttribute('disabled', '');
|
|
this.chat.sendButton.setAttribute('uk-tooltip', isFreeMember ? 'Waiting 30 seconds' : 'Waiting 5 seconds');
|
|
|
|
this.chatTimeout = setTimeout(( ) => {
|
|
delete this.chatTimeout;
|
|
this.chat.sendButton.removeAttribute('disabled');
|
|
this.chat.sendButton.setAttribute('uk-tooltip', 'Send message');
|
|
}, isFreeMember ? 30000 : 5000);
|
|
|
|
return true;
|
|
}
|
|
|
|
async sendChatRoomMessage (event) {
|
|
if (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
if (this.chatTimeout) {
|
|
return;
|
|
}
|
|
|
|
const form = new FormData(this.chat.form);
|
|
const roomId = this.chat.form.getAttribute('data-room-id');
|
|
|
|
const content = this.chat.input.value;
|
|
this.chat.input.value = '';
|
|
if (content.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
this.log.info('sendChatRoomMessage', 'sending chat message', { room: roomId, content });
|
|
const response = await fetch(this.chat.form.action, {
|
|
method: this.chat.form.method,
|
|
body: form,
|
|
});
|
|
await this.processResponse(response);
|
|
} catch (error) {
|
|
UIkit.modal.alert(`Failed to send chat message: ${error.message}`);
|
|
}
|
|
|
|
// set focus back to chat input
|
|
this.chat.input.focus();
|
|
|
|
this.chat.sendButton.setAttribute('disabled', '');
|
|
this.chatTimeout = setTimeout(( ) => {
|
|
delete this.chatTimeout;
|
|
this.chat.sendButton.removeAttribute('disabled');
|
|
}, 5000);
|
|
|
|
return true;
|
|
}
|
|
|
|
async deleteChatMessage (event) {
|
|
const target = event.currentTarget || event.target;
|
|
const messageId = target.getAttribute('data-message-id');
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
try {
|
|
const response = await fetch(`/chat/message/${messageId}`, { method: 'DELETE' });
|
|
await this.processResponse(response);
|
|
} catch (error) {
|
|
this.log.error('deleteChatMessage', 'failed to delete chat message', { error });
|
|
UIkit.modal.alert(`Failed to delete chat message: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async toggleMessageReaction (event) {
|
|
const target = event.currentTarget || event.target;
|
|
const messageId = target.getAttribute('data-message-id');
|
|
const emoji = target.getAttribute('data-emoji');
|
|
try {
|
|
const response = await fetch(`/chat/message/${messageId}/reaction`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ emoji }),
|
|
});
|
|
await this.processResponse(response);
|
|
} catch (error) {
|
|
this.log.error('toggleMessageReaction', 'failed to send emoji react', { error });
|
|
UIkit.modal.alert(`Failed to send emoji react: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async onDtpLoad ( ) {
|
|
this.log.info('onDtpLoad', 'dtp-load event received. Connecting to platform.');
|
|
|
|
await this.connect({
|
|
mode: 'User',
|
|
onSocketConnect: this.onChatSocketConnect.bind(this),
|
|
onSocketDisconnect: this.onChatSocketDisconnect.bind(this),
|
|
});
|
|
|
|
if (this.chat.messageList) {
|
|
try {
|
|
this.notificationPermission = await Notification.requestPermission();
|
|
this.log.debug('onDtpLoad', 'Notification permission status', { permission: this.notificationPermission });
|
|
} catch (error) {
|
|
this.log.error('onDtpLoad', 'failed to request Notification permission', { error });
|
|
}
|
|
}
|
|
}
|
|
|
|
async onDtpUnload ( ) {
|
|
await this.socket.disconnect();
|
|
}
|
|
|
|
async onChatSocketConnect (socket) {
|
|
this.log.debug('onSocketConnect', 'attaching socket events');
|
|
socket.on('chat-message', this.onChatMessage.bind(this));
|
|
socket.on('chat-control', this.onChatControl.bind(this));
|
|
socket.on('system-message', this.onSystemMessage.bind(this));
|
|
|
|
if (dtp.room) {
|
|
await this.joinChatChannel(dtp.room);
|
|
}
|
|
}
|
|
|
|
async onChatSocketDisconnect (socket) {
|
|
this.log.debug('onSocketDisconnect', 'detaching socket events');
|
|
socket.off('chat-message', this.onChatMessage.bind(this));
|
|
socket.off('chat-control', this.onChatControl.bind(this));
|
|
socket.off('system-message', this.onSystemMessage.bind(this));
|
|
}
|
|
|
|
async onChatMessage (message) {
|
|
const isAtBottom = this.chat.isAtBottom;
|
|
this.chat.messageList.insertAdjacentHTML('beforeend', message.html);
|
|
this.audio.playSound(ChatApp.SFX_CHAT_MESSAGE);
|
|
this.scrollChatToBottom(isAtBottom);
|
|
|
|
if (!this.haveFocus && (this.notificationPermission === 'granted')) {
|
|
const chatMessage = message.message;
|
|
new Notification(chatMessage.channel.name, {
|
|
body: `Message received from ${chatMessage.author.displayName || chatMessage.author.username}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
async onChatControl (message) {
|
|
const isAtBottom = this.chat.isAtBottom;
|
|
|
|
if (message.audio) {
|
|
if (message.audio.playSound) {
|
|
this.audio.playSound(message.audio.playSound);
|
|
}
|
|
}
|
|
if (message.displayList) {
|
|
this.displayEngine.executeDisplayList(message.displayList);
|
|
}
|
|
|
|
if (Array.isArray(message.systemMessages) && (message.systemMessages.length > 0)) {
|
|
for await (const sm of message.systemMessages) {
|
|
await this.onSystemMessage(sm);
|
|
}
|
|
}
|
|
|
|
if (message.cmd) {
|
|
switch (message.cmd) {
|
|
case 'call-start':
|
|
if (message.mediaServer && !this.call) {
|
|
dtp.mediaServer = message.mediaServer;
|
|
setTimeout(this.joinWebCall.bind(this), Math.floor(Math.random() * 3000));
|
|
}
|
|
break;
|
|
|
|
case 'call-end':
|
|
if (this.chat) {
|
|
this.chat.closeCall();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.scrollChatToBottom(isAtBottom);
|
|
}
|
|
|
|
async onSystemMessage (message) {
|
|
if (message.displayList) {
|
|
this.displayEngine.executeDisplayList(message.displayList);
|
|
}
|
|
|
|
if (!message.created || !message.content) {
|
|
return;
|
|
}
|
|
|
|
if (!this.chat || !this.chat.messageList) {
|
|
return;
|
|
}
|
|
|
|
const systemMessage = document.createElement('div');
|
|
systemMessage.classList.add('chat-message');
|
|
systemMessage.classList.add('system-message');
|
|
systemMessage.classList.add('no-select');
|
|
|
|
const grid = document.createElement('div');
|
|
grid.toggleAttribute('uk-grid', true);
|
|
grid.classList.add('uk-grid-small');
|
|
systemMessage.appendChild(grid);
|
|
|
|
let column = document.createElement('div');
|
|
column.classList.add('uk-width-expand');
|
|
grid.appendChild(column);
|
|
|
|
const chatContent = document.createElement('div');
|
|
chatContent.classList.add('message-content');
|
|
chatContent.classList.add('uk-text-break');
|
|
chatContent.innerHTML = message.content;
|
|
column.appendChild(chatContent);
|
|
|
|
column = document.createElement('div');
|
|
column.classList.add('uk-width-expand');
|
|
grid.appendChild(column);
|
|
|
|
const chatTimestamp = document.createElement('div');
|
|
chatTimestamp.classList.add('message-timestamp');
|
|
chatTimestamp.classList.add('uk-text-small');
|
|
chatTimestamp.classList.add('uk-text-right');
|
|
chatTimestamp.setAttribute('data-dtp-timestamp', message.created);
|
|
chatTimestamp.setAttribute('data-dtp-timestamp-format', 'time');
|
|
chatTimestamp.innerHTML = dayjs(message.created).format('h:mm:ss a');
|
|
column.appendChild(chatTimestamp);
|
|
|
|
this.chat.messageList.appendChild(systemMessage);
|
|
this.chat.messages.push(systemMessage);
|
|
|
|
while (this.chat.messages.length > 50) {
|
|
const message = this.chat.messages.shift();
|
|
this.chat.messageList.removeChild(message);
|
|
}
|
|
if (this.chat.isAtBottom) {
|
|
this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight + 50000);
|
|
}
|
|
}
|
|
|
|
async joinChatChannel (room) {
|
|
try {
|
|
const response = await fetch(`/chat/room/${dtp.room._id}/join`);
|
|
await this.processResponse(response);
|
|
|
|
await this.socket.joinChannel(dtp.room._id, 'ChatRoom');
|
|
} catch (error) {
|
|
this.log.error('failed to join chat room', { room, error });
|
|
UIkit.modal.alert(`Failed to join chat room: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async confirmNavigation (event) {
|
|
const target = event.currentTarget || event.target;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const href = target.getAttribute('href');
|
|
const hrefTarget = target.getAttribute('target');
|
|
const text = target.textContent;
|
|
const whitelist = [
|
|
'digitaltelepresence.com',
|
|
'www.digitaltelepresence.com',
|
|
'chat.digitaltelepresence.com',
|
|
'sites.digitaltelepresence.com',
|
|
];
|
|
try {
|
|
const url = new URL(href);
|
|
if (!whitelist.includes(url.hostname)) {
|
|
await UIkit.modal.confirm(`<p>You are navigating to <code>${href}</code>, a link or button that was displayed as:</p><p>${text}</p><div class="uk-text-small uk-text-danger">Please only open links to destinations you trust and want to visit.</div>`);
|
|
}
|
|
window.open(href, hrefTarget);
|
|
} catch (error) {
|
|
this.log.info('confirmNavigation', 'navigation canceled', { error });
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async confirmRoomDelete (event) {
|
|
const target = event.currentTarget || event.target;
|
|
const roomId = target.getAttribute('data-room-id');
|
|
const roomName = target.getAttribute('data-room-name');
|
|
try {
|
|
await UIkit.modal.confirm(`Are you sure you want to delete "${roomName}"?`);
|
|
} catch (error) {
|
|
return;
|
|
}
|
|
try {
|
|
const response = await fetch(`/chat/room/${roomId}`, { method: 'DELETE' });
|
|
await this.processResponse(response);
|
|
} catch (error) {
|
|
UIkit.modal.alert(error.message);
|
|
}
|
|
}
|
|
|
|
async generateOtpQR (canvas, keyURI) {
|
|
QRCode.toCanvas(canvas, keyURI);
|
|
}
|
|
|
|
async generateQRCanvas (canvas, uri) {
|
|
this.log.info('generateQRCanvas', 'creating QR code canvas', { uri });
|
|
QRCode.toCanvas(canvas, uri, { width: 256 });
|
|
}
|
|
|
|
async closeAllDropdowns ( ) {
|
|
const dropdowns = document.querySelectorAll('.uk-dropdown.uk-open');
|
|
for (const dropdown of dropdowns) {
|
|
this.log.info('closeAllDropdowns', 'closing dropdown', { dropdown });
|
|
UIkit.dropdown(dropdown).hide(false);
|
|
}
|
|
}
|
|
|
|
async muteChatUser (event) {
|
|
const target = (event.currentTarget || event.target);
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
this.closeAllDropdowns();
|
|
|
|
const messageId = target.getAttribute('data-message-id');
|
|
const userId = target.getAttribute('data-user-id');
|
|
const username = target.getAttribute('data-username');
|
|
|
|
try {
|
|
await UIkit.modal.confirm(`Are you sure you want to mute ${username}?`);
|
|
} catch (error) {
|
|
// canceled or error
|
|
return;
|
|
}
|
|
|
|
this.log.info('muteChatUser', 'muting chat user', { messageId, userId, username });
|
|
this.mutedUsers.push({ userId, username });
|
|
window.localStorage.mutedUsers = JSON.stringify(this.mutedUsers);
|
|
|
|
document.querySelectorAll(`.chat-message[data-author-id="${userId}"]`).forEach((message) => {
|
|
message.parentElement.removeChild(message);
|
|
});
|
|
}
|
|
|
|
async unmuteChatUser (event) {
|
|
const target = (event.currentTarget || event.target);
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const userId = target.getAttribute('data-user-id');
|
|
const username = target.getAttribute('data-username');
|
|
this.log.info('muteChatUser', 'muting chat user', { userId, username });
|
|
|
|
this.mutedUsers = this.mutedUsers.filter((block) => block.userId !== userId);
|
|
window.localStorage.mutedUsers = JSON.stringify(this.mutedUsers);
|
|
|
|
const entry = document.querySelector(`.chat-muted-user[data-user-id="${userId}"]`);
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
entry.parentElement.removeChild(entry);
|
|
}
|
|
|
|
async filterChatView ( ) {
|
|
if (!this.mutedUsers || (this.mutedUsers.length === 0)) {
|
|
return;
|
|
}
|
|
this.mutedUsers.forEach((block) => {
|
|
document.querySelectorAll(`.chat-message[data-author-id="${block.userId}"]`).forEach((message) => {
|
|
message.parentElement.removeChild(message);
|
|
});
|
|
});
|
|
}
|
|
|
|
async initSettingsView ( ) {
|
|
this.log.info('initSettingsView', 'settings', { settings: this.settings });
|
|
|
|
const mutedUserList = document.querySelector('ul#muted-user-list');
|
|
for (const block of this.mutedUsers) {
|
|
const li = document.createElement(`li`);
|
|
li.setAttribute('data-user-id', block.userId);
|
|
li.classList.add('chat-muted-user');
|
|
mutedUserList.appendChild(li);
|
|
|
|
const grid = document.createElement('div');
|
|
grid.setAttribute('uk-grid', '');
|
|
grid.classList.add('uk-grid-small');
|
|
grid.classList.add('uk-flex-middle');
|
|
li.appendChild(grid);
|
|
|
|
let column = document.createElement('div');
|
|
column.classList.add('uk-width-expand');
|
|
column.textContent = block.username;
|
|
grid.appendChild(column);
|
|
|
|
column = document.createElement('div');
|
|
column.classList.add('uk-width-auto');
|
|
grid.appendChild(column);
|
|
|
|
const button = document.createElement('button');
|
|
button.setAttribute('type', 'button');
|
|
button.setAttribute('title', `Remove ${block.username} from your mute list`);
|
|
button.setAttribute('data-user-id', block.userId);
|
|
button.setAttribute('data-username', block.username);
|
|
button.setAttribute('onclick', "return dtp.app.unmuteChatUser(event);");
|
|
button.classList.add('uk-button');
|
|
button.classList.add('uk-button-default');
|
|
button.classList.add('uk-button-small');
|
|
button.classList.add('uk-border-rounded');
|
|
button.textContent = 'Unmute';
|
|
column.appendChild(button);
|
|
}
|
|
}
|
|
|
|
loadSettings ( ) {
|
|
this.settings = { tutorials: { } };
|
|
if (window.localStorage) {
|
|
if (window.localStorage.settings) {
|
|
this.settings = JSON.parse(window.localStorage.settings);
|
|
} else {
|
|
this.saveSettings();
|
|
}
|
|
this.mutedUsers = window.localStorage.mutedUsers ? JSON.parse(window.localStorage.mutedUsers) : [ ];
|
|
this.filterChatView();
|
|
}
|
|
this.settings.tutorials = this.settings.tutorials || { };
|
|
}
|
|
|
|
saveSettings ( ) {
|
|
if (!window.localStorage) { return; }
|
|
window.localStorage.settings = JSON.stringify(this.settings);
|
|
}
|
|
|
|
async submitImageForm (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const formElement = event.currentTarget || event.target;
|
|
const form = new FormData(formElement);
|
|
|
|
this.cropper.getCroppedCanvas().toBlob(async (imageData) => {
|
|
try {
|
|
const imageId = formElement.getAttribute('data-image-id');
|
|
form.append('imageFile', imageData, imageId);
|
|
|
|
this.log.info('submitImageForm', 'uploading image', { event, action: formElement.action });
|
|
const response = await fetch(formElement.action, {
|
|
method: formElement.method,
|
|
body: form,
|
|
});
|
|
await this.processResponse(response);
|
|
} catch (error) {
|
|
UIkit.modal.alert(`Failed to upload image: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
async selectImageFile (event) {
|
|
event.preventDefault();
|
|
|
|
const imageId = event.target.getAttribute('data-image-id');
|
|
|
|
//z read the cropper options from the element on the page
|
|
let cropperOptions = event.target.getAttribute('data-cropper-options');
|
|
if (cropperOptions) {
|
|
cropperOptions = JSON.parse(cropperOptions);
|
|
}
|
|
this.log.debug('selectImageFile', 'cropper options', { cropperOptions }); //z remove when done
|
|
|
|
const fileSelectContainerId = event.target.getAttribute('data-file-select-container');
|
|
if (!fileSelectContainerId) {
|
|
UIkit.modal.alert('Missing file select container element ID information');
|
|
return;
|
|
}
|
|
|
|
const fileSelectContainer = document.getElementById(fileSelectContainerId);
|
|
if (!fileSelectContainer) {
|
|
UIkit.modal.alert('Missing file select element');
|
|
return;
|
|
}
|
|
|
|
const fileSelect = fileSelectContainer.querySelector('input[type="file"]');
|
|
if (!fileSelect.files || (fileSelect.files.length === 0)) {
|
|
return;
|
|
}
|
|
|
|
const selectedFile = fileSelect.files[0];
|
|
if (!selectedFile) {
|
|
return;
|
|
}
|
|
|
|
this.log.debug('selectImageFile', 'thumbnail file select', { event, selectedFile });
|
|
|
|
const filter = /^(image\/jpg|image\/jpeg|image\/png)$/i;
|
|
if (!filter.test(selectedFile.type)) {
|
|
UIkit.modal.alert(`Unsupported image file type selected: ${selectedFile.type}`);
|
|
return;
|
|
}
|
|
|
|
const fileSizeId = event.target.getAttribute('data-file-size-element');
|
|
const FILE_MAX_SIZE = parseInt(fileSelect.getAttribute('data-file-max-size'), 10);
|
|
const fileSize = document.getElementById(fileSizeId);
|
|
fileSize.textContent = numeral(selectedFile.size).format('0,0.0b');
|
|
if (selectedFile.size > (FILE_MAX_SIZE)) {
|
|
UIkit.modal.alert(`File is too large: ${fileSize.textContent}. Custom thumbnail images may be up to ${numeral(FILE_MAX_SIZE).format('0,0.00b')} in size.`);
|
|
return;
|
|
}
|
|
|
|
// const IMAGE_WIDTH = parseInt(event.target.getAttribute('data-image-w'));
|
|
// const IMAGE_HEIGHT = parseInt(event.target.getAttribute('data-image-h'));
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
const img = document.getElementById(imageId);
|
|
img.onload = (e) => {
|
|
console.log('image loaded', e, img.naturalWidth, img.naturalHeight);
|
|
// if (img.naturalWidth !== IMAGE_WIDTH || img.naturalHeight !== IMAGE_HEIGHT) {
|
|
// UIkit.modal.alert(`This image must be ${IMAGE_WIDTH}x${IMAGE_HEIGHT}`);
|
|
// img.setAttribute('hidden', '');
|
|
// img.src = '';
|
|
// return;
|
|
// }
|
|
|
|
fileSelectContainer.querySelector('#file-name').textContent = selectedFile.name;
|
|
fileSelectContainer.querySelector('#file-modified').textContent = dayjs(selectedFile.lastModifiedDate).fromNow();
|
|
fileSelectContainer.querySelector('#image-resolution-w').textContent = img.naturalWidth.toString();
|
|
fileSelectContainer.querySelector('#image-resolution-h').textContent = img.naturalHeight.toString();
|
|
|
|
fileSelectContainer.querySelector('#file-select').setAttribute('hidden', true);
|
|
fileSelectContainer.querySelector('#file-info').removeAttribute('hidden');
|
|
|
|
fileSelectContainer.querySelector('#file-save-btn').removeAttribute('hidden');
|
|
};
|
|
|
|
// set the image as the "src" of the <img> in the DOM.
|
|
img.src = e.target.result;
|
|
|
|
//z create cropper and set options here
|
|
this.createImageCropper(img, cropperOptions);
|
|
};
|
|
|
|
// read in the file, which will trigger everything else in the event handler above.
|
|
reader.readAsDataURL(selectedFile);
|
|
}
|
|
|
|
async createImageCropper (img, options) {
|
|
// https://github.com/fengyuanchen/cropperjs/blob/main/README.md#options
|
|
options = Object.assign({
|
|
aspectRatio: 1,
|
|
viewMode: 1, // restrict the crop box not to exceed the size of the canvas
|
|
dragMode: 'move',
|
|
autoCropArea: 0.85,
|
|
restore: false,
|
|
guides: false,
|
|
center: false,
|
|
highlight: false,
|
|
cropBoxMovable: true,
|
|
cropBoxResizable: true,
|
|
toggleDragModeOnDblclick: false,
|
|
modal: true,
|
|
}, options);
|
|
this.log.info("createImageCropper", "Creating image cropper", { img });
|
|
this.cropper = new Cropper(img, options);
|
|
}
|
|
|
|
async removeImageFile (event) {
|
|
const target = event.target || event.currentTarget;
|
|
const imageType = target.getAttribute('data-image-type');
|
|
const channelId = dtp.channel ? dtp.channel._id : dtp.channel;
|
|
|
|
try {
|
|
this.log.info('removeImageFile', 'request to remove image', event);
|
|
|
|
let imageUrl;
|
|
switch (imageType) {
|
|
case 'channel-thumbnail-file':
|
|
imageUrl = `/channel/${channelId}/thumbnail`;
|
|
break;
|
|
|
|
case 'profile-picture-file':
|
|
imageUrl = `/user/${this.user._id}/profile-photo`;
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`Invalid image type: ${imageType}`);
|
|
}
|
|
|
|
const response = await fetch(imageUrl, { method: 'DELETE' });
|
|
if (!response.ok) {
|
|
throw new Error('Server error');
|
|
}
|
|
|
|
await this.processResponse(response);
|
|
} catch (error) {
|
|
this.log.error('removeImageFile', 'failed to remove image', { error });
|
|
UIkit.modal.alert(`Failed to remove image: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
scrollChatToBottom (isAtBottom = true) {
|
|
if (this.chat && this.chat.messageList && isAtBottom) {
|
|
this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight);
|
|
setTimeout(( ) => {
|
|
this.chat.isAtBottom = true;
|
|
this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight);
|
|
this.chat.isModifying = false;
|
|
}, 25);
|
|
}
|
|
}
|
|
|
|
async onChatMessageListScroll (event) {
|
|
const scrollPos = this.chat.messageList.scrollTop + this.chat.messageList.clientHeight;
|
|
|
|
if (!this.chat.isModifying) {
|
|
this.chat.isAtBottom = (scrollPos >= (this.chat.messageList.scrollHeight - 10));
|
|
this.chat.isAtTop = (scrollPos <= 0);
|
|
}
|
|
|
|
if (event && (this.chat.isAtBottom || this.chat.isAtTop)) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
if (this.chat.isAtBottom) {
|
|
this.chat.messageMenu.classList.remove('chat-menu-visible');
|
|
} else {
|
|
this.chat.messageMenu.classList.add('chat-menu-visible');
|
|
}
|
|
}
|
|
|
|
async resumeChatScroll ( ) {
|
|
this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight + 50000);
|
|
this.chat.isAtBottom = true;
|
|
this.chat.messageMenu.classList.remove('chat-menu-visible');
|
|
}
|
|
|
|
async onWindowResize ( ) {
|
|
if (this.chat.messageList && this.chat.isAtBottom) {
|
|
this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight + 50000);
|
|
}
|
|
}
|
|
|
|
async showEmojiPicker (event) {
|
|
const target = event.currentTarget || event.target;
|
|
const emojiTargetSelector = target.getAttribute('data-target');
|
|
|
|
this.emojiPickerTarget = document.querySelector(emojiTargetSelector);
|
|
if (!this.emojiPickerTarget) {
|
|
UIkit.modal.alert('Invalid emoji picker target');
|
|
return;
|
|
}
|
|
|
|
this.emojiPickerDisplay.classList.add('picker-active');
|
|
}
|
|
|
|
async onEmojiPickerClose (event) {
|
|
if (!this.emojiPickerDisplay) {
|
|
return;
|
|
}
|
|
if (!event.target.classList.contains('emoji-picker-display')) {
|
|
return;
|
|
}
|
|
this.emojiPickerDisplay.classList.remove('picker-active');
|
|
}
|
|
|
|
async onEmojiPicked (event) {
|
|
event = event.detail;
|
|
this.log.info('onEmojiPicked', 'An emoji has been selected', { event });
|
|
this.emojiPickerTarget.value += event.unicode;
|
|
}
|
|
|
|
async processRoomInvite (event) {
|
|
const target = event.currentTarget || event.target;
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const roomId = target.getAttribute('data-room-id');
|
|
const inviteId = target.getAttribute('data-invite-id');
|
|
const action = target.getAttribute('data-invite-action');
|
|
|
|
try {
|
|
const url = `/chat/room/${roomId}/invite/${inviteId}`;
|
|
const payload = JSON.stringify({ action });
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': payload.length,
|
|
},
|
|
body: payload,
|
|
});
|
|
return this.processResponse(response);
|
|
} catch (error) {
|
|
this.log.error('processRoomInvite', 'failed to process room invite', { error });
|
|
UIkit.modal.alert(`Failed to process room invite: ${error.message}`);
|
|
}
|
|
}
|
|
}
|