// 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(`

You are navigating to ${href}, a link or button that was displayed as:

${text}

Please only open links to destinations you trust and want to visit.
`); } 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); } } }