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.
340 lines
11 KiB
340 lines
11 KiB
// base-client.js
|
|
// Copyright (C) 2024 DTP Technologies, LLC
|
|
// All Rights Reserved
|
|
|
|
'use strict';
|
|
|
|
const dtp = window.dtp = window.dtp || { };
|
|
|
|
import DtpApp from 'lib/dtp-app.js';
|
|
import BaseAudio from './base-audio.js';
|
|
|
|
import QRCode from 'qrcode';
|
|
import Cropper from 'cropperjs';
|
|
|
|
import dayjs from 'dayjs';
|
|
import dayjsRelativeTime from 'dayjs/plugin/relativeTime.js';
|
|
dayjs.extend(dayjsRelativeTime);
|
|
|
|
export class BaseApp extends DtpApp {
|
|
|
|
static get SFX_SAMPLE_SOUND ( ) { return 'sample-sound'; }
|
|
|
|
constructor (user) {
|
|
super('BaseApp', user);
|
|
|
|
this.loadSettings();
|
|
|
|
this.notificationPermission = 'default';
|
|
this.haveFocus = true; // hard to load the app w/o also being the focused app
|
|
|
|
window.addEventListener('dtp-load', this.onDtpLoad.bind(this));
|
|
window.addEventListener('focus', this.onWindowFocus.bind(this));
|
|
window.addEventListener('blur', this.onWindowBlur.bind(this));
|
|
|
|
/*
|
|
* Page Visibility API hooks
|
|
*/
|
|
window.addEventListener('pageshow', this.onWindowPageShow.bind(this));
|
|
window.addEventListener('pagehide', this.onWindowPageHide.bind(this));
|
|
window.addEventListener('freeze', this.onWindowFreeze.bind(this));
|
|
window.addEventListener('resume', this.onWindowResume.bind(this));
|
|
|
|
this.updateTimestamps();
|
|
|
|
this.log.info('constructor', 'BaseApp client online');
|
|
}
|
|
|
|
async onDtpLoad ( ) {
|
|
this.log.info('onDtpLoad', 'dtp-load event received. Connecting to platform.');
|
|
|
|
await this.connect({
|
|
mode: 'User',
|
|
onSocketConnect: this.onBaseSocketConnect.bind(this),
|
|
onSocketDisconnect: this.onBaseSocketDisconnect.bind(this),
|
|
});
|
|
}
|
|
|
|
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 onWindowPageShow (event) {
|
|
this.log.debug('onWindowPageShow', 'the page is being shown', { event });
|
|
}
|
|
|
|
async onWindowPageHide (event) {
|
|
this.log.debug('onWindowPageHide', 'the page is being hidden', { event });
|
|
if (!event.persisted) {
|
|
await this.socket.disconnect();
|
|
}
|
|
}
|
|
|
|
async onWindowFreeze (event) {
|
|
this.log.debug('onWindowFreeze', 'the page is being frozen', { event });
|
|
}
|
|
|
|
async onWindowResume (event) {
|
|
this.log.debug('onWindowResume', 'the page is being resumed', { event });
|
|
}
|
|
|
|
async startAudio ( ) {
|
|
this.log.info('startAudio', 'starting audio');
|
|
this.audio = new BaseAudio();
|
|
this.audio.start();
|
|
try {
|
|
await Promise.all([
|
|
this.audio.loadSound(BaseApp.SFX_SAMPLE_SOUND, '/static/sfx/sample-sound.mp3'),
|
|
]);
|
|
} catch (error) {
|
|
this.log.error('startAudio', 'failed to load sound', { error });
|
|
// fall through
|
|
}
|
|
}
|
|
|
|
async onBaseSocketConnect (socket) {
|
|
this.log.debug('onSocketConnect', 'attaching socket events');
|
|
|
|
this.systemMessageHandler = this.onSystemMessage.bind(this);
|
|
socket.on('system-message', this.systemMessageHandler);
|
|
}
|
|
|
|
async onBaseSocketDisconnect (socket) {
|
|
this.log.debug('onSocketDisconnect', 'detaching socket events');
|
|
|
|
socket.off('system-message', this.systemMessageHandler);
|
|
delete this.systemMessageHandler;
|
|
|
|
if (this.taskSession) {
|
|
await this.setTaskSessionStatus('reconnecting');
|
|
}
|
|
}
|
|
|
|
async onSystemMessage (message) {
|
|
if (message.displayList) {
|
|
this.displayEngine.executeDisplayList(message.displayList);
|
|
}
|
|
}
|
|
|
|
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',
|
|
];
|
|
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 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 initSettingsView ( ) {
|
|
this.log.info('initSettingsView', 'settings', { settings: this.settings });
|
|
}
|
|
|
|
loadSettings ( ) {
|
|
this.settings = { };
|
|
if (window.localStorage) {
|
|
if (window.localStorage.settings) {
|
|
this.settings = JSON.parse(window.localStorage.settings);
|
|
} else {
|
|
this.saveSettings();
|
|
}
|
|
}
|
|
}
|
|
|
|
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');
|
|
|
|
let cropperOptions = event.target.getAttribute('data-cropper-options');
|
|
if (cropperOptions) {
|
|
cropperOptions = JSON.parse(cropperOptions);
|
|
}
|
|
|
|
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 reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
const img = document.getElementById(imageId);
|
|
img.onload = (e) => {
|
|
console.log('image loaded', e, img.naturalWidth, img.naturalHeight);
|
|
|
|
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');
|
|
};
|
|
|
|
img.src = e.target.result;
|
|
this.createImageCropper(img, cropperOptions);
|
|
};
|
|
|
|
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');
|
|
|
|
try {
|
|
this.log.info('removeImageFile', 'request to remove image', event);
|
|
|
|
let imageUrl;
|
|
switch (imageType) {
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
async onWindowResize ( ) {
|
|
if (this.chat.messageList && this.chat.isAtBottom) {
|
|
this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight + 50000);
|
|
}
|
|
}
|
|
}
|