DTP Base provides a scalable and secure Node.js application development harness ready for production service.
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

// 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}`);
}
}
}