From 63d172267282db6ffabbb8785af7ed4b77cfc661 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 4 Aug 2024 21:18:50 +1200 Subject: [PATCH] move conversation viewer to own vue component --- src/frontend/components/App.vue | 1080 +--------------- .../messages/ConversationViewer.vue | 1105 +++++++++++++++++ src/frontend/js/DialogUtils.js | 15 + src/frontend/js/Utils.js | 22 + 4 files changed, 1184 insertions(+), 1038 deletions(-) create mode 100644 src/frontend/components/messages/ConversationViewer.vue create mode 100644 src/frontend/js/DialogUtils.js diff --git a/src/frontend/components/App.vue b/src/frontend/components/App.vue index 4a7217f..d8374d6 100644 --- a/src/frontend/components/App.vue +++ b/src/frontend/components/App.vue @@ -263,334 +263,14 @@ - + @@ -1043,10 +719,13 @@ import SidebarLink from "./SidebarLink.vue"; import MessagesSidebar from "./messages/MessagesSidebar.vue"; import NomadNetworkSidebar from "./nomadnetwork/NomadNetworkSidebar.vue"; +import ConversationViewer from "./messages/ConversationViewer.vue"; +import DialogUtils from "../js/DialogUtils"; export default { name: 'App', components: { + ConversationViewer, NomadNetworkSidebar, MessagesSidebar, SidebarLink, @@ -1061,14 +740,6 @@ export default { isShowingAnnounceSection: true, isShowingCallsSection: true, - newMessageText: "", - newMessageImage: null, - newMessageImageUrl: null, - newMessageAudio: null, - newMessageFiles: [], - isSendingMessage: false, - autoScrollOnNewMessage: true, - displayName: "Anonymous Peer", config: null, appInfo: null, @@ -1080,7 +751,6 @@ export default { peers: {}, selectedPeer: null, - selectedPeerPath: null, nodes: {}, selectedNode: null, @@ -1088,9 +758,6 @@ export default { conversations: [], - lxmfMessagesRequestSequence: 0, - chatItems: [], - isLoadingNodePage: false, nodePageRequestSequence: 0, nodePagePath: null, @@ -1161,25 +828,6 @@ export default { comports: [], - isRecordingAudioAttachment: false, - audioAttachmentMicrophoneRecorder: null, - audioAttachmentRecordingStartedAt: null, - audioAttachmentRecordingDuration: null, - audioAttachmentRecordingTimer: null, - lxmfMessageAudioAttachmentCache: {}, - lxmfAudioModeToCodec2ModeMap: { - // https://github.com/markqvist/LXMF/blob/master/LXMF/LXMF.py#L21 - 0x01: "450PWB", // AM_CODEC2_450PWB - 0x02: "450", // AM_CODEC2_450 - 0x03: "700C", // AM_CODEC2_700C - 0x04: "1200", // AM_CODEC2_1200 - 0x05: "1300", // AM_CODEC2_1300 - 0x06: "1400", // AM_CODEC2_1400 - 0x07: "1600", // AM_CODEC2_1600 - 0x08: "2400", // AM_CODEC2_2400 - 0x09: "3200", // AM_CODEC2_3200 - }, - }; }, mounted() { @@ -1265,39 +913,10 @@ export default { } case 'lxmf.delivery': { - // add inbound message to ui - this.chatItems.push({ - "type": "lxmf_message", - "lxmf_message": json.lxmf_message, - }); - - // if inbound message is for a conversation we are currently looking at, mark it as read - if(this.tab === "messages" - && json.lxmf_message.source_hash === this.selectedPeer?.destination_hash){ - - // find conversation - const conversation = this.findConversation(this.selectedPeer.destination_hash); - if(conversation){ - this.markConversationAsRead(conversation); - } - - } - - // show notification for new messages if window is not focussed - if(!document.hasFocus()){ - Notification.requestPermission().then((result) => { - if(result === "granted"){ - new window.Notification("New Message", { - body: "Someone sent you a message.", - tag: "new_message", // only ever show one notification at a time - }); - } - }); - } - - // auto scroll to bottom if we want to - if(this.autoScrollOnNewMessage){ - this.scrollMessagesToBottom(); + // pass lxmf message to conversation viewer + const conversationViewer = this.$refs["conversation-viewer"]; + if(conversationViewer){ + conversationViewer.onLxmfMessageReceived(json.lxmf_message); } break; @@ -1305,13 +924,10 @@ export default { } case 'lxmf_message_created': { - // add new outbound lxmf message from server - if(!this.isLxmfMessageInUi(json.lxmf_message.hash)){ - this.chatItems.push({ - "type": "lxmf_message", - "lxmf_message": json.lxmf_message, - "is_outbound": true, - }); + // pass lxmf message to conversation viewer + const conversationViewer = this.$refs["conversation-viewer"]; + if(conversationViewer){ + conversationViewer.onLxmfMessageCreated(json.lxmf_message); } break; @@ -1319,28 +935,21 @@ export default { } case 'lxmf_message_state_updated': { - // find existing chat item by lxmf message hash - const lxmfMessageHash = json.lxmf_message.hash; - const chatItemIndex = this.chatItems.findIndex((chatItem) => chatItem.lxmf_message?.hash === lxmfMessageHash); - if(chatItemIndex === -1){ - console.log("did not find existing chat item index for lxmf message hash: " + json.lxmf_message.hash); - return; + // pass lxmf message to conversation viewer + const conversationViewer = this.$refs["conversation-viewer"]; + if(conversationViewer){ + conversationViewer.onLxmfMessageUpdated(json.lxmf_message); } - // update lxmf message from server, while ensuring ui updates from nested object change - this.chatItems[chatItemIndex].lxmf_message = json.lxmf_message; - break; } case 'lxmf_message_deleted': { - // remove existing chat item by lxmf message hash - const lxmfMessageHash = json.hash; - if(lxmfMessageHash){ - this.chatItems = this.chatItems.filter((item) => { - return item.lxmf_message?.hash !== lxmfMessageHash; - }); + // pass lxmf message hash to conversation viewer + const conversationViewer = this.$refs["conversation-viewer"]; + if(conversationViewer){ + conversationViewer.onLxmfMessageDeleted(json.hash); } break; @@ -1427,17 +1036,6 @@ export default { this.ws.close(); } }, - scrollMessagesToBottom: function() { - Vue.nextTick(() => { - const container = document.getElementById("messages"); - if(container){ - container.scrollTop = container.scrollHeight; - } - }); - }, - isLxmfMessageInUi: function(hash) { - return this.chatItems.findIndex((chatItem) => chatItem.lxmf_message?.hash === hash) !== -1; - }, async getAppInfo() { try { const response = await window.axios.get(`/api/v1/app/info`); @@ -1461,158 +1059,13 @@ export default { try { await window.axios.get(`/api/v1/announce`); } catch(e) { - this.alert("failed to announce"); + DialogUtils.alert("failed to announce"); console.log(e); } // fetch config so it updates last announced timestamp await this.getConfig(); - }, - async sendMessage() { - - // do nothing if can't send message - if(!this.canSendMessage){ - return; - } - - // do nothing if no peer selected - if(!this.selectedPeer){ - return; - } - - this.isSendingMessage = true; - - try { - - // build fields - const fields = {}; - - // add file attachments - var fileAttachmentsTotalSize = 0; - if(this.newMessageFiles.length > 0){ - const fileAttachments = []; - for(const file of this.newMessageFiles){ - fileAttachmentsTotalSize += file.size; - fileAttachments.push({ - "file_name": file.name, - "file_bytes": this.arrayBufferToBase64(await file.arrayBuffer()), - }); - } - fields["file_attachments"] = fileAttachments; - } - - // add image attachment - var imageTotalSize = 0; - if(this.newMessageImage){ - imageTotalSize = this.newMessageImage.size; - fields["image"] = { - // Reticulum sends image type as "jpg" or "png" and not "image/jpg" or "image/png" - "image_type": this.newMessageImage.type.replace("image/", ""), - "image_bytes": this.arrayBufferToBase64(await this.newMessageImage.arrayBuffer()), - }; - } - - // add audio attachment - var audioTotalSize = 0; - if(this.newMessageAudio){ - audioTotalSize = this.newMessageImage.size; - fields["audio"] = { - "audio_mode": this.newMessageAudio.audio_mode, - "audio_bytes": this.arrayBufferToBase64(await this.newMessageAudio.audio_blob.arrayBuffer()), - }; - } - - // calculate estimated message size in bytes - const contentSize = this.newMessageText.length; - const totalMessageSize = contentSize + fileAttachmentsTotalSize + imageTotalSize + audioTotalSize; - - // ask user if they still want to send message if it may be rejected by sender - if(totalMessageSize > 1000 * 900){ // actual limit in LXMF Router is 1mb - if(!confirm(`Your message exceeds 900KB (It's ${this.formatBytes(totalMessageSize)}). It may be rejected by the recipient unless they have increased their delivery limit. Do you want to try sending anyway?`)){ - return; - } - } - - // send message to reticulum - const response = await window.axios.post(`/api/v1/lxmf-messages/send`, { - "lxmf_message": { - "destination_hash": this.selectedPeer.destination_hash, - "content": this.newMessageText, - "fields": fields, - }, - }); - - // add outbound message to ui - if(!this.isLxmfMessageInUi(response.data.lxmf_message.hash)){ - this.chatItems.push({ - "type": "lxmf_message", - "lxmf_message": response.data.lxmf_message, - "is_outbound": true, - }); - } - - // always scroll to bottom since we just sent a message - this.scrollMessagesToBottom(); - - // clear message inputs - this.newMessageText = ""; - this.newMessageImage = null; - this.newMessageImageUrl = null; - this.newMessageAudio = null; - this.newMessageFiles = []; - this.clearImageInput(); - this.clearFileInput(); - - } catch(e) { - - // show error - const message = e.response?.data?.message ?? "failed to send message"; - this.alert(message); - console.log(e); - - } finally { - this.isSendingMessage = false; - } - - }, - async retrySendingMessage(chatItem) { - - // force delete existing message - await this.deleteChatItem(chatItem, false); - - try { - - // send message to reticulum - const response = await window.axios.post(`/api/v1/lxmf-messages/send`, { - "lxmf_message": { - "destination_hash": chatItem.lxmf_message.destination_hash, - "content": chatItem.lxmf_message.content, - "fields": chatItem.lxmf_message.fields, - }, - }); - - // add outbound message to ui - if(!this.isLxmfMessageInUi(response.data.lxmf_message.hash)){ - this.chatItems.push({ - "type": "lxmf_message", - "lxmf_message": response.data.lxmf_message, - "is_outbound": true, - }); - } - - // always scroll to bottom since we just sent a message - this.scrollMessagesToBottom(); - - } catch(e) { - - // show error - const message = e.response?.data?.message ?? "failed to send message"; - this.alert(message); - console.log(e); - - } - }, async updateConfig(config) { @@ -1663,7 +1116,7 @@ export default { // simple attempt to prevent garbage input if(destinationHash.length !== 32){ - this.alert("Invalid Address"); + DialogUtils.alert("Invalid Address"); return; } @@ -1678,7 +1131,7 @@ export default { // do nothing if not connected to websocket if(!this.isWebsocketConnected){ - this.alert("Not connected to WebSocket!"); + DialogUtils.alert("Not connected to WebSocket!"); return; } @@ -1709,7 +1162,7 @@ export default { // do nothing if not connected to websocket if(!this.isWebsocketConnected){ - this.alert("Not connected to WebSocket!"); + DialogUtils.alert("Not connected to WebSocket!"); return; } @@ -1787,24 +1240,6 @@ export default { console.log(e); } }, - async getPeerPath(destinationHash) { - - // clear previous known path - this.selectedPeerPath = null; - - try { - - // get path to destination - const response = await window.axios.get(`/api/v1/destination/${destinationHash}/path`); - - // update ui - this.selectedPeerPath = response.data.path; - - } catch(e) { - console.log(e); - } - - }, async getNodePath(destinationHash) { // clear previous known path @@ -1859,40 +1294,6 @@ export default { name: this.getNodeNameFromAppData(announce.app_data), }; }, - async loadLxmfMessages(destinationHash) { - const seq = ++this.lxmfMessagesRequestSequence; - try { - - // fetch lxmf messages from "us to destination" and from "destination to us" - const response = await window.axios.get(`/api/v1/lxmf-messages/conversation/${destinationHash}`); - - // do nothing if response is for a previous request - if(seq !== this.lxmfMessagesRequestSequence){ - console.log("ignoring response for previous lxmf messages request") - return; - } - - // convert lxmf messages to chat items - const chatItems = []; - const lxmfMessages = response.data.lxmf_messages; - for(const lxmfMessage of lxmfMessages){ - chatItems.push({ - "type": "lxmf_message", - "is_outbound": this.config.lxmf_address_hash === lxmfMessage.source_hash, - "lxmf_message": lxmfMessage, - }); - } - - // update ui - this.chatItems = chatItems; - - // scroll to bottom - this.scrollMessagesToBottom(); - - } catch(e) { - // do nothing if failed to load messages - } - }, async loadInterfaces() { try { @@ -2154,7 +1555,7 @@ export default { // prevent simultaneous downloads if(this.isDownloadingNodeFile){ - this.alert("An existing download is in progress. Please wait for it to finish beforing starting another download."); + DialogUtils.alert("An existing download is in progress. Please wait for it to finish beforing starting another download."); return; } @@ -2178,7 +1579,7 @@ export default { this.isDownloadingNodeFile = false; // show error message - this.alert(`Failed to download file: ${failureReason}`); + DialogUtils.alert(`Failed to download file: ${failureReason}`); }, (progress) => { this.nodeFileProgress = Math.round(progress * 100); @@ -2201,62 +1602,7 @@ export default { } // unsupported url - this.alert("unsupported url: " + url); - - }, - async deleteChatItem(chatItem, shouldConfirm = true) { - try { - - // ask user to confirm deleting message - if(shouldConfirm && !confirm("Are you sure you want to delete this message? This can not be undone!")){ - return; - } - - // make sure it's an lxmf message - if(chatItem.type !== "lxmf_message"){ - return; - } - - // delete lxmf message from server - await window.axios.delete(`/api/v1/lxmf-messages/${chatItem.lxmf_message.hash}`); - - // remove lxmf message from chat items using hash, as other pending items might not have an id yet - this.chatItems = this.chatItems.filter((item) => { - return item.lxmf_message?.hash !== chatItem.lxmf_message.hash; - }); - - } catch(e) { - // do nothing if failed to delete message - } - }, - addNewLine: function() { - this.newMessageText += "\n"; - }, - onEnterPressed: function() { - - // add new line on mobile - if(this.isMobile){ - this.addNewLine(); - return; - } - - // send message on desktop - this.sendMessage(); - - }, - onShiftEnterPressed: function() { - this.addNewLine(); - }, - openImage: async function(url) { - - // convert data uri to blob - const blob = await (await fetch(url)).blob(); - - // create blob url - const fileUrl = window.URL.createObjectURL(blob); - - // open new tab - window.open(fileUrl); + DialogUtils.alert("unsupported url: " + url); }, downloadFileFromBase64: async function(fileName, fileBytesBase64) { @@ -2293,9 +1639,6 @@ export default { onPeerClick: function(peer) { this.selectedPeer = peer; this.tab = "messages"; - this.chatItems = []; - this.getPeerPath(peer.destination_hash); - this.loadLxmfMessages(peer.destination_hash); }, onNodeClick: function(node) { this.selectedNode = node; @@ -2308,7 +1651,7 @@ export default { this.onPeerClick(conversation); // mark conversation as read - this.markConversationAsRead(conversation); + this.$refs["conversation-viewer"].markConversationAsRead(conversation); }, parseSeconds: function(secondsToFormat) { @@ -2368,142 +1711,12 @@ export default { } }, - formatMinutesSeconds: function(seconds) { - const parsedSeconds = this.parseSeconds(seconds); - const paddedMinutes = parsedSeconds.minutes.toString().padStart(2, "0"); - const paddedSeconds = parsedSeconds.seconds.toString().padStart(2, "0"); - return `${paddedMinutes}:${paddedSeconds}`; - }, getNomadnetPageDownloadCallbackKey: function(destinationHash, pagePath) { return `${destinationHash}:${pagePath}`; }, getNomadnetFileDownloadCallbackKey: function(destinationHash, filePath) { return `${destinationHash}:${filePath}`; }, - addFilesToMessage: function() { - this.$refs["file-input"].click(); - }, - onFileInputChange: function(event) { - for(const file of event.target.files){ - this.newMessageFiles.push(file); - } - }, - clearFileInput: function() { - this.$refs["file-input"].value = null; - }, - removeFileAttachment: function(file) { - this.newMessageFiles = this.newMessageFiles.filter((newMessageFile) => { - return newMessageFile !== file; - }); - }, - addImageToMessage: function() { - this.$refs["image-input"].click(); - }, - onImageInputChange: function(event) { - if(event.target.files.length > 0){ - - // update selected file - this.newMessageImage = event.target.files[0]; - - // update image url when file is read - const fileReader = new FileReader(); - fileReader.onload = (event) => { - this.newMessageImageUrl = event.target.result - } - - // convert image to data url - fileReader.readAsDataURL(this.newMessageImage); - - // clear image input to allow selecting the same file after user removed it - this.clearImageInput(); - - } - }, - clearImageInput: function() { - this.$refs["image-input"].value = null; - }, - removeImageAttachment: function() { - this.newMessageImage = null; - this.newMessageImageUrl = null; - }, - async startRecordingAudioAttachment() { - - // do nothing if already recording - if(this.isRecordingAudioAttachment){ - return; - } - - // ask user to confirm recording new audio attachment, if an existing audio attachment exists - if(this.newMessageAudio && !confirm("An audio recording is already attached. A new recording will replace it. Do you want to continue?")){ - return; - } - - // start recording microphone - this.audioAttachmentMicrophoneRecorder = new Codec2MicrophoneRecorder(); - this.audioAttachmentRecordingStartedAt = Date.now(); - this.isRecordingAudioAttachment = await this.audioAttachmentMicrophoneRecorder.start(); - - // update recording time in ui every second - this.audioAttachmentRecordingDuration = this.formatMinutesSeconds(0); - this.audioAttachmentRecordingTimer = setInterval(() => { - const recordingDurationMillis = Date.now() - this.audioAttachmentRecordingStartedAt; - const recordingDurationSeconds = recordingDurationMillis / 1000; - this.audioAttachmentRecordingDuration = this.formatMinutesSeconds(recordingDurationSeconds); - }, 1000); - - // alert if failed to start recording - if(!this.isRecordingAudioAttachment){ - this.alert("failed to start recording"); - } - - }, - async stopRecordingAudioAttachment() { - - // clear audio recording timer - clearInterval(this.audioAttachmentRecordingTimer); - - // do nothing if not recording - if(!this.isRecordingAudioAttachment){ - return; - } - - // stop recording microphone and get audio - this.isRecordingAudioAttachment = false; - const audio = await this.audioAttachmentMicrophoneRecorder.stop(); - - // do nothing if no audio was provided - if(audio.length === 0){ - return; - } - - // decode codec2 audio back to wav so we can show a preview audio player before user sends it - const decoded = await Codec2Lib.runDecode("1200", new Uint8Array(audio)); - - // convert decoded codec2 to wav audio and create a blob - const wavAudio = await Codec2Lib.rawToWav(decoded); - const wavBlob = new Blob([wavAudio], { - type: "audio/wav", - }); - - // update message audio attachment - this.newMessageAudio = { - audio_mode: 0x04, // hardcoded to LXMF.AM_CODEC2_1200 for now - audio_blob: new Blob([audio]), - audio_wav_url: URL.createObjectURL(wavBlob), - }; - - }, - removeAudioAttachment: function() { - - // ask user to confirm removing audio attachment - if(!confirm("Are you sure you want to remove this audio attachment?")){ - return; - } - - // remove audio - this.newMessageAudio = null; - - }, formatBytes: function(bytes) { if(bytes === 0){ @@ -2543,62 +1756,6 @@ export default { const i = Math.floor(Math.log(hz) / Math.log(k)); return parseFloat((hz / Math.pow(k, i))) + ' ' + sizes[i]; - }, - arrayBufferToBase64: function(arrayBuffer) { - var binary = ''; - var bytes = new Uint8Array(arrayBuffer); - var len = bytes.byteLength; - for(var i = 0; i < len; i++){ - binary += String.fromCharCode(bytes[i]); - } - return window.btoa(binary); - }, - base64ToArrayBuffer: function(base64) { - return Uint8Array.from(atob(base64), c => c.charCodeAt(0)); - }, - async deleteConversation() { - - // do nothing if no peer selected - if(!this.selectedPeer){ - return; - } - - // ask user to confirm deleting conversation history - if(!confirm("Are you sure you want to delete all messages from this conversation? This can not be undone!")){ - return; - } - - // delete all lxmf messages from "us to destination" and from "destination to us" - try { - await window.axios.delete(`/api/v1/lxmf-messages/conversation/${this.selectedPeer.destination_hash}`); - } catch(e) { - this.alert("failed to delete conversation"); - console.log(e); - } - - // reload conversation - await this.loadLxmfMessages(this.selectedPeer.destination_hash); - - // reload conversations - await this.getConversations(); - - }, - async markConversationAsRead(conversation) { - - // manually mark conversation read in memory to avoid delay updating ui - conversation.is_unread = false; - - // mark conversation as read on server - try { - await window.axios.get(`/api/v1/lxmf/conversations/${conversation.destination_hash}/mark-as-read`); - } catch(e) { - // do nothing if failed to mark as read - console.log(e); - } - - // reload conversations - await this.getConversations(); - }, async enableInterface(interfaceName) { @@ -2608,7 +1765,7 @@ export default { name: interfaceName, }); } catch(e) { - this.alert("failed to enable interface"); + DialogUtils.alert("failed to enable interface"); console.log(e); } @@ -2624,7 +1781,7 @@ export default { name: interfaceName, }); } catch(e) { - this.alert("failed to disable interface"); + DialogUtils.alert("failed to disable interface"); console.log(e); } @@ -2720,7 +1877,7 @@ export default { name: interfaceName, }); } catch(e) { - this.alert("failed to delete interface"); + DialogUtils.alert("failed to delete interface"); console.log(e); } @@ -2764,12 +1921,12 @@ export default { // show success message if(response.data.message){ - this.alert(response.data.message); + DialogUtils.alert(response.data.message); } } catch(e) { const message = e.response?.data?.message ?? "failed to add interface"; - this.alert(message); + DialogUtils.alert(message); console.log(e); } @@ -2777,15 +1934,8 @@ export default { await this.loadInterfaces(); }, - onChatItemClick: function(chatItem) { - if(!chatItem.is_actions_expanded){ - chatItem.is_actions_expanded = true; - } else { - chatItem.is_actions_expanded = false; - } - }, onDestinationPathClick: function(path) { - this.alert(`${path.hops} ${ path.hops === 1 ? 'hop' : 'hops' } away via ${path.next_hop_interface}`); + DialogUtils.alert(`${path.hops} ${ path.hops === 1 ? 'hop' : 'hops' } away via ${path.next_hop_interface}`); }, async updateCallsList() { try { @@ -2820,15 +1970,6 @@ export default { } }, - alert(message) { - if(window.electron){ - // running inside electron, use ipc alert - window.electron.alert(message); - } else { - // running inside normal browser, use browser alert - window.alert(message); - } - }, async prompt(message) { if(window.electron){ // running inside electron, use ipc prompt @@ -2849,98 +1990,7 @@ export default { return value === "on" || value === "yes" || value === "true"; }, onIFACSignatureClick: function(ifacSignature) { - this.alert(ifacSignature); - }, - findConversation: function(destinationHash) { - return this.conversations.find((conversation) => { - return conversation.destination_hash === destinationHash; - }); - }, - async processAudioForSelectedPeerChatItems() { - for(const chatItem of this.selectedPeerChatItems){ - - // skip if no audio - if(!chatItem.lxmf_message?.fields?.audio){ - continue; - } - - // skip if audio already cached - if(this.lxmfMessageAudioAttachmentCache[chatItem.lxmf_message.hash]){ - continue; - } - - // decode audio to blob url - const objectUrl = await this.decodeLxmfAudioFieldToBlobUrl(chatItem.lxmf_message.fields.audio); - if(!objectUrl){ - continue; - } - - // update audio cache - this.lxmfMessageAudioAttachmentCache[chatItem.lxmf_message.hash] = objectUrl; - - } - }, - async decodeLxmfAudioFieldToBlobUrl(audioField) { - try { - - // get audio mode and audio bytes from audio field - const audioMode = audioField.audio_mode; - const audioBytes = audioField.audio_bytes; - - // handle opus: AM_OPUS_OGG - if(audioMode === 0x10){ - return this.decodeOpusAudioToBlobUrl(audioField.audio_bytes); - } - - // determine codec2 mode, or skip if unknown - const codecMode = this.lxmfAudioModeToCodec2ModeMap[audioMode]; - if(!codecMode){ - console.log("unsupported audio mode: " + audioMode) - return null; - } - - // convert base64 to uint8 array - const encoded = this.base64ToArrayBuffer(audioBytes); - - // decode codec2 audio - const decoded = await Codec2Lib.runDecode(codecMode, new Uint8Array(encoded)); - - // convert decoded codec2 to wav audio - const wavAudio = await Codec2Lib.rawToWav(decoded); - - // create blob from wav audio - const blob = new Blob([wavAudio], { - type: "audio/wav", - }); - - // create object url for blob - return URL.createObjectURL(blob); - - } catch(e) { - // failed to decode lxmf audio field - console.log(e); - return null; - } - }, - async decodeOpusAudioToBlobUrl(audioBytes) { - try { - - // convert base64 to uint8 array - const opusAudioBytes = this.base64ToArrayBuffer(audioBytes); - - // create blob from opus audio - const blob = new Blob([opusAudioBytes], { - type: "audio/opus", - }); - - // create object url for blob - return URL.createObjectURL(blob); - - } catch(e) { - // failed to decode opus audio - console.log(e); - return null; - } + DialogUtils.alert(ifacSignature); }, }, computed: { @@ -2950,49 +2000,11 @@ export default { isMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); }, - selectedPeerChatItems() { - - // get all chat items related to the selected peer - if(this.selectedPeer){ - return this.chatItems.filter((chatItem) => { - - if(chatItem.type === "lxmf_message"){ - const isFromSelectedPeer = chatItem.lxmf_message.source_hash === this.selectedPeer.destination_hash; - const isToSelectedPeer = chatItem.lxmf_message.destination_hash === this.selectedPeer.destination_hash; - return isFromSelectedPeer || isToSelectedPeer; - } - - return false; - - - }); - } - - // no peer, so no chat items! - return []; - - }, unreadConversationsCount() { return this.conversations.filter((conversation) => { return conversation.is_unread; }).length; }, - canSendMessage() { - - // can't send if empty message - const messageText = this.newMessageText.trim(); - if(messageText == null || messageText === ""){ - return false; - } - - // can't send if already sending - if(this.isSendingMessage){ - return false; - } - - return true; - - }, activeAudioCalls() { return this.audioCalls.filter(function(audioCall) { return audioCall.is_active; @@ -3018,13 +2030,5 @@ export default { return results; }, }, - watch: { - async selectedPeerChatItems() { - - // chat items for selected peer changed, so lets process any available audio - await this.processAudioForSelectedPeerChatItems(); - - }, - }, } diff --git a/src/frontend/components/messages/ConversationViewer.vue b/src/frontend/components/messages/ConversationViewer.vue new file mode 100644 index 0000000..cce35bf --- /dev/null +++ b/src/frontend/components/messages/ConversationViewer.vue @@ -0,0 +1,1105 @@ + + + diff --git a/src/frontend/js/DialogUtils.js b/src/frontend/js/DialogUtils.js new file mode 100644 index 0000000..5dc8183 --- /dev/null +++ b/src/frontend/js/DialogUtils.js @@ -0,0 +1,15 @@ +class DialogUtils { + + static alert(message) { + if(window.electron){ + // running inside electron, use ipc alert + window.electron.alert(message); + } else { + // running inside normal browser, use browser alert + window.alert(message); + } + } + +} + +export default DialogUtils; diff --git a/src/frontend/js/Utils.js b/src/frontend/js/Utils.js index 9db0530..1e7c131 100644 --- a/src/frontend/js/Utils.js +++ b/src/frontend/js/Utils.js @@ -72,6 +72,28 @@ class Utils { return this.formatSeconds(secondsAgo); } + static formatSecondsAgo(seconds) { + const secondsAgo = Math.round((Date.now() / 1000) - seconds); + return this.formatSeconds(secondsAgo); + } + + static formatMinutesSeconds(seconds) { + const parsedSeconds = this.parseSeconds(seconds); + const paddedMinutes = parsedSeconds.minutes.toString().padStart(2, "0"); + const paddedSeconds = parsedSeconds.seconds.toString().padStart(2, "0"); + return `${paddedMinutes}:${paddedSeconds}`; + } + + static arrayBufferToBase64(arrayBuffer) { + var binary = ''; + var bytes = new Uint8Array(arrayBuffer); + var len = bytes.byteLength; + for(var i = 0; i < len; i++){ + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + } + } export default Utils;