From 1611cee234d33c994f3e79f8c2dd4143bad879a0 Mon Sep 17 00:00:00 2001 From: Andrew Gundersen Date: Fri, 26 Feb 2021 15:40:55 -0600 Subject: [PATCH 001/163] merged css branch into main --- src/components/messageView/anime.ts | 60 --------- src/components/messageView/index.vue | 90 ------------- .../messageView/message/calcPosition.ts | 33 ----- .../messageView/message/message.vue | 101 --------------- src/components/messageView/messageView.vue | 120 ------------------ .../messageView/offsetCalculator.ts | 41 ------ .../messageView/textBubble/computed.ts | 79 ------------ .../messageView/textBubble/drawer.ts | 50 -------- .../messageView/textBubble/textBubble.vue | 116 ----------------- src/components/messageView/transitions.ts | 100 --------------- .../textBubble/bubbleDetails/computed.ts | 81 ------------ 11 files changed, 871 deletions(-) delete mode 100644 src/components/messageView/anime.ts delete mode 100644 src/components/messageView/index.vue delete mode 100644 src/components/messageView/message/calcPosition.ts delete mode 100644 src/components/messageView/message/message.vue delete mode 100644 src/components/messageView/messageView.vue delete mode 100644 src/components/messageView/offsetCalculator.ts delete mode 100644 src/components/messageView/textBubble/computed.ts delete mode 100644 src/components/messageView/textBubble/drawer.ts delete mode 100644 src/components/messageView/textBubble/textBubble.vue delete mode 100644 src/components/messageView/transitions.ts delete mode 100644 src/components/textBubble/bubbleDetails/computed.ts diff --git a/src/components/messageView/anime.ts b/src/components/messageView/anime.ts deleted file mode 100644 index 0e56eb4..0000000 --- a/src/components/messageView/anime.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { inject } from "vue"; -import AnimeFunc from "@/types/animejs/index"; - -/** - * anime js animations used by message view transitions - */ -export default function useMessageViewAnims() { - // imports animejs safely - let anime: AnimeFunc; - const animeInject: AnimeFunc | undefined = inject("animejs"); - if (animeInject) anime = animeInject; - - // update message View bottom bound - const shiftView = (el: SVGGElement, transform: string) => { - anime({ - targets: el, - transform: transform, - easing: "easeInOutQuad", - duration: 500, - }); - } - - // stacks message from top to bottom - const stackAnimate = (el: Element, transform: string) => { - return new Promise((resolve, _reject) => { - anime({ - targets: el, - transform: transform, - easing: "easeInOutQuad", - duration: 1000, - complete: function() { - resolve(); - } - }); - }) - } - - // adds new message at the bottom of the list and shifts existing message list - const shiftAnimate = (el: Element, transform: string, view: SVGGElement, viewTransform: string) => { - return new Promise((resolve, _reject) => { - anime({ - targets: el, - transform: transform, - easing: "easeInOutQuad", - duration: 800, - begin: function() { - shiftView(view, viewTransform); - }, - complete: function() { - resolve(); - } - }); - }) - } - - return { - shiftAnimate, - stackAnimate, - }; -} diff --git a/src/components/messageView/index.vue b/src/components/messageView/index.vue deleted file mode 100644 index 262303c..0000000 --- a/src/components/messageView/index.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - diff --git a/src/components/messageView/message/calcPosition.ts b/src/components/messageView/message/calcPosition.ts deleted file mode 100644 index ada379c..0000000 --- a/src/components/messageView/message/calcPosition.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {computed} from "vue"; - -// Size of "side-gutters" of message window. -const margin = 20; -const messagePadding = 15; -const firstChildMargin = 45; - - -function calcXPosition(messageWidth: number, windowWidth: number, modifier: string) { - let offset = margin; - - if (modifier == "sf") { - offset = windowWidth - messageWidth - margin; - } - - return offset -} - -function calcMessagePosition(modifier: string, messageWidth: number, - windowWidth: number, anchorPosition: any) { - - const X = calcXPosition(messageWidth, windowWidth, modifier); - - // Depends on if first child. - let Y = firstChildMargin; - if (anchorPosition != "none") { - Y = anchorPosition + messagePadding; - } - - return `translate(${X} ${Y})`; -} - -export default calcMessagePosition; \ No newline at end of file diff --git a/src/components/messageView/message/message.vue b/src/components/messageView/message/message.vue deleted file mode 100644 index bd8e804..0000000 --- a/src/components/messageView/message/message.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - diff --git a/src/components/messageView/messageView.vue b/src/components/messageView/messageView.vue deleted file mode 100644 index 5b19dfa..0000000 --- a/src/components/messageView/messageView.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - diff --git a/src/components/messageView/offsetCalculator.ts b/src/components/messageView/offsetCalculator.ts deleted file mode 100644 index 01be800..0000000 --- a/src/components/messageView/offsetCalculator.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ref } from "vue"; -/** - * Calculates vertical and horizontal message offsets - */ -export default function useOffsetCalculator() { - const xOffset = ref(0); - const yOffset = ref(0); - const firstChildMarginTop = 45; - let previousMessageHeight = 0; - const messagePadding = 15; - - function calculateX(message: HTMLElement| SVGGElement): void { - const computedQuery = window.getComputedStyle(message); - const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform); - xOffset.value = matrix.m41; - } - - function calculateY(messageHeight: number): void { - if (previousMessageHeight === 0) { - yOffset.value = firstChildMarginTop; - } else { - yOffset.value += previousMessageHeight + messagePadding; - } - previousMessageHeight = messageHeight; - } - - const calculateMessageOffsets = (msg: HTMLElement): { x: number; y: number } => { - const msgRect = msg.getBoundingClientRect(); - calculateX(msg); - calculateY(msgRect.height); - const offsets = { - x: xOffset.value as number, - y: yOffset.value as number - }; - return offsets; - } - - return { - calculateMessageOffsets, - } -} diff --git a/src/components/messageView/textBubble/computed.ts b/src/components/messageView/textBubble/computed.ts deleted file mode 100644 index e26ba0f..0000000 --- a/src/components/messageView/textBubble/computed.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { computed, onMounted, ref } from "vue"; - -/** - * single message svg item computed properties - */ -export default function useComputedBubble(m: string, id: string) { - const yMargin = 0; - const messageMargin = 20; - const messageWidth = ref(0); - - - // const textFill = computed(() => { - // switch (m) { - // case "sf": - // return "#fff"; - // default: - // return "#000"; - // } - // }); - - // const bubbleFill = computed(() => { - // switch (m) { - // case "sf": - // return "#61b4f4"; - // default: - // return "#fff"; - // } - // }); - - // const setMessageWidthRef = () => { - // const msg = document.getElementById(id); - - // if (msg) { - // messageWidth.value = msg.getBoundingClientRect().width; - // } - - // } - - const calculateXoffset = (w: number) => { - let offset = messageMargin; - - // If self message, render on righthand side. - if (m == "sf") { - offset = w - messageMargin - 232 - - // Else, render on left. - } - - console.log(`${m} : ${offset}`); - return offset - } - - const calculateYoffset = (h: number) => { - - // Bubble will render a little bit below the screen. - return h + yMargin - } - - // Where the textBubble is going to initially render. - const startingOffset = computed(() => { - - // Get current dimensions of the messenger. - const parentView = document.getElementById('messagesView'); - - if (parentView) { - const dimensions = parentView.getBoundingClientRect(); - - const X = calculateXoffset(dimensions.width); - const Y = calculateYoffset(dimensions.height); - - return `translate(${X} ${Y})`; - } - - }); - - return { - startingOffset - }; -} diff --git a/src/components/messageView/textBubble/drawer.ts b/src/components/messageView/textBubble/drawer.ts deleted file mode 100644 index 9f6ee20..0000000 --- a/src/components/messageView/textBubble/drawer.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ref, onMounted } from "vue"; -import { Ref } from "@/types/vueRef/index"; - -/** - * handles visualization of single message svg items - */ -export default function useBubbleDrawer({ - mr, - tr, -}: { - mr: Ref; - tr: Ref; -}) { - const signatureTranslate = ref(""); - let messageRect: SVGSVGElement & SVGRectElement; - let messageText: SVGSVGElement & SVGTextElement; - - onMounted(async () => { - messageRect = (mr.value as unknown) as SVGSVGElement & SVGRectElement; - messageText = (tr.value as unknown) as SVGSVGElement & SVGTextElement; - }); - - function adjustMessageRect(): void { - // must get dimensions of text box to fit bubble around - const padding = 12.5; - const textBox = messageText.getBBox(); - messageRect.setAttribute("x", String(textBox.x - padding)); - messageRect.setAttribute("y", String(textBox.y - padding)); - messageRect.setAttribute("width", String(textBox.width + 2 * padding)); - messageRect.setAttribute("height", String(textBox.height + 2 * padding)); - } - - function translateMsgSignature(): void { - if (messageRect.getAttribute("height") !== null) { - const height = Number(messageRect.getAttribute("height")); - signatureTranslate.value = `translate(-10 ${height})`; - } - } - - async function drawMessage() { - // ignore lint error: await needed for proper render - await adjustMessageRect(); - translateMsgSignature(); - } - - return { - drawMessage, - signatureTranslate, - }; -} diff --git a/src/components/messageView/textBubble/textBubble.vue b/src/components/messageView/textBubble/textBubble.vue deleted file mode 100644 index fd778f9..0000000 --- a/src/components/messageView/textBubble/textBubble.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - diff --git a/src/components/messageView/transitions.ts b/src/components/messageView/transitions.ts deleted file mode 100644 index 43e3078..0000000 --- a/src/components/messageView/transitions.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Do stuff before, on, and after message enters the view. - * Rendering Process consists of the execution of the above - * mentioned callbacks followed by the shift or stack animation. - * The animation called depends on the height of the message view - * relative to the window height. Additionally, the animation will - * always be called on the after enter callback, after all the neccesary - * calculations have been completed. While all of this is hapenning, - * the rendering state is updated throughout the process. - */ - -import { VueTransitionCallback } from '@/types/vue3/vueTransitionCallback/index'; -import useMessageViewAnims from "./anime"; -import useOffsetCalculator from './offsetCalculator'; -import { ref } from 'vue'; - -const wH = 500; // window Height -const vP = 10; // view padding -const mP = 15; // message padding -let mH: number; // message Height -let vH: number; // view Height -let vB: number; // view Bottom Bound -let message: HTMLElement | null; -let messageOffset = { x: 0, y: 0 }; -let shift = false; -let beforeEnterVH: number; -let messageView: SVGGElement; - -export default function useTransitions() { - const { calculateMessageOffsets } = useOffsetCalculator(); - const { shiftAnimate, stackAnimate } = useMessageViewAnims(); - const rendering = ref(false); - - const animateMessage = async (el: SVGGElement) => { - - // Where the message gets translated to. - const elTransform = `translate(${messageOffset.x} ${messageOffset.y})`; - - const viewTransform = `translate(0 ${-vB})`; - - // Shift of view height > window height. - if (shift) { - await shiftAnimate(el, elTransform, messageView, viewTransform); - } else { - await stackAnimate(el, elTransform) - } - rendering.value = false; - } - - // const beforeEnter: VueTransitionCallback = () => { - // const parentView = document.getElementById('messagesView'); - - // if (parentView) { - // const dimensions = parentView.getBoundingClientRect(); - - // } - // } - - - - // Get height of messageView. - const beforeEnter: VueTransitionCallback = () => { - rendering.value = true; - // query DOM for messageView element - const messageViewQuery = document.querySelectorAll("#messageView"); - messageView = messageViewQuery[0] as SVGGElement; - if (messageViewQuery) { - beforeEnterVH = messageView.getBBox().height; - } - } - - const enter: VueTransitionCallback = (el) => { - // query DOM for latest message - if (el) { - message = document.getElementById(el.id); - } - } - - const afterEnter: VueTransitionCallback = (el) => { - - if (message) messageOffset = calculateMessageOffsets(message); - - if (el) { - // calculate message & view heights, as well as view bottom bound - mH = el.getBoundingClientRect().height; - vH = mH + beforeEnterVH + mP; - vB = Math.abs(vH - wH) + vP; - - shift = vH > wH ? true : false; - animateMessage(el); - } - } - - return { - beforeEnter, - enter, - afterEnter, - rendering - }; -} diff --git a/src/components/textBubble/bubbleDetails/computed.ts b/src/components/textBubble/bubbleDetails/computed.ts deleted file mode 100644 index 7c3eacf..0000000 --- a/src/components/textBubble/bubbleDetails/computed.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { computed, onMounted, ref } from "vue"; - -/** - * single message svg item computed properties - */ -export default function useComputedBubble(m: string, id: string) { - const x = ref(0) - const paddingLeft = 30; - - let winW = 350; - const messageWidth = ref(0); - const messageMarginTop = 75; - - const textFill = computed(() => { - switch (m) { - case "sf": - return "#fff"; - default: - return "#000"; - } - }); - - const bubbleFill = computed(() => { - switch (m) { - case "sf": - return "#61b4f4"; - default: - return "#fff"; - } - }); - - const updatewinW = computed(() => { - const parentView = document.getElementById('messageView'); - if (parentView) { - winW = parentView.getBoundingClientRect().width; - } - }) - - const setMessageWidthRef = async () => { - const msg = await document.getElementById(id); - if (msg) { - messageWidth.value = msg.getBoundingClientRect().width; - } - } - - const selfMessageMarginRight = 2.5; - const calculateXoffset = async () => { - switch (m) { - case "sf": - x.value = winW - messageWidth.value - selfMessageMarginRight; - break; - default: - x.value = paddingLeft; - // to center a message - // x = messageMargin + (winW -messageWidth) / 2; - } - } - - const startingOffset = computed(() => { - const parentView = document.getElementById('messageView'); - if (parentView) { - const rect = parentView.getBoundingClientRect(); - let Y = 600; - if (rect.height > 500) { - Y = rect.height + messageMarginTop; - } - return `translate(${x.value} ${Y})`; - } - }); - - onMounted(async () => { - await setMessageWidthRef(); - calculateXoffset(); - }) - - return { - textFill, - bubbleFill, - startingOffset - }; -} From d06fb395b92b6cf40abfc608c9be83e8b055663e Mon Sep 17 00:00:00 2001 From: Enrique Hernandez Date: Tue, 23 Feb 2021 14:33:58 -0600 Subject: [PATCH 002/163] add audio playback --- gcloud.json | 12 ++ src/background/audio.ts | 128 +++++++++++++----- src/background/run.ts | 4 +- src/background/session.ts | 10 +- src/components/messageView/index.vue | 111 +++++++++++++++ src/components/taInput/inputController.ts | 2 +- .../textBubble/bubbleDetails/drawer.ts | 50 +++++++ tests/audio.js | 77 +++++++++-- tests/server.js | 6 +- 9 files changed, 342 insertions(+), 58 deletions(-) create mode 100644 gcloud.json create mode 100644 src/components/messageView/index.vue create mode 100644 src/components/textBubble/bubbleDetails/drawer.ts diff --git a/gcloud.json b/gcloud.json new file mode 100644 index 0000000..5c68549 --- /dev/null +++ b/gcloud.json @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "crimata", + "private_key_id": "3a324fa3fcca4cea589e4b5007f4142db4b3a84d", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCxFJOjhIkYlyhn\nHjtpFC5EOwzNFZem6k6SS7zcgWbNLe9mcBsiTKCjpxItzkIo0pnT32yqw+AKsqpv\n6+LVyzzjfB7vtEUflvg6KgmYMS4ixOTCEbN3e2qj1t2RnITXZR2VGSGdoKKyqZm0\ntWrmexQvSN20cByL1vfxD3E69oYFGhVraa0zmm94WXHAzofT6DkvGdrbK1s1G9jI\nG3mdQ1jN71tghyloeHL+R/1iKQoZFYTkVe6yWvccSNAZelt/5sciCKRTCArXj89X\nLwU6jGOKIsyqx3BeUyWaSS59xpnGX3meSf5Q9U+j45ePUiqO6TTleF+kWiBwjUpp\nfeYcwEIDAgMBAAECggEAElkN9R7p75zV6F1XDYu0QYiWyncmx/o2Gu1zC6vywWa1\ni/korpSe/mX0ub9J1p3/p1balRUHlUQu6brYvYs56140fGTC1sOXQ7uQU+8glySs\niTk5TbOBeKluOsSrdP/6oTTB6Wm4AegVz4YOpgPxsvaLVkNZidnDxfvyIQhjGYsO\nsvRaw48eEmGOa1uvDhgvPOxiDp5YEdxqSue/7ylzjBdKecDyLgTSDDf/p+FwIx5B\n2d/0uRgQBaAk6D/DNOvN8GrYyakFmlDjE72nrBGAJq50UyV6THOSDk+8cK+2QiS1\nEDCFufj+7Dk1EBfBFUFY8ZBB/pphpEWyLoFl7NzGCQKBgQDaj/KOOcj6BkkTj1WM\nRW3KhOE3nhsCQEgQGCflpycEAZj6J7ktBG2/4teDt9sr+Epx8ymnQCZa9pmysdrD\nU0aWku1qUx0IUKTtZbAEbB+AnrWYDYkIWU8kRUUvNHNTBg43VyuohV9wprPih138\nHWMeNac82/XICz2A3rv+Xkx8KwKBgQDPaaEEGWVs3nIhf/p3M3/xWNKt8fhAERpG\n0YpTjgHW3FQFYjiCTTIcuLO3qsDGmfIAB9tL37s2Y3e2wKBGUj/o69Fr7sWVPdsX\nJtUc0+KiG2Bs0hsyZPdk9UQiQX8y2eq1NZW7vEoxWso1bM64WURB5N8ocWePNJJx\nL2Oe75rtiQKBgQDLfQQOiRxmNF3rOSMkAywyRr9NQgXRdbniSiszNQotP7OHDF7q\n29m2suOGfjIv4O6m4wdf8WkEfd4hsleETc9Ft6wVtyYrrLGxWWCk7WnzHVDjLY7s\n2AHIOjostf+9R8EKoz1BnFN8laibev71EQNMiBWZow1VX6m2hymurWs2mwKBgQCb\nuY3n2v14qOb92e1+U89KsEq1yMd/qpeU9jwqEaO14wS+aglNY5ItWEuuqWhFdE3q\n0ftHUzpnUnUOZD+xrI1JXsyEgegc7i0xi7lUBI3S8kUKTxGWW5IXXcKDCbPrxQtg\ndFPweSUnOyg4xnHKnVMPOjyGS+bZ8TnF+zOLoBAtKQKBgF6hAOIwF1DheXT4p8xS\nlSGhl8dBxjsJDVwbnFWQceGzGnq1e3ncy/UItt42UCDVRWTH4r7pQ9eKyd/3foUD\n6KDIcCrYo2KJh6ZNFJ67XF30U+TuEg5U/9jTbWqDOwYVXcMcnzQE4ZiGBxVHL6xr\nNc8FRW5+7xh4zNfzhGUGt5kt\n-----END PRIVATE KEY-----\n", + "client_email": "crimata-service-account@crimata.iam.gserviceaccount.com", + "client_id": "102914612880037864172", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/crimata-service-account%40crimata.iam.gserviceaccount.com" +} diff --git a/src/background/audio.ts b/src/background/audio.ts index 78fcff5..3263582 100644 --- a/src/background/audio.ts +++ b/src/background/audio.ts @@ -1,50 +1,104 @@ -"use strict"; /* eslint @typescript-eslint/no-var-requires: "off" */ + +"use strict"; + import { sendAudio } from './session' const portAudio = require('naudiodon'); -let ia: typeof portAudio.AudioIO; -let audioInput: string[] = []; - -function processAudio(audioInput: Array): Buffer { - let audio = ''; - audioInput.forEach(chunk => audio += chunk); - return Buffer.from(audio, "base64"); +let ai: typeof portAudio.AudioIO; +let ao: typeof portAudio.AudioIO; +let audioInput: Buffer[] = []; +const encoding = "base64"; +const audioOptions = { + channelCount: 1, + sampleFormat: 16, + sampleRate: 16000, + deviceId: -1, + closeOnError: false, } -export const updateRecorder = (_event, record: boolean): void => { - if (record) { - ia.resume(); - } else { - ia.pause(); - const audio = processAudio(audioInput); - sendAudio(audio); - audioInput = []; +// run every time the main function launches. +export const initAudioIO = () => { + // init portAudio readable stream once. + if (!ai) { + ai = new portAudio.AudioIO({ inOptions: audioOptions }); + + // base64 encoding needed for google speech to text. + ai.setEncoding(encoding); + + // pause the portAudio data flow as soon as stream is initiated. + ai.start(); + ai.pause(); + + ai.on('error', (e: Error) => { + console.log('Error recording audio', + e); + }); + ai.on('data', (chunk: string) => { + // turn string base64 data into buffer object. + audioInput.push(Buffer.from(chunk, encoding)); + }); + } + + // init portAudio writable stream once. + if (!ao) { + ao = new portAudio.AudioIO({ outOptions: audioOptions }); + ao.start(); } } -export const initRecorder = () => { - - if (!ia) { - ia = new portAudio.AudioIO({ - inOptions: { - channelCount: 1, - sampleFormat: 16, - sampleRate: 16000, - deviceId: -1, - closeOnError: false, - } - }); - ia.setEncoding('base64'); - ia.start(); - ia.pause(); - ia.on('error', (e: Error) => { - console.log('Error recording audio', + e); - }); - ia.on('data', (chunk: string) => { - audioInput.push(chunk); - }); +// run this function on space bar key up and down. +export const updateRecorder = (_event, record: boolean): void => { + if (record) { + // resume mic data flow on space bar key down. + ai.resume(); + } else { + // stop mic data flow on space bar key up. + ai.pause(); + sendAudio(Buffer.concat(audioInput)); + audioInput = []; } } +// play audio buffers. +export const play = (input: Array) => { + let i = 0; + // format buffers into half their size to account for writable highwaterMark. + const audio = bufSplit(input); + // call this fuction after last audio chunk has been written. + const callback = () => { + // TODO: clear portAudio writable buffer on write end. + } + write(); + // iterate through audio array and write buffers to portAudio writable. + function write() { + let chunk: Buffer; + let ok = true; + do { + chunk = audio[i]; + if (i === audio.length - 1) { + // write last chunk. + ao.write(chunk, null, callback); + } else { + // check for backpreassure. + ok = ao.write(chunk, null); + } + i++; + } while (i < audio.length && ok); + if (i < audio.length) { + // Had to stop early! + // Write some more once it drains. + ao.once('drain', write); + } + } +} + +// utility function used by play func +function bufSplit(input: Array): Array { + let result: Buffer[] = []; + input.forEach((b: Buffer) => { + // split buffer into two. + result.push(b.slice(0, b.length / 2), b.slice(b.length / 2, b.length)); + }); + return result; +} diff --git a/src/background/run.ts b/src/background/run.ts index d0cfde1..a92e941 100644 --- a/src/background/run.ts +++ b/src/background/run.ts @@ -2,7 +2,7 @@ import { createWindow } from './window'; import { initSession } from './session'; -import { initRecorder } from './audio'; +import { initAudioIO } from './audio'; let socket = false; @@ -23,6 +23,6 @@ export async function main() { socket = true; // Begin audio stream. - // initRecorder(); + initAudioIO(); } diff --git a/src/background/session.ts b/src/background/session.ts index 71e91ee..0b4a741 100644 --- a/src/background/session.ts +++ b/src/background/session.ts @@ -5,7 +5,10 @@ import { backgroundMitt } from './emitter'; import { ipcMain } from "electron"; interface RenderMessage { - content: string; + content: { + text: string; + audio: boolean | Buffer; + }; avatar: string; context: string; subContext: string; @@ -24,7 +27,7 @@ interface TextMessage { content: string; } -const ip = 'ws://127.0.0.1'; +const ip = 'ws://127.0. 0.1'; const port = 8760; const reconnectTimeout = 3000; //ms let socket: WebSocket; @@ -67,6 +70,9 @@ const receiveMessage = (message: string): void => { } const parsed: RenderMessage = JSON.parse(message); + console.log('message received', parsed); + if (parsed.content.audio) { + } backgroundMitt.emit('ipc-renderer', { endpoint: 'render-message', message: parsed diff --git a/src/components/messageView/index.vue b/src/components/messageView/index.vue new file mode 100644 index 0000000..e231033 --- /dev/null +++ b/src/components/messageView/index.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/src/components/taInput/inputController.ts b/src/components/taInput/inputController.ts index facbe1f..1f8df0b 100644 --- a/src/components/taInput/inputController.ts +++ b/src/components/taInput/inputController.ts @@ -34,7 +34,7 @@ export default function useInputController() { emitter.emit("animate-send"); // send message to backend - post('message', message); + if (input) post('message', message); } const { diff --git a/src/components/textBubble/bubbleDetails/drawer.ts b/src/components/textBubble/bubbleDetails/drawer.ts new file mode 100644 index 0000000..8343fb6 --- /dev/null +++ b/src/components/textBubble/bubbleDetails/drawer.ts @@ -0,0 +1,50 @@ +import { ref, onMounted } from "vue"; +import { Ref } from "@/types/vueRef/index"; + +/** + * handles visualization of individual svg text bubbles + */ +export default function useBubbleDrawer({ + mr, + tr, +}: { + mr: Ref; + tr: Ref; +}) { + const signatureTranslate = ref(""); + let messageRect: SVGSVGElement & SVGRectElement; + let messageText: SVGSVGElement & SVGTextElement; + + onMounted(async () => { + messageRect = (mr.value as unknown) as SVGSVGElement & SVGRectElement; + messageText = (tr.value as unknown) as SVGSVGElement & SVGTextElement; + }); + + function adjustMessageRect(): void { + // must get dimensions of text box to fit bubble around + const padding = 12.5; + const textBox = messageText.getBBox(); + messageRect.setAttribute("x", String(textBox.x - padding)); + messageRect.setAttribute("y", String(textBox.y - padding)); + messageRect.setAttribute("width", String(textBox.width + 2 * padding)); + messageRect.setAttribute("height", String(textBox.height + 2 * padding)); + } + + function translateMsgSignature(): void { + if (messageRect.getAttribute("height") !== null) { + const height = Number(messageRect.getAttribute("height")); + signatureTranslate.value = `translate(-10 ${height})`; + } + } + + async function drawMessage() { + // ignore lint error: await needed for proper render + await adjustMessageRect(); + translateMsgSignature(); + } + + return { + drawMessage, + signatureTranslate, + }; +} diff --git a/tests/audio.js b/tests/audio.js index 070960d..8fde8eb 100644 --- a/tests/audio.js +++ b/tests/audio.js @@ -50,43 +50,90 @@ const ia = new portAudio.AudioIO({ sampleRate: 16000, deviceId: -1, closeOnError: false, - } + }, }); -ia.setEncoding('base64'); +// ia.setEncoding('base64'); ia.start(); ia.on('error', (e) => { console.log('error recording audio', + e); }); ia.on('data', (chunk) => { - audioInput.push(chunk); - console.log('Got %d characters of string data:', chunk.length); + audioInput.push(chunk); }); +const ao = new portAudio.AudioIO({ + outOptions: { + channelCount: 1, + sampleFormat: portAudio.SampleFormat16Bit, + sampleRate: 16000, + deviceId: -1, // Use -1 or omit the deviceId to select the default device + closeOnError: true // Close the stream if an audio error is detected, if set false then just log the error + } +}); + +ao.start(); + +const callback = () => { + console.log('yayayay') +} + +const play = (audio) => { + + console.log('audio length', audio.length); + let i = 0; + write(); + + function write() { + let ok = true; + do { + const chunk = audio[i] + console.log(chunk, i) + + i++; + if (i === audio.length - 1) { + // Last time! + ao.write(chunk, null, callback); + } else { + // See if we should continue, or wait. + // Don't pass the callback, because we're not done yet. + ok = ao.write(chunk, null); + } + } while (i < audio.length - 1 && ok); + if (i < audio.length - 1) { + // Had to stop early! + // Write some more once it drains. + ao.on('drain', write); + } + } + +} + async function processAudio() { - audio = ''; - audioInput.forEach(s => { - audio += s; - }) - const buffer = Buffer.from(audio, 'base64'); + + const buf = Buffer.concat(audioInput); + + play(audioInput); + audioInput = []; - await transcribeSpeech({ - content: buffer - }); + + transcribeSpeech({ + content: buf + }); } setTimeout(async () => { ia.pause(); - await processAudio(); + processAudio(); // ia.resume(); }, 3000); setTimeout(() => { ia.resume(); -}, 4000) +}, 5000) setTimeout(async () => { ia.pause(); - await processAudio(); + processAudio(); // ia.resume(); }, 8000); diff --git a/tests/server.js b/tests/server.js index 01358e8..0440047 100644 --- a/tests/server.js +++ b/tests/server.js @@ -9,8 +9,12 @@ wss.on("connection", function connection(ws, req) { console.log(message) if (message === 'no-token' || message === 'undefined') { - console.log('no token to authenticate'); ws.send('success'); + } else if (message instanceof Buffer) { + // audio message + + } else { + const parsed = JSON.parse(message); } }); From 7391f6f85d26a62e642ee9c58c3721a2d742f3e4 Mon Sep 17 00:00:00 2001 From: riqo Date: Sat, 27 Feb 2021 12:19:30 -0600 Subject: [PATCH 003/163] invalid credentials error on login.vue --- src/background/session.ts | 2 +- src/views/login.vue | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/background/session.ts b/src/background/session.ts index 0b4a741..88d1586 100644 --- a/src/background/session.ts +++ b/src/background/session.ts @@ -27,7 +27,7 @@ interface TextMessage { content: string; } -const ip = 'ws://127.0. 0.1'; +const ip = 'ws://127.0.0.1'; const port = 8760; const reconnectTimeout = 3000; //ms let socket: WebSocket; diff --git a/src/views/login.vue b/src/views/login.vue index e098c01..fe49927 100644 --- a/src/views/login.vue +++ b/src/views/login.vue @@ -1,6 +1,7 @@ + + From 346bfcba019701739d0aa0ab9988aea693387124 Mon Sep 17 00:00:00 2001 From: riqo Date: Sun, 28 Feb 2021 14:27:51 -0600 Subject: [PATCH 007/163] add splash screen on load --- src/App.vue | 29 +++--- src/background/audio.ts | 7 +- src/background/window.ts | 14 ++- src/components/messageItem.vue | 140 +++++++++++++++++++++++++ src/{views => components}/splash.vue | 52 +++++----- src/modules/auth.ts | 7 +- src/router.ts | 9 -- src/views/messenger.vue | 150 +++------------------------ tests/audio.js | 55 +--------- 9 files changed, 219 insertions(+), 244 deletions(-) create mode 100644 src/components/messageItem.vue rename src/{views => components}/splash.vue (53%) diff --git a/src/App.vue b/src/App.vue index 583ff1b..17e28a1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,5 @@ diff --git a/src/background/audio.ts b/src/background/audio.ts index 3263582..4424d44 100644 --- a/src/background/audio.ts +++ b/src/background/audio.ts @@ -35,7 +35,9 @@ export const initAudioIO = () => { }); ai.on('data', (chunk: string) => { // turn string base64 data into buffer object. - audioInput.push(Buffer.from(chunk, encoding)); + console.log('received string chunk of length', chunk.length) + const buf = Buffer.from(chunk, encoding); + audioInput.push(buf); }); } @@ -54,7 +56,8 @@ export const updateRecorder = (_event, record: boolean): void => { } else { // stop mic data flow on space bar key up. ai.pause(); - sendAudio(Buffer.concat(audioInput)); + const audio = Buffer.concat(audioInput); + sendAudio(audio); audioInput = []; } } diff --git a/src/background/window.ts b/src/background/window.ts index 334ae67..b84a742 100644 --- a/src/background/window.ts +++ b/src/background/window.ts @@ -30,10 +30,11 @@ let win: BrowserWindow | null; const windowMount = (): void => { backgroundMitt.emit('window-active', true); + ipcMain.removeAllListeners('update-recorder'); ipcMain.on('update-recorder', updateRecorder); - // handle renderer auth-token event - ipcMain.removeHandler('nav-bar'); // avoid setting duplicate handlers - ipcMain.handle('nav-bar', navBarHandler); + // handle renderer auth-token event + ipcMain.removeHandler('nav-bar'); // avoid setting duplicate handlers + ipcMain.handle('nav-bar', navBarHandler); } const windowDismount = (): void => { @@ -58,6 +59,7 @@ export const createWindow = async (options: WindowSettings): Promise => { width: options.width, height: options.height, resizable: options.resizable, + backgroundColor: '#EBEBEB', frame: false, minWidth: 350, minHeight: 500, @@ -82,8 +84,12 @@ export const createWindow = async (options: WindowSettings): Promise => { // Handle window close. win.on("closed", windowDismount); + win.once('ready-to-show', () => { + if (win) win.show() + }) + win.webContents.on('did-finish-load', () => { - windowMount(); + windowMount(); resolve(); }); diff --git a/src/components/messageItem.vue b/src/components/messageItem.vue new file mode 100644 index 0000000..d60795e --- /dev/null +++ b/src/components/messageItem.vue @@ -0,0 +1,140 @@ + + + + + \ No newline at end of file diff --git a/src/views/splash.vue b/src/components/splash.vue similarity index 53% rename from src/views/splash.vue rename to src/components/splash.vue index d30173d..247847a 100644 --- a/src/views/splash.vue +++ b/src/components/splash.vue @@ -1,57 +1,55 @@ @@ -68,4 +66,4 @@ export default defineComponent({ } - \ No newline at end of file + diff --git a/src/modules/auth.ts b/src/modules/auth.ts index 6a1120e..c541c3f 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -1,4 +1,4 @@ -import { reactive, toRefs } from 'vue'; +import { reactive, toRefs, ref } from 'vue'; import { useIpc } from './ipc'; interface AuthState { @@ -11,6 +11,8 @@ const state = reactive({ error: undefined, }); +const loading = ref(true); + const AUTH_KEY = 'crimata_token'; const token = window.localStorage.getItem(AUTH_KEY); @@ -20,6 +22,7 @@ if (token) { } const authToken = async () => { + loading.value = true; const { invoke } = useIpc(); try { const res = await invoke('auth-session', token); @@ -32,6 +35,7 @@ const authToken = async () => { window.localStorage.removeItem(AUTH_KEY); state.accessToken = null; } + loading.value = false; } // authenticate on auth event @@ -56,5 +60,6 @@ export const useAuth = () => { setToken, logout, ...toRefs(state), // accessToken, error + loading } } diff --git a/src/router.ts b/src/router.ts index d3459dc..ee94a61 100644 --- a/src/router.ts +++ b/src/router.ts @@ -5,7 +5,6 @@ import { RouteRecordRaw } from "vue-router"; import Home from "@/views/messenger.vue"; -import Splash from "@/views/splash.vue"; import { useAuth } from '@/modules/auth'; // Define the routes (/*) for the app here. @@ -16,14 +15,6 @@ const routes: Array = [ component: Home, meta: { requiresAuth: true }, }, - { - path: "/splash", - name: "splash", - component: Splash, - meta: { - requiresAuth: false - }, - }, { path: "/login", name: "login", diff --git a/src/views/messenger.vue b/src/views/messenger.vue index 58f0094..25ebd75 100644 --- a/src/views/messenger.vue +++ b/src/views/messenger.vue @@ -3,74 +3,37 @@ -
- - -
- - -
- - -
- {{ message.content }} -
- - - - - -
- -
- -
- {{ message.avatar }} -
- - {{ message.context }} - -
- -
- -
- +
+
- + + + \ No newline at end of file diff --git a/src/components/messageItem.vue b/src/components/messageItem.vue index d60795e..6197c94 100644 --- a/src/components/messageItem.vue +++ b/src/components/messageItem.vue @@ -5,6 +5,9 @@ :key="message.id" > + + +
@@ -13,19 +16,20 @@ class="bubble" :id="`${message.modifier}Bubble`" > - {{ message.content }} + {{ message.content.text }}
- -
+ +
-
- {{ message.avatar }} + +
+ {{ message.context[0] }}
{{ message.context }} @@ -79,12 +83,28 @@ align-items: flex-end; } + #frMessage { + align-items: flex-start; + } + + .messageBox { + display: flex; + flex-direction: column; + } + #aiMessageBox { margin-left: 20px; + align-items: flex-start; } #sfMessageBox { margin-right: 20px; + align-items: flex-end; + } + + #frMessageBox { + margin-left: 20px; + align-items: flex-start; } .bubble { @@ -110,6 +130,10 @@ color: white; } + #frBubble { + background-color: white; + } + .context { display: flex; align-items: center; @@ -117,7 +141,6 @@ font-family: "SF Compact Display"; font-size: 12px; font-weight: bold; - margin-left: 7px; margin-top: 5px; } diff --git a/src/components/resize.ts b/src/components/resize.ts new file mode 100644 index 0000000..4a28203 --- /dev/null +++ b/src/components/resize.ts @@ -0,0 +1,7 @@ + +// Update inputItem position on window resize. +// Gets called on window.resize event. +function handleWindowResize (e: any) { + +} + diff --git a/src/test.vue b/src/test.vue new file mode 100644 index 0000000..5651d69 --- /dev/null +++ b/src/test.vue @@ -0,0 +1,66 @@ + // Snap to the nearest window border and update percentages. Called when + // user stops dragging the inputItem. + const mouseUp = (e: any) => { + window.removeEventListener('mousemove', divMove, true); + + const winW = window.innerWidth + const winH = window.innerHeight + console.log(`Window dimensions: W${winW} H${winH}`) + + // First, are we changing inputItemX or inputItemY? + + let xShort = winW - inputItemX.value + if (xShort > winW / 2) { + xShort = inputItemX.value + } + + let yShort = winH - inputItemY.value + if (yShort > winH / 2) { + yShort = inputItemY.value + } + + const shortest = (xShort < yShort) ? "inputItemX" : "inputItemY" + + // Then, are we tranlating it positive or negative? + // To answer this, we see which side is it closer to. + // We also update percentages to aid in window resizing (handleResize). + + if (shortest == "inputItemX") { + if (inputItemX.value < winW / 2) { + inputItemX.value = 0 + percentX = 0.0 + } else { + inputItemX.value = winW - 38 + percentX = 1.0 + } + percentY = inputItemY.value / winH + } else { + if (inputItemY.value < winH / 2) { + inputItemY.value = 0 + percentY = 0.0 + } else { + inputItemY.value = winH - 38 + percentY = 1.0 + } + percentX = inputItemX.value / winW + + } + console.log(`Percents updated: iX: ${percentX} iY: ${percentY}`) + } + + + + + if (percentX == 1.0) { + inputItemX.value = window.innerWidth * percentX - 38 + } else { + inputItemX.value = window.innerWidth * percentX + } + + if (percentY == 1.0) { + inputItemY.value = window.innerHeight * percentY - 38 + } else { + inputItemY.value = window.innerHeight * percentY + } + + } \ No newline at end of file diff --git a/src/types/message/index.ts b/src/types/message/index.ts index 1997303..5163d68 100644 --- a/src/types/message/index.ts +++ b/src/types/message/index.ts @@ -1,11 +1,11 @@ export interface Message { - content: string; - avatar: string; + content: { + text: string; + audio: boolean | Buffer; + }; context: string; - subContext: string; modifier: string; id: string; - time: string; } interface MessageTransform { diff --git a/src/views/messenger.vue b/src/views/messenger.vue index 25ebd75..91ccc8c 100644 --- a/src/views/messenger.vue +++ b/src/views/messenger.vue @@ -1,6 +1,7 @@ @@ -34,35 +35,30 @@ export default defineComponent({ components: { Splash }, setup() { - const { invoke } = useIpc(); + + // Show spash screen til true. const ready = ref(false); - const { saveMessages } = useMessages(); + + const { invoke } = useIpc(); + + // Post navbar action to backend. + const callNavbar = (action: string) => { + console.log("Calling navbar") + invoke('nav-bar', action); + } + + const onWindowReady = (_event: any, payload: any) => { + ready.value = true; + } onMounted(() => { - window.ipcRenderer.on('window-ready', (_event, payload: {message: boolean}) => { - ready.value = payload.message; - }) + window.ipcRenderer.on("window-ready", onWindowReady); }); onUnmounted(() => { - window.ipcRenderer.removeAllListeners('window-ready') + window.ipcRenderer.removeAllListeners("window-ready") }) - const saveWindowState = async () => { - await invoke('window-save', ""); - } - - const callNavbar = async (action: string) => { - - // save messages before closing. - if (action === 'close') { - saveMessages(); - await saveWindowState(); - } - - invoke('nav-bar', action); - } - return { callNavbar, ready @@ -77,7 +73,7 @@ export default defineComponent({ html, body { margin: 0; padding: 0; - background-color: #EBEBEB; + // Background color set in window.ts } #app { diff --git a/src/background/init.ts b/src/background/init.ts index 0f60068..cb90050 100644 --- a/src/background/init.ts +++ b/src/background/init.ts @@ -9,7 +9,7 @@ import { stopStream } from './audio'; let winActive: boolean; backgroundMitt.on('window-active', (state: boolean) => { - winActive = state; + winActive = state; }); export function initApp(dev: boolean): void { diff --git a/src/background/session.ts b/src/background/session.ts index 27a5546..766df03 100644 --- a/src/background/session.ts +++ b/src/background/session.ts @@ -59,7 +59,6 @@ const handleAuthMessage = (message: string) => { } const handleProfileMessage = (message: Profile) => { - console.log("Updating profile...") backgroundMitt.emit('ipc-renderer', { endpoint: 'update-profile', message: message @@ -86,7 +85,6 @@ const handleStandardMessage = (m: StandardMessage) => { } const handleAnnotationMessage = (message: Annotation) => { - console.log("Annotation message") backgroundMitt.emit('ipc-renderer', { endpoint: 'annotate-message', message: message diff --git a/src/background/window.ts b/src/background/window.ts index d46da18..a34427b 100644 --- a/src/background/window.ts +++ b/src/background/window.ts @@ -14,16 +14,17 @@ interface IpcRendererPayload { let win: BrowserWindow | null; -// Util function to handle window close & minimize. -const navBarHandler = (_event, action: string): void => { - switch(action) { - case 'close': - if (win) win.close(); - break; - case 'min': - if (win) win.minimize(); - break; +// Called when a NavBar button is pressed. +const onNavBar = (_event: any, action: string): void => { + console.log("onNavBar") + if (win) { + if (action === "close") { + saveWindowState() + win.close() + } else { + win.minimize() } + } } // Util function to render message on ipc-renderer event. @@ -35,34 +36,42 @@ const renderMessage = (payload: IpcRendererPayload): void => { } } -const onWindowSave = (_event, _s: string) => { +// Write a json with position and size of window. +const saveWindowState = () => { if (win) { const bounds = win.getBounds(); - const state = JSON.stringify({w: bounds.width, h: bounds.height}); + const position = win.getPosition(); - fs.writeFile('windowstate.json', state, (err: Error) => { + const state = JSON.stringify( + { + w: bounds.width, + h: bounds.height, + x: position[0], + y: position[1] + } + ); + + fs.writeFile('windowState.json', state, (err) => { if (err) throw err; return; }); } - } // Do this on window mount. -const windowMount = (): void => { +const onWindowMount = (): void => { backgroundMitt.emit('window-active', true); + // handle win nav-bar event. ipcMain.removeHandler('nav-bar'); // avoid setting duplicate handlers - ipcMain.handle('nav-bar', navBarHandler); - - ipcMain.removeHandler('window-save'); // avoid setting duplicate handlers - ipcMain.handle('window-save', onWindowSave); - // render messages through ipc-renderer. + ipcMain.handle('nav-bar', onNavBar); + + // Gateway for messages to the frontend. backgroundMitt.on('ipc-renderer', renderMessage); } -// do this on window dismount. -const windowDismount = (): void => { +// Do this on window dismount (close). +const onWindowDismount = (): void => { win = null; backgroundMitt.emit('window-active', false); } @@ -74,49 +83,48 @@ export async function createWindow(): Promise { // avoid creating duplicate windows. if (win) resolve(); - // read windowState from json. - const rawData = fs.readFileSync('windowState.json'); - const windowState = JSON.parse(rawData); + // Load the saved window state. + const state = JSON.parse(fs.readFileSync('windowState.json').toString()); + // Define the browser window. win = new BrowserWindow({ - width: windowState.w, - height: windowState.h, + width: state.w, + height: state.h, + x: state.x, + y: state.y, resizable: true, backgroundColor: '#EBEBEB', frame: false, minWidth: 350, minHeight: 500, webPreferences: { - // Use pluginOptions.nodeIntegration, leave this alone - // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info - nodeIntegration: (process.env - .ELECTRON_NODE_INTEGRATION as unknown) as boolean, - preload: path.join(__dirname, "preload.js") + nodeIntegration: (process.env.ELECTRON_NODE_INTEGRATION as unknown) as boolean, preload: path.join(__dirname, "preload.js") } }); - + // Load the URL. if (process.env.WEBPACK_DEV_SERVER_URL) { - // Load the url of the dev server if in development mode - win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string); - } else { + win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string); // dev + } + + else { createProtocol("app"); - // Load the index.html when not in development - win.loadURL("app://./index.html"); + win.loadURL("app://./index.html"); // prod } // Handle window close. - win.on("closed", windowDismount); + win.on("closed", onWindowDismount); win.once('ready-to-show', () => { if (win) win.show() }) win.webContents.on('did-finish-load', () => { - if (win) win.webContents.send('window-ready', { - message: true - }); - windowMount(); + if (win) win.webContents.send('window-ready', { + message: true + }); + + onWindowMount(); resolve(); }); diff --git a/src/components/inputItem/inputItem.vue b/src/components/inputItem/inputItem.vue index 8ea6a5d..91ef863 100644 --- a/src/components/inputItem/inputItem.vue +++ b/src/components/inputItem/inputItem.vue @@ -41,11 +41,23 @@ export default defineComponent({ // Mark input item with user's initials. const initials = ref("") - initials.value = "IN"; - // Initial position. - const xStart = 15; - const yStart = window.innerHeight - 200; + // ---Set xStart and yStart------------------------------------ + const cordsStr = window.localStorage.getItem("input_cords") + + let cords = {x: 15, y: window.innerHeight - 200} // default values + + if (cordsStr) { + console.log("found archived cords!") + cords = JSON.parse(cordsStr) + } + + const xStart = cords.x; + const yStart = cords.y; + + console.log(`xStart: ${xStart}`) + console.log(`yStart: ${yStart}`) + // ------------------------------------------------------------ // Calculate position of inputItem on drag. const { elementX, elementY } = draggify("inputItem", xStart, yStart, 15); @@ -63,6 +75,17 @@ export default defineComponent({ window.ipcRenderer.on("update-profile", onUpdateProfile); }) + const savePosition = () => { + const cords = JSON.stringify( + { + x: elementX.value, + y: elementY.value + } + ) + window.localStorage.setItem("input_cords", cords) + console.log(`Start saved: ${cords}`) + } + onUnmounted(() => { window.ipcRenderer.removeAllListeners("update-profile"); }) diff --git a/src/views/messenger.vue b/src/views/messenger.vue index 9be42ef..5c28758 100644 --- a/src/views/messenger.vue +++ b/src/views/messenger.vue @@ -32,7 +32,12 @@ export default defineComponent({ const { emitter } = useMitt(); // Handle messages in view. - const { messages, addMessage, updateMessage } = useMessages(); + const { + messages, + addMessage, + updateMessage, + saveMessages + } = useMessages(); // Alter existing message. const onAnnotateMessage = (_event: any, payload: any) => { diff --git a/windowState.json b/windowState.json index d637d9e..9957688 100644 --- a/windowState.json +++ b/windowState.json @@ -1 +1 @@ -{"w":352,"h":500} \ No newline at end of file +{"w":688,"h":536,"x":1478,"y":513} \ No newline at end of file From fe44d23453fb41b032d97eed04110b8e0a7ddbb8 Mon Sep 17 00:00:00 2001 From: Andrew Gundersen Date: Sat, 20 Mar 2021 10:58:22 -0500 Subject: [PATCH 073/163] save states on ctrl W --- src/App.vue | 4 +--- src/components/inputItem/inputItem.vue | 30 +++----------------------- src/modules/draggify.ts | 22 +++++++++++++++---- src/modules/messages.ts | 15 +++++++------ windowState.json | 2 +- 5 files changed, 31 insertions(+), 42 deletions(-) diff --git a/src/App.vue b/src/App.vue index 308977c..116efb1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -29,7 +29,6 @@ import { defineComponent, onMounted, onUnmounted, ref } from 'vue'; import { useIpc } from "@/modules/ipc"; import Splash from "@/components/splash.vue" -import useMessages from '@/modules/messages'; export default defineComponent({ components: { Splash }, @@ -43,11 +42,10 @@ export default defineComponent({ // Post navbar action to backend. const callNavbar = (action: string) => { - console.log("Calling navbar") invoke('nav-bar', action); } - const onWindowReady = (_event: any, payload: any) => { + const onWindowReady = (_event: any, _payload: any) => { ready.value = true; } diff --git a/src/components/inputItem/inputItem.vue b/src/components/inputItem/inputItem.vue index 91ef863..4dc3409 100644 --- a/src/components/inputItem/inputItem.vue +++ b/src/components/inputItem/inputItem.vue @@ -42,22 +42,9 @@ export default defineComponent({ // Mark input item with user's initials. const initials = ref("") - // ---Set xStart and yStart------------------------------------ - const cordsStr = window.localStorage.getItem("input_cords") - - let cords = {x: 15, y: window.innerHeight - 200} // default values - - if (cordsStr) { - console.log("found archived cords!") - cords = JSON.parse(cordsStr) - } - - const xStart = cords.x; - const yStart = cords.y; - - console.log(`xStart: ${xStart}`) - console.log(`yStart: ${yStart}`) - // ------------------------------------------------------------ + // Default values for position. + const xStart = 15; + const yStart = window.innerHeight - 200; // Calculate position of inputItem on drag. const { elementX, elementY } = draggify("inputItem", xStart, yStart, 15); @@ -75,17 +62,6 @@ export default defineComponent({ window.ipcRenderer.on("update-profile", onUpdateProfile); }) - const savePosition = () => { - const cords = JSON.stringify( - { - x: elementX.value, - y: elementY.value - } - ) - window.localStorage.setItem("input_cords", cords) - console.log(`Start saved: ${cords}`) - } - onUnmounted(() => { window.ipcRenderer.removeAllListeners("update-profile"); }) diff --git a/src/modules/draggify.ts b/src/modules/draggify.ts index 5e400ab..f3d8978 100644 --- a/src/modules/draggify.ts +++ b/src/modules/draggify.ts @@ -69,7 +69,6 @@ export default function draggify(elementId: string, xStart: number, }, 16) // 60fps } - } //---Event Handlers----------------------------------------------- @@ -130,6 +129,12 @@ export default function draggify(elementId: string, xStart: number, percentX = elementX.value / winW; percentY = elementY.value / winH; + // Save position. + const position = { + x: elementX.value, + y: elementY.value + } + window.localStorage.setItem("inputItem_position", JSON.stringify(position)); } // Update position of targetEl on windowResize. @@ -166,9 +171,18 @@ export default function draggify(elementId: string, xStart: number, window.addEventListener('resize', onWindowResize, false); }); - // Initialize the coordinants. - elementX.value = xStart; - elementY.value = yStart; + // Try loading initPosition, otherwise set default values + let initPosition: any; + const rawData = window.localStorage.getItem("inputItem_position") + + if (rawData) { + initPosition = JSON.parse(rawData) + } else { + initPosition = {x: xStart, y: yStart} + } + + elementX.value = initPosition.x; + elementY.value = initPosition.y; // remove event listeners on component dismount. onUnmounted(() => { diff --git a/src/modules/messages.ts b/src/modules/messages.ts index c7f6144..52953e5 100644 --- a/src/modules/messages.ts +++ b/src/modules/messages.ts @@ -72,6 +72,12 @@ export default function useMessages() { // Control the scrolling. const { updateScrollRef, adjustScroll } = useScroll("messenger"); + // Save message to local storage. + const saveMessages = () => { + const messageData = Object.fromEntries(messages.value); + window.localStorage.setItem(messagesKey, JSON.stringify(messageData)); + } + // Add a new message to the view. const addMessage = (message: RenderMessage) => { updateScrollRef() @@ -84,6 +90,7 @@ export default function useMessages() { setTimeout(adjustScroll, 20); updateGrouping(message.uid) + saveMessages() } // Update an existing message in the view. @@ -96,13 +103,7 @@ export default function useMessages() { message.content.text = a.text setTimeout(adjustScroll, 20); - } - - // Used in App.vue on shutdown. - const saveMessages = () => { - // de-reactivate message data before saving. - const messageData = Object.fromEntries(messages.value); - window.localStorage.setItem(messagesKey, JSON.stringify(messageData)); + saveMessages() } return { diff --git a/windowState.json b/windowState.json index 9957688..7289e3d 100644 --- a/windowState.json +++ b/windowState.json @@ -1 +1 @@ -{"w":688,"h":536,"x":1478,"y":513} \ No newline at end of file +{"w":432,"h":702,"x":1753,"y":477} \ No newline at end of file From e7fd5a0bfab1a283cb9ecd660196d717469fb9e4 Mon Sep 17 00:00:00 2001 From: Andrew Gundersen Date: Sat, 20 Mar 2021 12:33:55 -0500 Subject: [PATCH 074/163] save window stats on all close types --- src/background/window.ts | 9 ++++-- src/modules/messages.ts | 59 ++++++++++++++++++++++------------------ src/modules/scroll.ts | 15 +++++++++- src/views/messenger.vue | 1 - windowState.json | 2 +- 5 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/background/window.ts b/src/background/window.ts index a34427b..f302746 100644 --- a/src/background/window.ts +++ b/src/background/window.ts @@ -19,7 +19,6 @@ const onNavBar = (_event: any, action: string): void => { console.log("onNavBar") if (win) { if (action === "close") { - saveWindowState() win.close() } else { win.minimize() @@ -54,7 +53,7 @@ const saveWindowState = () => { fs.writeFile('windowState.json', state, (err) => { if (err) throw err; return; - }); + }); } } @@ -62,7 +61,7 @@ const saveWindowState = () => { const onWindowMount = (): void => { backgroundMitt.emit('window-active', true); - // handle win nav-bar event. + // Handle win nav-bar event. ipcMain.removeHandler('nav-bar'); // avoid setting duplicate handlers ipcMain.handle('nav-bar', onNavBar); @@ -115,6 +114,10 @@ export async function createWindow(): Promise { // Handle window close. win.on("closed", onWindowDismount); + // Save window state on resize and move. + win.on("moved", saveWindowState); + win.on("resize", saveWindowState); + win.once('ready-to-show', () => { if (win) win.show() }) diff --git a/src/modules/messages.ts b/src/modules/messages.ts index 52953e5..c6e3a94 100644 --- a/src/modules/messages.ts +++ b/src/modules/messages.ts @@ -2,27 +2,7 @@ import { ref } from 'vue'; import useScroll from "@/modules/scroll"; import { RenderMessage, Annotation } from "@/types/message/index"; -const messagesKey = "CRIMATA_MESSAGES"; -const sizeLimit = 200; - -// list of messages where the first item is the oldest message. -const messages = ref(new Map()); - -// Get stored messages. -const messagesString = window.localStorage.getItem(messagesKey); - -if (messagesString) { - // parse stored messages. - const parsed = JSON.parse(messagesString); - // set messages. - messages.value = new Map(Object.entries(parsed)); -} - -// Remove oldest message from the messages map. -function deleteOldest() { - const lastKey = Array.from(messages.value.keys()).shift(); - messages.value.delete(lastKey); -} +const messages = ref(new Map()); // ze messages. //---Message Grouping----------------------------------------------- @@ -70,20 +50,42 @@ const updateGrouping = (currentMessageRef: string) => { export default function useMessages() { // Control the scrolling. - const { updateScrollRef, adjustScroll } = useScroll("messenger"); + const { + setScroll, + updateScrollRef, + adjustScroll + } = useScroll("messenger"); // Save message to local storage. const saveMessages = () => { const messageData = Object.fromEntries(messages.value); - window.localStorage.setItem(messagesKey, JSON.stringify(messageData)); + window.localStorage.setItem("crimata_messages", JSON.stringify(messageData)); + } + + // Load messages from local storage. + const loadMessages = () => { + const rawData = window.localStorage.getItem("crimata_messages"); + + if (rawData) { + const messageData = JSON.parse(rawData) + messages.value = new Map(Object.entries(messageData)); + } + + } + + // Remove oldest message from the messages map. + function popMessage() { + const lastKey = Array.from(messages.value.keys()).shift(); + messages.value.delete(lastKey); } // Add a new message to the view. const addMessage = (message: RenderMessage) => { updateScrollRef() - if (messages.value.size >= sizeLimit) { - deleteOldest(); + // Size limit of 200. + if (messages.value.size >= 200) { + popMessage(); } messages.value.set(message.uid, message); @@ -106,10 +108,15 @@ export default function useMessages() { saveMessages() } + loadMessages() + setTimeout(setScroll, 1000); + console.log("loaded messages") + return { messages, saveMessages, addMessage, - updateMessage + updateMessage, + loadMessages } } \ No newline at end of file diff --git a/src/modules/scroll.ts b/src/modules/scroll.ts index 800d3b0..e9e6501 100644 --- a/src/modules/scroll.ts +++ b/src/modules/scroll.ts @@ -4,6 +4,18 @@ export default function useScroll(element: string) { let isScrolledToBottom: boolean; + // Set the initial scroll position. + const setScroll = () => { + const view = document.getElementById(element) + + if (view) { + view.scrollTo({ + top: view.scrollHeight - view.clientHeight, + behavior: 'smooth' + }); + } + } + // Update isScrolledToBottom const updateScrollRef = () => { const view = document.getElementById(element) @@ -31,7 +43,8 @@ export default function useScroll(element: string) { return { updateScrollRef, - adjustScroll + adjustScroll, + setScroll, } } diff --git a/src/views/messenger.vue b/src/views/messenger.vue index 5c28758..5b7d515 100644 --- a/src/views/messenger.vue +++ b/src/views/messenger.vue @@ -36,7 +36,6 @@ export default defineComponent({ messages, addMessage, updateMessage, - saveMessages } = useMessages(); // Alter existing message. diff --git a/windowState.json b/windowState.json index 7289e3d..8e8aa57 100644 --- a/windowState.json +++ b/windowState.json @@ -1 +1 @@ -{"w":432,"h":702,"x":1753,"y":477} \ No newline at end of file +{"w":350,"h":807,"x":1770,"y":453} \ No newline at end of file From 447a68e2deeaa771a775629982905e90f15e92ec Mon Sep 17 00:00:00 2001 From: Andrew Gundersen Date: Mon, 22 Mar 2021 09:12:09 -0500 Subject: [PATCH 075/163] update grouping --- src/App.vue | 1 + src/background/session.ts | 5 +- src/background/window.ts | 2 +- src/components/inputItem/inputItem.vue | 5 + src/modules/auth.ts | 6 +- src/modules/messages.ts | 153 +++++++++++-------------- src/views/messenger.vue | 22 +--- windowState.json | 2 +- 8 files changed, 88 insertions(+), 108 deletions(-) diff --git a/src/App.vue b/src/App.vue index 116efb1..c60eb6c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -34,6 +34,7 @@ export default defineComponent({ components: { Splash }, setup() { + // Show spash screen til true. const ready = ref(false); diff --git a/src/background/session.ts b/src/background/session.ts index 766df03..49ddb13 100644 --- a/src/background/session.ts +++ b/src/background/session.ts @@ -86,7 +86,7 @@ const handleStandardMessage = (m: StandardMessage) => { const handleAnnotationMessage = (message: Annotation) => { backgroundMitt.emit('ipc-renderer', { - endpoint: 'annotate-message', + endpoint: 'render-message', message: message }); } @@ -102,6 +102,7 @@ const onMessage = (messageStr: string): void => { } const message = JSON.parse(messageStr) + console.log("New message:") console.log(message) if (message.content) { @@ -159,7 +160,7 @@ export function initSession(): void { socket.on('open', () => { console.log('Connected to Crimata Servers.'); - // call auth renderer event on connection + // Try to authenticate token right away. backgroundMitt.emit('ipc-renderer', { endpoint: 'auth', message: null diff --git a/src/background/window.ts b/src/background/window.ts index f302746..cc669eb 100644 --- a/src/background/window.ts +++ b/src/background/window.ts @@ -54,7 +54,7 @@ const saveWindowState = () => { if (err) throw err; return; }); - } + } } // Do this on window mount. diff --git a/src/components/inputItem/inputItem.vue b/src/components/inputItem/inputItem.vue index 4dc3409..c2d2c90 100644 --- a/src/components/inputItem/inputItem.vue +++ b/src/components/inputItem/inputItem.vue @@ -42,6 +42,10 @@ export default defineComponent({ // Mark input item with user's initials. const initials = ref("") + // Set initials. + const initialData = window.localStorage.getItem("initials") + if (initialData) initials.value = initialData + // Default values for position. const xStart = 15; const yStart = window.innerHeight - 200; @@ -56,6 +60,7 @@ export default defineComponent({ // Update profile functionality. const onUpdateProfile = (_event: any, payload: any) => { initials.value = payload.message.first[0] + payload.message.last[0] + window.localStorage.setItem("initials", initials.value) } onMounted(() => { diff --git a/src/modules/auth.ts b/src/modules/auth.ts index 1baacda..f1481f2 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -21,8 +21,10 @@ if (token) { window.localStorage.setItem(AUTH_KEY, token); } +// Load key and invoke onAuthSession to try key-login. const authToken = async () => { - const { invoke } = useIpc(); + console.log("Calling auth token.") + const { invoke } = useIpc(); try { token = window.localStorage.getItem(AUTH_KEY); @@ -43,7 +45,7 @@ const authToken = async () => { } } -// authenticate on auth event +// Gets called on socket open. window.ipcRenderer.on("auth", async (_event, _arg) => { await authToken(); }); diff --git a/src/modules/messages.ts b/src/modules/messages.ts index c6e3a94..7f4d79c 100644 --- a/src/modules/messages.ts +++ b/src/modules/messages.ts @@ -2,121 +2,102 @@ import { ref } from 'vue'; import useScroll from "@/modules/scroll"; import { RenderMessage, Annotation } from "@/types/message/index"; -const messages = ref(new Map()); // ze messages. +const messages = ref(new Map()); -//---Message Grouping----------------------------------------------- +const addMessage = (message: RenderMessage) => { + messages.value.set(message.uid, message) +} -let prevRef: string; -let activeGroup = false; +const updateMessage = (annotation: Annotation) => { + const message = messages.value.get(annotation.uid) + message.context = annotation.context + message.content.text = annotation.text +} -const updateGrouping = (currentMessageRef: string) => { - if (prevRef) { +const loadMessages = () => { + const rawData = window.localStorage.getItem("crimata_messages"); + if (rawData) { + const messageData = JSON.parse(rawData) + messages.value = new Map(Object.entries(messageData)); + } +} - // Get references to the two most recent messages. - const prev = messages.value.get(prevRef) - const current = messages.value.get(currentMessageRef) +const saveMessages = () => { + const messageData = Object.fromEntries(messages.value); + window.localStorage.setItem("crimata_messages", JSON.stringify(messageData)); +} - // See if they should be grouped. - if (current.time < prev.time + 20000) { - if (prev.modifier === current.modifier) { - if (prev.context === current.context) { +const isSimmilar = (messageA: RenderMessage, messageB: RenderMessage) => { + if ((Math.abs(messageA.time - messageB.time) < 20000) && (messageA.modifier == messageB.modifier) && (messageA.context == messageB.context)) { + return true + } + return false +} - // Update grouping accordingly. - if (activeGroup) { - prev.isChild = "middle"; - } +const updateGrouping = () => { + console.log("updating grouping") + const refs = Array.from(messages.value.keys()) - else { - prev.isChild = "first"; - } + // Get the last three messages. + const first = messages.value.get(refs[refs.length - 1]) + const second = messages.value.get(refs[refs.length - 2]) + const third = messages.value.get(refs[refs.length - 3]) - current.isChild = "last"; - activeGroup = true; - } + // If messages are simmilar, update the classes. + if ((first) && (second)) { + if (isSimmilar(first, second)) { + first.isChild = "last" // i.e. last in group. + second.isChild = "first" - // If messages unrealated, reset activeGroup. - else { - activeGroup = false; + if (third) { + if ((third.isChild == "first") || (third.isChild == "middle")) { + second.isChild = "middle" } } } } - prevRef = currentMessageRef; } -//------------------------------------------------------------------ +const { setScroll, updateScrollRef, adjustScroll } = useScroll("messenger"); + +loadMessages() +setTimeout(setScroll, 1000); export default function useMessages() { - // Control the scrolling. - const { - setScroll, - updateScrollRef, - adjustScroll - } = useScroll("messenger"); + // Main function for updating the message view. + const updateMessageView = (message: RenderMessage | Annotation) => { - // Save message to local storage. - const saveMessages = () => { - const messageData = Object.fromEntries(messages.value); - window.localStorage.setItem("crimata_messages", JSON.stringify(messageData)); - } - - // Load messages from local storage. - const loadMessages = () => { - const rawData = window.localStorage.getItem("crimata_messages"); - - if (rawData) { - const messageData = JSON.parse(rawData) - messages.value = new Map(Object.entries(messageData)); - } - - } - - // Remove oldest message from the messages map. - function popMessage() { - const lastKey = Array.from(messages.value.keys()).shift(); - messages.value.delete(lastKey); - } - - // Add a new message to the view. - const addMessage = (message: RenderMessage) => { + // Step 1: See if user is scrolled down. updateScrollRef() - // Size limit of 200. - if (messages.value.size >= 200) { - popMessage(); + // Step 2: Add the new content to the view. + if ("content" in message) { + addMessage(message) + } else { + updateMessage(message) } - messages.value.set(message.uid, message); + // Step 3: Pop off oldest message (if > 200). + if (messages.value.size >= 200) { + const oldest = Array.from(messages.value.keys()).shift(); + messages.value.delete(oldest); + }; + // Step 4: Update grouping. + updateGrouping() + + // Setp 5: Scroll the view (if scrolled down). setTimeout(adjustScroll, 20); - updateGrouping(message.uid) + + // Step 6: Save the view data. saveMessages() + } - // Update an existing message in the view. - const updateMessage = (a: Annotation) => { - updateScrollRef() - - // update message if in the map. - const message = messages.value.get(a.uid) - message.context = a.context - message.content.text = a.text - - setTimeout(adjustScroll, 20); - saveMessages() - } - - loadMessages() - setTimeout(setScroll, 1000); - console.log("loaded messages") - return { messages, - saveMessages, - addMessage, - updateMessage, - loadMessages + updateMessageView } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/views/messenger.vue b/src/views/messenger.vue index 5b7d515..318c5cf 100644 --- a/src/views/messenger.vue +++ b/src/views/messenger.vue @@ -32,30 +32,20 @@ export default defineComponent({ const { emitter } = useMitt(); // Handle messages in view. - const { - messages, - addMessage, - updateMessage, - } = useMessages(); + const { messages, updateMessageView } = useMessages(); - // Alter existing message. - const onAnnotateMessage = (_event: any, payload: any) => { - updateMessage(payload.message); - } - - // Render new message. - const onRenderMessage = (_event: any, payload: any) => { - addMessage(payload.message); + // Update message view on new content. + const onNewContent = (_event: any, payload: any) => { + updateMessageView(payload.message) } onMounted(() => { // Front end listener. - emitter.on('self-message', (message) => addMessage(message)); + emitter.on('self-message', (message) => updateMessageView(message)); // Back end listener. - window.ipcRenderer.on("render-message", onRenderMessage); - window.ipcRenderer.on("annotate-message", onAnnotateMessage); + window.ipcRenderer.on("render-message", onNewContent); }); diff --git a/windowState.json b/windowState.json index 8e8aa57..9b3dcc5 100644 --- a/windowState.json +++ b/windowState.json @@ -1 +1 @@ -{"w":350,"h":807,"x":1770,"y":453} \ No newline at end of file +{"w":735,"h":510,"x":1354,"y":513} \ No newline at end of file From 7bb0bda7567180b42ba2cca34ba6944d7b34704a Mon Sep 17 00:00:00 2001 From: Andrew Gundersen Date: Mon, 22 Mar 2021 09:53:13 -0500 Subject: [PATCH 076/163] fixed text input --- .../inputItem/controllers/textCtrl.ts | 20 +++++++++++++++---- src/modules/messages.ts | 4 ++-- windowState.json | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/inputItem/controllers/textCtrl.ts b/src/components/inputItem/controllers/textCtrl.ts index 6a3c22d..ce3a552 100644 --- a/src/components/inputItem/controllers/textCtrl.ts +++ b/src/components/inputItem/controllers/textCtrl.ts @@ -97,6 +97,14 @@ export default function useTextInputController(elementX: Ref) { } } + // Keys that are capable of opening the text input. + const hotKeyRange = keyboardNameMap.slice(47, 91) + const isHotKey = (key: string) => { + if (hotKeyRange.includes(key)) { + return true + } + } + //---Callbacks----------------------------------------------- const onKeyDown = (e: KeyboardEvent) => { @@ -104,16 +112,20 @@ export default function useTextInputController(elementX: Ref) { if (textInput) { - // First-key handler. + // Only runs on firstKey. if (firstKey) { - if (key == "SPACE") return + + if (!isHotKey(key)) { + return + } + prepInput() } textInput.focus(); // Close input when no text or on ESC. - if (textInput.value == "" && !firstKey) { + if ((textInput.value == "") && (!firstKey) && (key === "BACK_SPACE")) { clearInput() return } @@ -138,7 +150,7 @@ export default function useTextInputController(elementX: Ref) { //----------------------------------------------------------- // Watch parent position and update side. - watch(elementX, (elementX, previous) => { + watch(elementX, (elementX, _previous) => { const winW = window.innerWidth // Logic depends on the side we are on. diff --git a/src/modules/messages.ts b/src/modules/messages.ts index 7f4d79c..2dcaf00 100644 --- a/src/modules/messages.ts +++ b/src/modules/messages.ts @@ -83,7 +83,7 @@ export default function useMessages() { if (messages.value.size >= 200) { const oldest = Array.from(messages.value.keys()).shift(); messages.value.delete(oldest); - }; + } // Step 4: Update grouping. updateGrouping() @@ -100,4 +100,4 @@ export default function useMessages() { messages, updateMessageView } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/windowState.json b/windowState.json index 9b3dcc5..a9fec30 100644 --- a/windowState.json +++ b/windowState.json @@ -1 +1 @@ -{"w":735,"h":510,"x":1354,"y":513} \ No newline at end of file +{"w":735,"h":510,"x":944,"y":489} \ No newline at end of file From 4b3023fd4cf0f8612563d2246d1f4ee90c5c8de9 Mon Sep 17 00:00:00 2001 From: Andrew Gundersen Date: Mon, 22 Mar 2021 20:35:22 -0500 Subject: [PATCH 077/163] upgrading session --- src/background/handlers.ts | 69 ++++++++ src/background/run.ts | 9 +- src/background/session.ts | 161 +++++++----------- .../inputItem/controllers/textCtrl.ts | 2 +- src/modules/auth.ts | 2 +- src/modules/messages.ts | 7 +- src/modules/ws.ts | 130 ++++++++++++++ src/views/login.vue | 11 +- windowState.json | 2 +- 9 files changed, 288 insertions(+), 105 deletions(-) create mode 100644 src/background/handlers.ts create mode 100644 src/modules/ws.ts diff --git a/src/background/handlers.ts b/src/background/handlers.ts new file mode 100644 index 0000000..4f3764b --- /dev/null +++ b/src/background/handlers.ts @@ -0,0 +1,69 @@ +import { backgroundMitt } from './emitter'; + +// Attempt an authenticaion, either with Token or with Creds. +const onAuthRequest = async (e: any, payload: string | Creds) => { + + return new Promise((resolve, reject) => { + + console.log('Authenticating...'); + + // Handle auth response from server. + backgroundMitt.once('auth-res', (res: string) => { + + if (res == "locked") { + reject(res); + } else { + console.log('Success!'); + auth = true; + resolve(res); + } + + }); + + if (typeof payload !== "string") { + payload = JSON.stringify(payload) + } + + // Send token to backend + socket.send(payload); + + }); +}; + + +const handleAuthMessage = (message: string) => { + backgroundMitt.emit('auth-res', message); +} + +const handleProfileMessage = (message: Profile) => { + backgroundMitt.emit('ipc-renderer', { + endpoint: 'update-profile', + message: message + }); +} + +const handleStandardMessage = (m: StandardMessage) => { + + // Create a render message object. + const message = renderMessage( + m.content.text, m.content.audio, m.context, m.modifier); + + // Play audio if any. + if (message.content.audio) { + const audioBytes = Buffer.from(m.content.audio as string, 'hex'); + m.content.audio = true; + play(audioBytes); + } + + backgroundMitt.emit('ipc-renderer', { + endpoint: 'render-message', + message: message + }); +} + +const handleAnnotationMessage = (message: Annotation) => { + backgroundMitt.emit('ipc-renderer', { + endpoint: 'render-message', + message: message + }); +} \ No newline at end of file diff --git a/src/background/run.ts b/src/background/run.ts index a007ad5..4c312d8 100644 --- a/src/background/run.ts +++ b/src/background/run.ts @@ -1,7 +1,7 @@ "use strict"; import { createWindow } from './window'; -import { initSession } from './session'; +import useWebSockets from './session'; import { initAudioIO } from './audio'; let socket = false; @@ -11,11 +11,16 @@ let socket = false; */ export async function main() { + const { initSession } = useWebSockets() + // create main window. await createWindow(); // Instantiate socket session with crimata-platorm. - if (!socket) initSession(); + if (!socket) { + initSocketSession(); + } + socket = true; // Begin audio stream. diff --git a/src/background/session.ts b/src/background/session.ts index 49ddb13..28787f4 100644 --- a/src/background/session.ts +++ b/src/background/session.ts @@ -1,28 +1,72 @@ -"use strict"; - -import WebSocket from 'ws'; import { backgroundMitt } from './emitter'; -import { ipcMain } from "electron"; -import { play } from './audio'; -import { - Creds, - Profile, - Annotation, - StandardMessage, - ClientMessage -} from "@/types/message/index"; +import { handleAuthMessage, handleStandardMessage, handleProfileMessage, handleAnnotation } from "./handlers"; -import { clientMessage, renderMessage } from '@/modules/message'; -const ip = 'ws://127.0.0.1'; -const port = 8760; -const reconnectTimeout = 3000; //ms -let socket: WebSocket; -let success = false; -let auth = false; +// Handle messages from server. +const onServerMessage = (data: string): void => { + const message = JSON.parse(data) -const onAuthSession = async (e: any, payload: string | Creds) => { + if (message.key) { + handleAuthMessage(message) + } + + else if (message.content) { + handleStandardMessage(message) + } + + else if (message.first) { + handleProfileMessage(message) + } + + else { + handleAnnotationMessage(message) + } + +}; + +// Handle messages from client. +const onClientMessage = (_event: IpcMainEvent, payload: any) { + sendMessage(JSON.stringify(payload)) +} + +export default function agentInterface() { + + const { createSocket, sendMessage } = useWebSockets(onServerMessage) + + const initSession = () => { + + ipcMain.removeAllListeners() + ipcMain.removeHandler('auth-session'); + + ipcMain.handle('auth-request', onAuthRequest); + ipcMain.on('client-message', onClientMessage); + + createSocket() + } + + return { + initSession + } + +} + + + + + + + + + + + + + + + +// Attempt an authenticaion, either with Token or with Creds. +const onAuthRequest = async (e: any, payload: string | Creds) => { return new Promise((resolve, reject) => { @@ -52,8 +96,6 @@ const onAuthSession = async (e: any, payload: string | Creds) => { }; -//---Message Handlers----------------------------------------------- - const handleAuthMessage = (message: string) => { backgroundMitt.emit('auth-res', message); } @@ -91,8 +133,6 @@ const handleAnnotationMessage = (message: Annotation) => { }); } -//------------------------------------------------------------------ - const onMessage = (messageStr: string): void => { // Auth messages are strings. @@ -122,77 +162,4 @@ const onMessage = (messageStr: string): void => { const onClientMessage = (e: any, payload: ClientMessage): void => { console.log('sending message'); socket.send(JSON.stringify(payload)); -} - -const restart = () => { - auth = false; - initSession(); -} - -// Run every time we want to connect to backend. -export function initSession(): void { - - // Close existing sockets. - if (socket) { - socket.removeAllListeners(); - socket.terminate(); - socket.close(); - success = false; - } - - // Init new socket. - socket = new WebSocket(`${ip}:${port}`); - socket.binaryType = 'arraybuffer'; - - // handle renderer auth-token event - ipcMain.removeHandler('auth-session'); // avoid setting duplicate handlers - ipcMain.handle('auth-session', onAuthSession); - - // handle user message event - ipcMain.removeAllListeners('client-message'); - ipcMain.on('client-message', onClientMessage); - - // handle renderer logout event - ipcMain.removeAllListeners('logout'); - ipcMain.on('logout', (_event, _payload: string) => restart()); - - // Connect to the backend. - socket.on('open', () => { - console.log('Connected to Crimata Servers.'); - - // Try to authenticate token right away. - backgroundMitt.emit('ipc-renderer', { - endpoint: 'auth', - message: null - }); - - success = true; - - }); - - // Handle for failed connect, try again. - socket.on('error', (_e) => { - console.log('ERROR: Failed to connect.'); - socket.removeAllListeners(); - socket.close(); - setTimeout(() => { - if (!success) { - console.log('Reconnecting...'); - initSession(); - } - }, reconnectTimeout); - - }); - - socket.on('close', () => { - console.log('Connection droped! Restarting...'); - restart(); - }); - - socket.on("message", onMessage); -} - -export function sendAudio(audio: string, uid: string): void { - const m = clientMessage("", audio, uid); - socket.send(JSON.stringify(m)); } \ No newline at end of file diff --git a/src/components/inputItem/controllers/textCtrl.ts b/src/components/inputItem/controllers/textCtrl.ts index ce3a552..18d686a 100644 --- a/src/components/inputItem/controllers/textCtrl.ts +++ b/src/components/inputItem/controllers/textCtrl.ts @@ -97,7 +97,7 @@ export default function useTextInputController(elementX: Ref) { } } - // Keys that are capable of opening the text input. + // Keys that are capable of opening the text input (numbers and letters). const hotKeyRange = keyboardNameMap.slice(47, 91) const isHotKey = (key: string) => { if (hotKeyRange.includes(key)) { diff --git a/src/modules/auth.ts b/src/modules/auth.ts index f1481f2..3430cf2 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -23,7 +23,7 @@ if (token) { // Load key and invoke onAuthSession to try key-login. const authToken = async () => { - console.log("Calling auth token.") + console.log("Calling auth token!!") const { invoke } = useIpc(); try { token = window.localStorage.getItem(AUTH_KEY); diff --git a/src/modules/messages.ts b/src/modules/messages.ts index 2dcaf00..4b0c8b7 100644 --- a/src/modules/messages.ts +++ b/src/modules/messages.ts @@ -96,8 +96,13 @@ export default function useMessages() { } + const resetMessages = () => { + messages.value.clear() + } + return { messages, - updateMessageView + updateMessageView, + resetMessages } } \ No newline at end of file diff --git a/src/modules/ws.ts b/src/modules/ws.ts new file mode 100644 index 0000000..c487573 --- /dev/null +++ b/src/modules/ws.ts @@ -0,0 +1,130 @@ +"use strict"; + +import WebSocket from 'ws'; +import { ipcMain } from "electron"; + +let socket: WebSocket; + + +// Run every time we want to connect to backend. +export default function useWebSockets(receiveCallback: (s: Buffer) => any) { + + const sendMessage = (data: string) => { + + console.log("Sending message: ", data) + + socket.send(data) + + } + + const onOpen = (event: WebSocket.OpenEvent) => { + + console.log('Connected to Server!'); + + } + + const onServerMessage = (event: WebSocket.MessageEvent) => { + + console.log('Message received: ', event); + + receiveCallback(event.data) + + } + + const onClose = (event: WebSocket.CloseEvent) => { + console.log("socket closed normally.") + } + + const onError = (event: WebSocket.ErrorEvent) => { + console.log('WebSocket error: ', event); + + console.log("Reconnecting...") + createSocket() + } + + const createSocket = () => { + socket = new WebSocket(`ws://127.0.0.1:8760`) + + // Add listeners. + socket.addEventListener("open", onOpen) + socket.addEventListener("message", onServerMessage) + socket.addEventListener("close", onClose) + socket.addEventListener("error", createSocket) + + console.log("New socket created.") + } + + return { + createSocket + } + +} + + + + + + + + + // Close existing sockets. + if (socket) { + socket.removeAllListeners(); + socket.terminate(); + socket.close(); + success = false; + } + + // Init new socket. + socket = new WebSocket(`ws://127.0.0.1:8760`); + socket.binaryType = 'arraybuffer'; + + // Handle requests for authentication (promise). + ipcMain.removeHandler('auth-session'); // avoid setting duplicate handlers + ipcMain.handle('auth-session', onAuthSession); + + // handle user message event + ipcMain.removeAllListeners('client-message'); + ipcMain.on('client-message', onClientMessage); + + // handle renderer logout event + ipcMain.removeAllListeners('logout'); + ipcMain.on('logout', (_event, _payload: string) => restart()); + + // Connect to the backend. + socket.on('open', () => { + console.log('Connected to Crimata Servers.'); + + // Try to authenticate token right away. + backgroundMitt.emit('ipc-renderer', { + endpoint: 'auth', + message: null + }); + + success = true; + + }); + + // Error handling. + socket.on('error', (_e) => { + + console.log('ERROR: Failed to connect.'); + socket.removeAllListeners(); + socket.close(); + + setTimeout(() => { + if (!success) { + console.log('Reconnecting...'); + initSession(); + } + }, 3000); // reconnect timeout + + }); + + socket.on('close', () => { + console.log('Connection droped! Restarting...'); + restart(); + }); + + socket.on("message", onMessage); +} \ No newline at end of file diff --git a/src/views/login.vue b/src/views/login.vue index cba4b85..53e335f 100644 --- a/src/views/login.vue +++ b/src/views/login.vue @@ -41,6 +41,7 @@ import { defineComponent, ref } from "vue"; import { useAuth } from "@/modules/auth"; import { useIpc } from "@/modules/ipc"; import { useRouter } from "vue-router"; +import useMessages from "@/modules/messages"; export default defineComponent({ name: "Login", @@ -48,6 +49,7 @@ export default defineComponent({ setup() { const { setToken } = useAuth(); + const { resetMessages } = useMessages(); const router = useRouter(); const email = ref(""); @@ -65,8 +67,13 @@ export default defineComponent({ }; try { - const res = await invoke('auth-session', payload); - setToken(res); + const response = await invoke('auth-session', payload); + + // On login without token, we clear localStorage and messages. + window.localStorage.clear(); + resetMessages(); + + setToken(response); invalid.value = false; router.push({ name: "home" }); } diff --git a/windowState.json b/windowState.json index a9fec30..6e4a7d7 100644 --- a/windowState.json +++ b/windowState.json @@ -1 +1 @@ -{"w":735,"h":510,"x":944,"y":489} \ No newline at end of file +{"w":350,"h":699,"x":649,"y":168} \ No newline at end of file From 7dcfd9252add36016ec282da0742a18c9a452766 Mon Sep 17 00:00:00 2001 From: Andrew Gundersen Date: Tue, 23 Mar 2021 08:09:10 -0500 Subject: [PATCH 078/163] prevent window drag when inputitem overlaps with titlebar --- src/App.vue | 5 ++--- src/components/inputItem/inputItem.vue | 3 +++ windowState.json | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/App.vue b/src/App.vue index c60eb6c..99fa548 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,7 @@ diff --git a/src/main.ts b/src/main.ts index fcad020..35774f7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,6 @@ // src/main.ts import App from "./App.vue"; -import router from "./router"; import mitt from "mitt"; import { createApp } from "vue"; @@ -12,6 +11,5 @@ const emitter = mitt(); const app = createApp(App) -app.use(router) app.provide("mitt", emitter) app.mount("#app"); diff --git a/src/modules/auth.ts b/src/modules/auth.ts index 3430cf2..b095034 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -21,7 +21,7 @@ if (token) { window.localStorage.setItem(AUTH_KEY, token); } -// Load key and invoke onAuthSession to try key-login. +// Token authentication. const authToken = async () => { console.log("Calling auth token!!") const { invoke } = useIpc(); diff --git a/src/modules/ws.ts b/src/modules/ws.ts index c487573..6a6594b 100644 --- a/src/modules/ws.ts +++ b/src/modules/ws.ts @@ -7,7 +7,7 @@ let socket: WebSocket; // Run every time we want to connect to backend. -export default function useWebSockets(receiveCallback: (s: Buffer) => any) { +export default function useWebSockets(receiveCallback: (s: string) => any) { const sendMessage = (data: string) => { @@ -27,14 +27,15 @@ export default function useWebSockets(receiveCallback: (s: Buffer) => any) { console.log('Message received: ', event); - receiveCallback(event.data) + receiveCallback(event.data.toString()) } const onClose = (event: WebSocket.CloseEvent) => { - console.log("socket closed normally.") + console.log("Socket closed normally.") } + // Reconnect automatically on error. const onError = (event: WebSocket.ErrorEvent) => { console.log('WebSocket error: ', event); @@ -55,76 +56,8 @@ export default function useWebSockets(receiveCallback: (s: Buffer) => any) { } return { - createSocket + createSocket, + sendMessage } -} - - - - - - - - - // Close existing sockets. - if (socket) { - socket.removeAllListeners(); - socket.terminate(); - socket.close(); - success = false; - } - - // Init new socket. - socket = new WebSocket(`ws://127.0.0.1:8760`); - socket.binaryType = 'arraybuffer'; - - // Handle requests for authentication (promise). - ipcMain.removeHandler('auth-session'); // avoid setting duplicate handlers - ipcMain.handle('auth-session', onAuthSession); - - // handle user message event - ipcMain.removeAllListeners('client-message'); - ipcMain.on('client-message', onClientMessage); - - // handle renderer logout event - ipcMain.removeAllListeners('logout'); - ipcMain.on('logout', (_event, _payload: string) => restart()); - - // Connect to the backend. - socket.on('open', () => { - console.log('Connected to Crimata Servers.'); - - // Try to authenticate token right away. - backgroundMitt.emit('ipc-renderer', { - endpoint: 'auth', - message: null - }); - - success = true; - - }); - - // Error handling. - socket.on('error', (_e) => { - - console.log('ERROR: Failed to connect.'); - socket.removeAllListeners(); - socket.close(); - - setTimeout(() => { - if (!success) { - console.log('Reconnecting...'); - initSession(); - } - }, 3000); // reconnect timeout - - }); - - socket.on('close', () => { - console.log('Connection droped! Restarting...'); - restart(); - }); - - socket.on("message", onMessage); } \ No newline at end of file diff --git a/src/router.ts b/src/router.ts deleted file mode 100644 index ee94a61..0000000 --- a/src/router.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - createRouter, - createWebHistory, - createWebHashHistory, - RouteRecordRaw -} from "vue-router"; -import Home from "@/views/messenger.vue"; -import { useAuth } from '@/modules/auth'; - -// Define the routes (/*) for the app here. -const routes: Array = [ - { - path: "/", - name: "home", - component: Home, - meta: { requiresAuth: true }, - }, - { - path: "/login", - name: "login", - component: () => import("@/views/login.vue"), - meta: { requiresAuth: false }, - }, - { - path: "/register", - name: "register", - component: () => import("@/views/register.vue"), - meta: { requiresAuth: false }, - }, -]; - -const router = createRouter({ - history: process.env.IS_ELECTRON ? createWebHashHistory() : createWebHistory(process.env.BASE_URL), - routes -}); - -// route auth check -router.beforeEach((to, from, next) => { - const { accessToken } = useAuth(); - - // Not logged into a guarded route? - if (to.meta.requiresAuth && !accessToken.value) { - next({ name: 'login' }) - } - - // Logged in for an auth route - else if ((to.name == 'login' || to.name == 'register') && accessToken.value){ - next({ name: 'home' }); - } - - // Carry On... - else next(); -}) - -export default router; diff --git a/src/views/login.vue b/src/views/login.vue index 53e335f..5f31fbd 100644 --- a/src/views/login.vue +++ b/src/views/login.vue @@ -24,7 +24,7 @@
- +
Incorrect Credentials @@ -48,57 +48,31 @@ export default defineComponent({ setup() { + const { post } = useIpc(); const { setToken } = useAuth(); const { resetMessages } = useMessages(); + const router = useRouter(); - const email = ref(""); - const password = ref(""); - const invalid = ref(false); + const form = ref({ + email: "", + password: "", + }) - const { invoke } = useIpc(); - - const submit = async () => { - - const payload = { - request: "login", - email: email.value, - password: password.value - }; - - try { - const response = await invoke('auth-session', payload); - - // On login without token, we clear localStorage and messages. - window.localStorage.clear(); - resetMessages(); - - setToken(response); - invalid.value = false; - router.push({ name: "home" }); - } - - catch(e) { - invalid.value = true; - console.log('Error loging in.'); - } - - }; - - const switchView = () => { - router.push({ name: "register" }); - }; + // Submit login credentials to the backend. + const submitForm = () => { + post("auth-message", form) + } return { - submit, - email, - password, - switchView, - invalid + form, + submitForm } }, + }); + diff --git a/windowState.json b/windowState.json index 6e4a7d7..26c34cb 100644 --- a/windowState.json +++ b/windowState.json @@ -1 +1 @@ -{"w":350,"h":699,"x":649,"y":168} \ No newline at end of file +{"w":629,"h":500,"x":1423,"y":657} \ No newline at end of file From 85c3799fca3294f98ed55a6d551a1b1c14b46a91 Mon Sep 17 00:00:00 2001 From: Andrew Gundersen Date: Mon, 29 Mar 2021 10:15:54 -0500 Subject: [PATCH 081/163] auth enhancements amongst other things --- session.json | 1 + src/App.vue | 96 ++++++-------- src/background/audio.ts | 71 ++++------ src/background/handlers.ts | 45 ------- src/background/helpers.ts | 37 ++++++ src/background/init.ts | 9 +- src/background/session.ts | 136 ++++++++++++++++---- src/background/window.ts | 15 ++- src/components/controllers/audioCtrl.ts | 8 +- src/components/{input.vue => inputItem.vue} | 35 ++--- src/components/login.vue | 9 +- src/components/messenger.vue | 50 ++++--- src/components/settings.vue | 1 - src/components/{text.vue => textInput.vue} | 0 src/modules/auth.ts | 71 ---------- src/modules/ipc.ts | 6 +- src/modules/messages.ts | 84 ++++++------ src/modules/scroll.ts | 4 +- src/modules/{ws.ts => websockets.ts} | 0 src/types/message/index.ts | 29 +++-- windowState.json | 2 +- 21 files changed, 341 insertions(+), 368 deletions(-) create mode 100644 session.json delete mode 100644 src/background/handlers.ts create mode 100644 src/background/helpers.ts rename src/components/{input.vue => inputItem.vue} (81%) rename src/components/{text.vue => textInput.vue} (100%) delete mode 100644 src/modules/auth.ts rename src/modules/{ws.ts => websockets.ts} (100%) diff --git a/session.json b/session.json new file mode 100644 index 0000000..50bed80 --- /dev/null +++ b/session.json @@ -0,0 +1 @@ +{"key":"0c116d29-8cfe-491f-a84d-1d021297cc69","newMessages":[]} \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index ce8fa5b..7bc0ee1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,6 @@ @@ -31,6 +36,7 @@ import { defineComponent, onMounted, onUnmounted, ref } from "vue"; import { IpcRendererEvent } from "electron"; import { authRequest } from '@/modules/message'; import { useIpc } from "@/modules/ipc"; +import { Profile } from "@/types/message"; import Splash from "@/components/splash.vue"; import Messenger from "@/components/messenger.vue"; @@ -44,74 +50,52 @@ export default defineComponent({ }, setup() { - const { post, invoke } = useIpc(); + const { post } = useIpc(); - const auth = ref(false); - const windowReady = ref(false); - const sessionReady = ref(false); + // Whether browser has received user info yet. + const ready = ref(false); - // When connection is established, we send key over. - const onOpen = (_event: IpcRendererEvent, payload: any) => { - console.log(`Connected to Crimata.`) - const key = window.localStorage.getItem("key"); + // Information about current user. + const profile = ref(false); - if (key) { - console.log(`Sending key: ${key}`) - post("client-message", authRequest(key, false, false)) - } + // New messages that browser missed while closed. + const newMessages = ref([]) - else { - console.log("No key, session ready.") - sessionReady.value = true + // Receive updated information about the session. + const updateState = (_event: IpcRendererEvent, payload: any) => { + console.log("APP:Received updated profile and new messages: \n" + + ` profile: ${payload.message.profile}\n` + + ` new: ${payload.message.newMessages}`) + + if (payload.message.profile) { + console.log(`Logged-in, showing Messenger View.`) + } else { + console.log(`Logged-out, showing Login View.`) } - } + // Set profile and newMessages. + profile.value = payload.message.profile; + newMessages.value = payload.message.newMessages; - // Update authenticate state on auth message from server. - const onAuthResponse = (_event: IpcRendererEvent, payload: any) => { - const key = payload.message.key - const usr = payload.message.usr - console.log(`Received auth response: ${usr}, ${key}`) + ready.value = true; - if (key) { - console.log(`Auth success, saving key: ${key}.`) - window.localStorage.setItem("key", payload.message.key) - auth.value = true; - } - - else { - console.log(`Auth failed, clearing local storage.`) - window.localStorage.clear() - auth.value = false; - } - - console.log("Session is ready.") - sessionReady.value = true - - } - - // Post navbar action to backend. - const callNavbar = (action: string) => { - invoke("nav-bar", action); } onMounted(() => { - window.ipcRenderer.on("on-connect", onOpen) - window.ipcRenderer.on("auth-response", onAuthResponse) - window.ipcRenderer.on("window-ready", () => windowReady.value = true); + console.log("APP:mounted.") + window.ipcRenderer.on("update-state", updateState) + post("app-mounted", "") }); onUnmounted(() => { - window.ipcRenderer.removeAllListeners("on-connect") - window.ipcRenderer.removeAllListeners("window-ready") - window.ipcRenderer.removeAllListeners("auth-response") + window.ipcRenderer.removeAllListeners("update-state") }) return { - callNavbar, - sessionReady, - windowReady, - auth + ready, + profile, + post, + newMessages } } }) diff --git a/src/background/audio.ts b/src/background/audio.ts index c4cd6fd..aa57716 100644 --- a/src/background/audio.ts +++ b/src/background/audio.ts @@ -3,13 +3,13 @@ "use strict"; import { ipcMain } from "electron"; -import { backgroundMitt } from './emitter'; +import { backgroundMitt } from '@/modules/emitter'; const portAudio = require('naudiodon'); // Audio in and out stream objects. -let ai: typeof portAudio.AudioIO | null = null; -let ao: typeof portAudio.AudioIO | null = null; +let ai: typeof portAudio.AudioIO | Boolean = false; +let ao: typeof portAudio.AudioIO | Boolean = false; // Whether activly recording. let record = false; @@ -26,9 +26,15 @@ const audioOptions = { closeOnError: false, } +// Toggles record to true to begin capturing chunks. +const onRecordingStart = (_event: any, _payload: any) => { + console.log("AUDIO: Beginning audio capture.") + record = true; +} // Returns recorded audio to frontend and sets record to false. const onRecordingEnd = async (_event: any, payload: any) => { + console.log("AUDIO:Sending audio to browser.") return new Promise((resolve, reject) => { @@ -59,7 +65,7 @@ export function initAudioIO(): void { // If recording, we capture the data. if (record) { - console.log('Recording...') + console.log('AUDIO:Recording...') audioContainer.input += chunk; } @@ -84,11 +90,11 @@ export function initAudioIO(): void { // Listen to record. console.log("AUDIO:Adding recording listeners.") - ipcMain.removeAllListeners('start-recording'); - ipcMain.on('start-recording', () => record = true); + ipcMain.removeAllListeners("start-recording"); + ipcMain.on("start-recording", onRecordingStart); - ipcMain.removeHandler('stop-recording'); - ipcMain.handle('stop-recording', onRecordingEnd); + ipcMain.removeHandler("stop-recording"); + ipcMain.handle("stop-recording", onRecordingEnd); } @@ -110,10 +116,13 @@ function bufSplit(buf: Buffer, len: number): Array { } // Audio playback. -export function play(input: Buffer): void { - let i = 0; +export function play(input: string): void { - const audio = bufSplit(input, 8192); + // Format the audio. + const audio = bufSplit( + Buffer.from(input as string, 'hex'), + 8192 + ); // Called on end of write. const callback = () => { @@ -131,6 +140,7 @@ export function play(input: Buffer): void { function write() { let chunk: Buffer; let ok = true; + let i = 0; do { chunk = audio[i]; @@ -157,42 +167,13 @@ export function play(input: Buffer): void { // Get's called on window close. export function stopStream() { console.log("AUDIO:Stopping audio stream.") - if(ai != null) { - ai.quit(); - ai = null; + if (ai) { + ai.quit() } - if (ao != null) { - ao.quit(); - ao = null; + if (ao) { + ao.quit() } + console.log("AUDIO:Audio closed.") } -// // Returns recorded audio to frontend. -// const getAudio = async (_event: any, payload: any) => { - -// return new Promise((resolve, reject) => { - -// // Handle auth response from server. -// backgroundMitt.once('auth-res', (res: string) => { - -// if (res == "locked") { -// reject(res); -// } else { -// console.log('Success!'); -// auth = true; -// resolve(res); -// } - -// }); - -// if (typeof payload !== "string") { -// payload = JSON.stringify(payload) -// } - -// // Send token to backend -// socket.send(payload); - -// }); -// }; - diff --git a/src/background/handlers.ts b/src/background/handlers.ts deleted file mode 100644 index 4ba8b7a..0000000 --- a/src/background/handlers.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { backgroundMitt } from "./emitter"; -import { renderMessage } from "@/modules/message"; -import { Profile, StandardMessage, Annotation } from "@/types/message/index"; -import { play } from "./audio"; - - -export const handleAuthMessage = (message: string) => { - backgroundMitt.emit('ipc-renderer', { - endpoint: 'auth-response', - message: message - }); -} - -export const handleProfileMessage = (message: Profile) => { - backgroundMitt.emit('ipc-renderer', { - endpoint: 'update-profile', - message: message - }); -} - -export const handleStandardMessage = (m: StandardMessage) => { - - // Create a render message object. - const message = renderMessage( - m.content.text, m.content.audio, m.context, m.modifier); - - // Play audio if any. - if (message.content.audio) { - const audioBytes = Buffer.from(m.content.audio as string, 'hex'); - m.content.audio = true; - play(audioBytes); - } - - backgroundMitt.emit('ipc-renderer', { - endpoint: 'render-message', - message: message - }); -} - -export const handleAnnotation = (message: Annotation) => { - backgroundMitt.emit('ipc-renderer', { - endpoint: 'render-message', - message: message - }); -} \ No newline at end of file diff --git a/src/background/helpers.ts b/src/background/helpers.ts new file mode 100644 index 0000000..098c203 --- /dev/null +++ b/src/background/helpers.ts @@ -0,0 +1,37 @@ +import fs from 'fs'; +import { backgroundMitt } from "@/modules/emitter"; +import { SessionState } from "@/types/message"; + + +export const ipcEmit = (channel: string, payload: any) => { + backgroundMitt.emit('ipc-renderer', { + endpoint: channel, + message: payload + }); +} + +export const loadState = (fileName: string): SessionState => { + let state: SessionState; + + try { + state = JSON.parse(fs.readFileSync(fileName).toString()); + } + + catch (error) { + state = { + key: false, + newMessages: [] + } + } + + return state + +} + +export const saveState = (fileName: string, state: SessionState) => { + fs.writeFile(fileName, JSON.stringify(state), (err) => { + if (err) { + console.log("Error when saving state.") + } + }); +} \ No newline at end of file diff --git a/src/background/init.ts b/src/background/init.ts index a805b79..69a2938 100644 --- a/src/background/init.ts +++ b/src/background/init.ts @@ -37,15 +37,10 @@ export function initApp(dev: boolean): void { main() }); - // Quit app on window closed. - app.on("window-all-closed", () => { - console.log("MAIN:Quitting app.") - app.quit() + app.on("before-quit", () => { }); - // Shutdown audio streams peacefully. - app.on("before-quit", () => { - stopStream() + app.on("window-all-closed", () => { }); // When user clicks app icon (re-open) diff --git a/src/background/session.ts b/src/background/session.ts index ece5915..828709a 100644 --- a/src/background/session.ts +++ b/src/background/session.ts @@ -1,62 +1,131 @@ /* * Creates a websocket session with Crimata Servers. * - * Connects to Servers and attempts token authentication. Will send the result - * of the authentication to the window. It will then serve as a communication - * interface between the window and the servers. It will automatically try to - * reconnect on websocket disconnect. + * Connects to Servers and attempts key authentication. Server will respond + * with key and user profile. We send the profile to the browser. We also + * resend this information on new broser window. We then serve as a + * communication interface between the window and the servers. It will + * automatically try to reconnect on websocket disconnect. */ -import { backgroundMitt } from './emitter'; -import { ipcMain, IpcMainEvent } from "electron"; +import { backgroundMitt } from "@/modules/emitter"; +import { ipcEmit, loadState, saveState } from './helpers'; +import { ipcMain, IpcMainEvent, IpcMainInvokeEvent } from "electron"; -import useWebSockets from "@/modules/ws"; +import useWebSockets from "@/modules/websockets"; -import { handleAuthMessage, handleStandardMessage, handleProfileMessage, handleAnnotation } from "./handlers"; +import { play } from "./audio"; +import { renderMessage } from "@/modules/message"; +import { AuthProtocol, SessionState, Profile } from "@/types/message"; -// Key for key-based auth. -let key: string; +let win = true; +// Info saved to json on quit (key, newMessages). +let state: SessionState; +// Profile of current user. +let profile: Profile | boolean; +// Called when server sends auth message. +const updateState = (res: AuthProtocol) => { + console.log("SESS:Auth message received: \n" + + ` key: ${res.key}\n` + + ` alias: ${res.profile}`) + + if (state) { + + // Update key. + state.key = res.key; + + // Update the user profile. + profile = res.profile; + + // Send upated profile to frontend. + console.log("SESS:Sending updated user profile to browser.") + ipcEmit("update-state", { + profile: profile, + newMessages: state.newMessages + }) + + // Save the updated state to json. + console.log("SESS:Saving session state.") + saveState("session.json", state) + + } + +} + +// Send state on new window. +const onNewBrowserWindow = (_event: IpcMainInvokeEvent, _payload: any) => { + console.log("SESS:New window, sending profile.") + if (profile) { + ipcEmit("update-state", { + profile: profile, + newMessages: state.newMessages + }) + } +} // Calls appropriate endpoint for a server message. const onMessage = (data: string) => { - const message = JSON.parse(data) + let message = JSON.parse(data) + // AuthProtocol message. if (message.hasOwnProperty("key")) { - handleAuthMessage(message) + updateState(message) } + // Standard message. else if (message.content) { - handleStandardMessage(message) - } - else if (message.first) { - handleProfileMessage(message) + // Convert to render message + message = renderMessage( + message.content.text, + message.content.audio, + message.context, + message.modifier + ); + + if (win) { + console.log("SESS:Emitting standard message.") + if (message.audio) { + play(message.audio) + } + ipcEmit("render-message", message) + } + + else { + console.log("SESS:No window: saving message.") + state.newMessages.push(message); + } } else { - handleAnnotation(message) + if (win) { + ipcEmit("render-message", message) + } } }; -// Attempt token authentication onOpen. +// When socket connects, we update state. const onOpen = () => { - if (key) { + console.log(`SESS:Sending key: ${state.key}`) + + if (state) { sendMessage({ - "key": key, + "key": state.key, "usr": false, "pwd": false }) } } +// Websockets module. const { createSocket, sendMessage } = useWebSockets(onMessage, onOpen); // Handle messages from window/client. -const onMessageFromWindow = (_event: IpcMainEvent, payload: any) => { +const onClientMessage = (_event: IpcMainEvent, payload: any) => { console.log("New client message") const success = sendMessage(payload) @@ -68,17 +137,30 @@ const onMessageFromWindow = (_event: IpcMainEvent, payload: any) => { } // Call this to initialize session with Crimata servers. -export const initSession = (key: string) => { +export const initSession = () => { + console.log("SESS:Creating new session.") - // Set the key. - key = key; + // Load Json or createState. + state = loadState("session.json") + + console.log("SESS:State loaded: \n" + + ` key: ${state.key}\n` + + ` new: ${state.newMessages}`) // Open socket connection. createSocket() + // Attack browser window init listener. + ipcMain.removeAllListeners("app-mounted") + ipcMain.on("app-mounted", onNewBrowserWindow); + // Attach listeners for frontend. ipcMain.removeAllListeners("client-message") - ipcMain.on("client-message", onMessageFromWindow); + ipcMain.on("client-message", onClientMessage); - -} \ No newline at end of file + // Keep win up-to-date. + backgroundMitt.on('window-active', (state: boolean) => { + win = state; + }); + +} diff --git a/src/background/window.ts b/src/background/window.ts index 246f5a1..5d180cc 100644 --- a/src/background/window.ts +++ b/src/background/window.ts @@ -2,7 +2,7 @@ import { BrowserWindow, ipcMain } from "electron"; import { createProtocol } from "vue-cli-plugin-electron-builder/lib"; -import { backgroundMitt } from './emitter'; +import { backgroundMitt } from '@/modules/emitter'; import { RenderMessage } from "@/types/message/index"; import * as path from "path"; import fs from 'fs'; @@ -50,8 +50,9 @@ const saveWindowState = () => { ); fs.writeFile('windowState.json', state, (err) => { - if (err) throw err; - return; + if (err) { + console.log("BW:Error saving window state.") + } }); } } @@ -64,12 +65,12 @@ const onWindowMount = (): void => { backgroundMitt.emit('window-active', true); // Handle win nav-bar event. - ipcMain.removeHandler("nav-bar") // avoid setting duplicate handlers - ipcMain.handle("nav-bar", onNavBar); + ipcMain.removeAllListeners("nav-bar") // avoid setting duplicate handlers + ipcMain.on("nav-bar", onNavBar); // Gateway for messages to the frontend. - backgroundMitt.removeAllListeners('ipc-renderer') - backgroundMitt.on('ipc-renderer', renderMessage); + backgroundMitt.removeAllListeners("ipc-renderer") + backgroundMitt.on("ipc-renderer", renderMessage); console.log("BW:Listeners created.") } diff --git a/src/components/controllers/audioCtrl.ts b/src/components/controllers/audioCtrl.ts index 0ae0fe8..8a73868 100644 --- a/src/components/controllers/audioCtrl.ts +++ b/src/components/controllers/audioCtrl.ts @@ -49,11 +49,11 @@ export default function useAudioInputController (typing: Ref) { // Start recording on space bar. if (cmd == "SPACE" && !recording.value && !typing.value) { - post('start-recording', ""); + console.log("INPT:Starting record.") + post("start-recording", ""); showRecIcon() recording.value = true; - console.log("Recording...") } @@ -70,7 +70,8 @@ export default function useAudioInputController (typing: Ref) { emitter.emit("self-message", message); // Stop recording and get audio from recorder. - const audio = await invoke('stop-recording', ""); + console.log("INPT:Stopping record.") + const audio = await invoke("stop-recording", ""); // Send message to the backend for processing. const clientM = clientMessage("", audio, message.uid); @@ -78,7 +79,6 @@ export default function useAudioInputController (typing: Ref) { hideRecIcon() recording.value = false; - console.log("Stopping record...") } diff --git a/src/components/input.vue b/src/components/inputItem.vue similarity index 81% rename from src/components/input.vue rename to src/components/inputItem.vue index c2d2c90..b24d9c4 100644 --- a/src/components/input.vue +++ b/src/components/inputItem.vue @@ -26,26 +26,24 @@ import { defineComponent, ref, onMounted, onUnmounted } from "vue"; import draggify from "@/modules/draggify"; -import TextInput from "@/components/inputItem/textInput.vue"; +import TextInput from "@/components/textInput.vue"; + import useTextInputController from - "@/components/inputItem/controllers/textCtrl"; + "@/components/controllers/textCtrl"; import useAudioInputController from - "@/components/inputItem/controllers/audioCtrl"; + "@/components/controllers/audioCtrl"; export default defineComponent({ name: "InputItem", + + props: ["initials"], + components: { TextInput }, + setup() { - // Mark input item with user's initials. - const initials = ref("") - - // Set initials. - const initialData = window.localStorage.getItem("initials") - if (initialData) initials.value = initialData - // Default values for position. const xStart = 15; const yStart = window.innerHeight - 200; @@ -57,25 +55,10 @@ export default defineComponent({ const { typing } = useTextInputController(elementX) const { recording } = useAudioInputController(typing) - // Update profile functionality. - const onUpdateProfile = (_event: any, payload: any) => { - initials.value = payload.message.first[0] + payload.message.last[0] - window.localStorage.setItem("initials", initials.value) - } - - onMounted(() => { - window.ipcRenderer.on("update-profile", onUpdateProfile); - }) - - onUnmounted(() => { - window.ipcRenderer.removeAllListeners("update-profile"); - }) - return { elementX, elementY, - recording, - initials + recording }; }, }); diff --git a/src/components/login.vue b/src/components/login.vue index 8382179..95cf62f 100644 --- a/src/components/login.vue +++ b/src/components/login.vue @@ -29,13 +29,12 @@ - + \ No newline at end of file + .undoButton { + position: absolute; + + bottom: 0px; + right: 0px; + + opacity: 0; + + border: none; + outline: none; + + margin: 0px; + padding: 0px; + + background-color: Transparent; + + font-family: "SF Compact Display"; + font-size: 12px; + font-weight: bold; + color: #9B9B9B; + + animation-name: undo-anim; + animation-duration: 3s; +} + +.undoButton:hover { + cursor: pointer; +} + +@keyframes undo-anim { + 0%, 90% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + + + \ No newline at end of file diff --git a/src/modules/emitter.ts b/src/modules/emitter.ts index 14aa77f..bc91693 100644 --- a/src/modules/emitter.ts +++ b/src/modules/emitter.ts @@ -1,4 +1,11 @@ /* eslint-disable */ + +import { Emitter } from "mitt"; + +// Backend emitter + +type Mitt = Emitter; + const EventEmitter = require('events'); class BackgroundMitt extends EventEmitter { } diff --git a/src/modules/message.ts b/src/modules/message.ts index 9601f95..80be1dd 100644 --- a/src/modules/message.ts +++ b/src/modules/message.ts @@ -1,4 +1,4 @@ -import { RenderMessage, ClientMessage, AuthRequest, LogoutRequest } from "@/types/message/index"; +import { RenderMessage, ClientMessage, ClientRequest, AuthRequest, LogoutRequest } from "@/types"; import { v4 as uuidv4 } from 'uuid'; function getTimeStamp(): number { @@ -31,6 +31,15 @@ export const clientMessage = (text: string, audio: string | boolean, uid: string } ) +export const clientRequest = (intent: string, params: object, epic: string | boolean): ClientRequest => ( + { + intent: intent, + params: params, + epic: epic, + confidence: 1.0 + } +) + export const authRequest = (key: boolean | string, usr: boolean | string, pwd: boolean | string): AuthRequest => ( { key, diff --git a/src/modules/messages.ts b/src/modules/messages.ts index 0b89aaa..1d2c4b4 100644 --- a/src/modules/messages.ts +++ b/src/modules/messages.ts @@ -1,6 +1,6 @@ import { ref } from 'vue'; import useScroll from "@/modules/scroll"; -import { RenderMessage, Annotation } from "@/types/message/index"; +import { RenderMessage, Annotation } from "@/types"; const messages = ref(new Map()); diff --git a/src/modules/mitt.ts b/src/modules/mitt.ts index 9804d0b..4f3f448 100644 --- a/src/modules/mitt.ts +++ b/src/modules/mitt.ts @@ -1,5 +1,9 @@ import { inject } from "vue"; -import { Mitt } from "@/types/mitt/index"; +import { Emitter } from "mitt"; + +// Frontend emitter + +type Mitt = Emitter; let emitter: Mitt; diff --git a/src/types/message/index.ts b/src/types.ts similarity index 90% rename from src/types/message/index.ts rename to src/types.ts index eaf3845..d1826ee 100644 --- a/src/types/message/index.ts +++ b/src/types.ts @@ -18,6 +18,13 @@ export interface ClientMessage { uid: string; } +export interface ClientRequest { + intent: string, + params: object, + epic: string | boolean, + confidence: number +} + export interface SessionState { key: string | boolean; newMessages: RenderMessage[]; diff --git a/src/types/mitt/index.ts b/src/types/mitt/index.ts deleted file mode 100644 index a1ba637..0000000 --- a/src/types/mitt/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Emitter } from "mitt"; - -export type Mitt = Emitter; diff --git a/window.json b/window.json index 16e3948..a91e611 100644 --- a/window.json +++ b/window.json @@ -1 +1 @@ -{"width":386,"height":815,"x":1720,"y":495} \ No newline at end of file +{"width":609,"height":721,"x":1376,"y":517} \ No newline at end of file From 7d97756d600c94b996339e538711c334f74130aa Mon Sep 17 00:00:00 2001 From: Andrew Gundersen Date: Fri, 9 Apr 2021 08:30:34 -0500 Subject: [PATCH 084/163] minor enhancements (e.g. idk what else to say) --- session.json | 2 +- src/components/controllers/audioCtrl.ts | 13 +++- src/components/controllers/textCtrl.ts | 10 ++- src/components/message.vue | 98 ++----------------------- src/modules/message.ts | 9 ++- src/types.ts | 8 +- window.json | 2 +- 7 files changed, 37 insertions(+), 105 deletions(-) diff --git a/session.json b/session.json index 688a9c4..c5383dd 100644 --- a/session.json +++ b/session.json @@ -1 +1 @@ -{"key":"6cde67f6-6188-452d-a54e-9d38cabf2d63","newMessages":[]} \ No newline at end of file +{"key":"1099b966-d9e5-4f1a-876e-13535af32f26","newMessages":[]} \ No newline at end of file diff --git a/src/components/controllers/audioCtrl.ts b/src/components/controllers/audioCtrl.ts index 8a73868..c9efaa9 100644 --- a/src/components/controllers/audioCtrl.ts +++ b/src/components/controllers/audioCtrl.ts @@ -1,7 +1,7 @@ import anime from "animejs"; -import { onMounted, onUnmounted, ref, Ref } from "vue"; import useMitt from "@/modules/mitt"; import { useIpc } from '@/modules/ipc'; +import { onMounted, onUnmounted, ref, Ref } from "vue"; import keyboardNameMap from "../keyBoardMaps/keyboardNameMap"; import { renderMessage, clientMessage } from '@/modules/message'; @@ -65,8 +65,15 @@ export default function useAudioInputController (typing: Ref) { // Stop recording on space up. if (cmd == "SPACE" && recording.value) { - // Render audio immediately. - const message = renderMessage("", "", "", "sf") + // Create a message. + const message = renderMessage( + "", + "", + "", + "sf" + ) + + // Render it immediately. emitter.emit("self-message", message); // Stop recording and get audio from recorder. diff --git a/src/components/controllers/textCtrl.ts b/src/components/controllers/textCtrl.ts index 18d686a..7a2e2e8 100644 --- a/src/components/controllers/textCtrl.ts +++ b/src/components/controllers/textCtrl.ts @@ -85,8 +85,14 @@ export default function useTextInputController(elementX: Ref) { const sendMessage = () => { if (textInput) { - // Render message - const message = renderMessage(textInput.value, false, "", "sf") + // Create the message. + const message = renderMessage( + textInput.value, + false, + "", + "sf" + ) + emitter.emit("self-message", message); // Send it to the backend for processing. diff --git a/src/components/message.vue b/src/components/message.vue index 41e039e..e69f29c 100644 --- a/src/components/message.vue +++ b/src/components/message.vue @@ -11,7 +11,7 @@
new session
@@ -23,14 +23,6 @@ class="messageBox" > - - -
@@ -98,9 +89,7 @@ - - diff --git a/src/api/account.ts b/src/api/account.ts index 1d72822..de1335b 100644 --- a/src/api/account.ts +++ b/src/api/account.ts @@ -1,27 +1,14 @@ -import { useHttp } from "@/modules/http"; +import { useHttp } from "@/composables/http"; import axios from "axios"; const { post } = useHttp(); -export const submit = - async (email: string, password: string) => ( - - await post('/account/login', { - email, - password - }) - +export const submit = async (email: string, password: string) => ( + await post('/account/login', { email, password }) ) - -export const logout = - async (): Promise => (await post('/account/logout')); - - - -export const fetchProfile = async(email: string, token: string) => ( - +export const fetchAccount = async (email: string, token: string) => ( await axios({ url: "http://127.0.0.1:3000/api/account/profile", headers: { diff --git a/src/assets/crimata.svg b/src/assets/crimata.svg deleted file mode 100644 index 8bb28dc..0000000 --- a/src/assets/crimata.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/audio.ts b/src/audio.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/background.ts b/src/background.ts deleted file mode 100644 index b67e829..0000000 --- a/src/background.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Entry point for Crimata electron app. - * "Look on my Works, ye Mighty, and despair!" - */ - -"use strict"; - -import { initApp } from './background/init'; -import { protocol } from "electron"; -require('dotenv').config() - -// Scheme must be registered before the app is ready -protocol.registerSchemesAsPrivileged([ - { scheme: "app", privileges: { secure: true, standard: true } } -]); - -// Load environment variable -const isDev = require('electron-is-dev'); - -// NOTE Program Begins Here -(() => { - - console.log('Starting Crimata electron app.'); - initApp(isDev); - -})(); diff --git a/src/background/audio.ts b/src/background/audio.ts deleted file mode 100644 index fa6efb2..0000000 --- a/src/background/audio.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* eslint @typescript-eslint/no-var-requires: "off" */ - -"use strict"; - -import { backgroundMitt } from '@/modules/emitter'; -const portAudio = require('naudiodon'); - -// Audio in and out stream objects. -let ai: typeof portAudio.AudioIO | boolean = false; -let ao: typeof portAudio.AudioIO | boolean = false; - -// Whether activly recording. -let record = false; - -const audioContainer = { - input: '', -} - -const audioOptions = { - channelCount: 1, - sampleFormat: 16, - sampleRate: 16000, - deviceId: -1, - closeOnError: false, -} - -export const toggleRecord = (): void => { record = !record }; - - -export const fetchAudioInput = (): Promise => ( - - new Promise((resolve, reject) => { - - try { - resolve(audioContainer.input); - toggleRecord(); - } catch (e) { - reject(new Error('Failed to fetch the audio.')) - } - - }) -) - - -// Main audio function run by run.ts module. -export function initAudioIO(): void { - console.log("AUDIO:Starting io streams.") - - if (!ai) { - - // Initialize and start input stream. - ai = new portAudio.AudioIO({ inOptions: audioOptions }); - ai.setEncoding("hex"); - ai.start(); - - // On each data chunk... - ai.on('data', (chunk: string) => { - - // If recording, we capture the data. - if (record) { - console.log('AUDIO:Recording...') - audioContainer.input += chunk; - } - - // Else, we don't capture and also clear audioContainer. - else { - if (audioContainer.input.length) { - audioContainer.input = ""; - } - } - - }); - } - - if (!ao) { - - // Initialize and start input stream. - ao = new portAudio.AudioIO({ outOptions: audioOptions }); - ao.start(); - - } -} - - -// ---Audio playback-------------------------------------------- - -// Split Buffer into an array of len-sized Buffers. -function bufSplit(buf: Buffer, len: number): Array { - const chunks = []; - let i = 0; - let L = len; - - while(i < buf.byteLength) { - chunks.push(buf.slice(i, L)); - i = L; - L += len; - } - - return chunks; -} - -// Audio playback. -export function play(input: string): void { - - // Format the audio. - const audio = bufSplit( - Buffer.from(input as string, 'hex'), - 8192 - ); - - // Called on end of write. - const callback = () => { - - // We stop audio playback anim. - backgroundMitt.emit('ipc-renderer', { - endpoint: 'stop-playback-anim' - }); - - } - - write(); - - // Iterate through audio array and write buffers to portAudio writable. - function write() { - let chunk: Buffer; - let ok = true; - let i = 0; - - do { - chunk = audio[i]; - if (i === audio.length - 1) { - // write last chunk. - ao.write(chunk, null, callback); - } else { - // check for backpreassure. - ok = ao.write(chunk, null); - } - i++; - } while (i < audio.length && ok); - - if (i < audio.length) { - // Had to stop early! - // Write some more once it drains. - ao.once('drain', write); - } - } -} - -// ------------------------------------------------------------- - -// Get's called on window close. -export async function stopStream() { - console.log("AUDIO:Stopping audio stream.") - if (ai) { - try { - await ai.quit() - } catch(e){ - console.log('AUDIO: Failed to shutdown audio input.'); - throw e; - } - } - if (ao) { - try { - await ao.quit() - } catch(e){ - console.log('AUDIO: Failed to shutdown audio output.'); - throw e; - } - } -} - - diff --git a/src/background/helpers.ts b/src/background/helpers.ts deleted file mode 100644 index f0074fb..0000000 --- a/src/background/helpers.ts +++ /dev/null @@ -1,62 +0,0 @@ -import fs from 'fs'; -import { backgroundMitt } from "@/modules/emitter"; -import { SessionState, WindowState } from "@/types"; -import { app } from "electron"; - -const configPath = app.getPath("userData"); - -export const ipcEmit = (channel: string, payload: any) => { - backgroundMitt.emit('ipc-renderer', { - endpoint: channel, - message: payload - }); -} - -export const loadState = (fileName: string): SessionState => { - let state: SessionState; - - try { - state = JSON.parse(fs.readFileSync(configPath + fileName).toString()); - } - - catch (error) { - state = { - key: false, - newMessages: [] - } - } - - return state - -} - -export const loadWinState = (fileName: string): WindowState => { - let state: WindowState; - - try { - state = JSON.parse(fs.readFileSync(configPath + fileName).toString()); - } - - catch (error) { - state = { - width: 600, - height: 500, - x: null, - y: null, - } - } - - return state - -} - -// Save session or window state. -export const saveToJson = (fileName: string, data: any) => { - - fs.writeFile(configPath + fileName, JSON.stringify(data), (err) => { - if (err) { - console.log("Error when saving to json.") - } - }) - -} \ No newline at end of file diff --git a/src/background/init.ts b/src/background/init.ts deleted file mode 100644 index 1f47214..0000000 --- a/src/background/init.ts +++ /dev/null @@ -1,89 +0,0 @@ - -"use strict"; - -import { app, dialog } from "electron"; -import { createWindow } from './window'; -import { stopStream } from './audio'; -import { backgroundMitt } from '@/modules/emitter'; -import useIpc from "@/background/ipc/index"; -const { autoUpdater } = require('electron-updater'); - -let win: boolean; - -// Listen for window creation. -backgroundMitt.on('window-active', (state: boolean) => { - win = state; -}); - -// Auto updating. -autoUpdater.requestHeaders = { 'PRIVATE-TOKEN': 'mvvgWYwWnot4bisiQMh_' } - -autoUpdater.on('update-available', (info: any) => { - console.log(`Update available: ${info.version}`) -}) - -autoUpdater.on('update-downloaded', (info: any) => { - - const updateDialog = { - type: 'info', - buttons: ['Restart', 'Later'], - title: 'Application Update', - message: info.version, - detail: 'A new version has been downloaded. Restart the application to apply the updates.' - } - - dialog.showMessageBox(updateDialog).then((returnValue) => { - if (returnValue.response === 0) autoUpdater.quitAndInstall() - }) - -}) - - - -// Run when electron app is initialized. -async function main(): Promise { - - console.log("MAIN:Initializing Electron App."); - - useIpc(); - - // Must wait til window is created. - await createWindow(); - -} - -// Root function of app. -export function initApp(dev: boolean): void { - - // On initial startup. - app.on("ready", () => { - // autoUpdater.checkForUpdates() - main(); - }); - - // Must keep to ensure app doesn't quit on close. - app.on("before-quit", async () => { - await stopStream(); - }); - - // Must keep to ensure app doesn't quit on close. - app.on("window-all-closed", () => { - }); - - // When user clicks app icon (re-open) - app.on("activate", () => { - - if (!win) { - createWindow(); - } - - }); - - // Exit cleanly on request from parent process in development mode. - if (dev) { - process.on("SIGTERM", () => { - app.quit(); - }); - } -} - diff --git a/src/background/ipc/audio.ts b/src/background/ipc/audio.ts deleted file mode 100644 index 1419273..0000000 --- a/src/background/ipc/audio.ts +++ /dev/null @@ -1,40 +0,0 @@ - -"use strict"; - -import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron"; -import { fetchAudioInput, toggleRecord } from "@/background/audio"; - - -// Toggles record to true to begin capturing chunks. -const onRecordingStart = ( - _event: IpcMainEvent, - _payload: null -): void => { - - console.log('[IPC]: start-recording'); - - toggleRecord(); -}; - - -// Returns recorded audio to frontend and sets record to false. -const onRecordingStop = async ( - _event: IpcMainInvokeEvent, - _payload: null -): Promise => { - - console.log('[IPC]: stop-recording'); - - return await fetchAudioInput() -}; - - -export default function useAudioListeners(): void { - - ipcMain.removeAllListeners("start-recording"); - ipcMain.on("start-recording", onRecordingStart); - - ipcMain.removeHandler("stop-recording"); - ipcMain.handle("stop-recording", onRecordingStop); - -} diff --git a/src/background/ipc/session.ts b/src/background/ipc/session.ts deleted file mode 100644 index cc6edeb..0000000 --- a/src/background/ipc/session.ts +++ /dev/null @@ -1,70 +0,0 @@ - -"use strict"; - -import { initSession, emitNewMessages, sendMessage } from '@/background/session'; -import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron"; -import { initAudioIO } from "@/background/audio"; -import { ClientMessage } from "@/types"; -import { store } from "@/background/store"; - - -// Instantiate socket session with crimata-platorm. -const onSessionInit = ( - _event: IpcMainInvokeEvent, - cid: string -): void => { - - console.log('[IPC]: init-session'); - - const token = store.get('key'); - const crimataId = store.get('crimataId'); - - - initSession({ - token, - crimataId - }); - - initAudioIO(); -} - - -const onAppMounted = ( - _event: IpcMainInvokeEvent, - _payload: null -): void => { - - console.log('[IPC]: app-mounted'); - - emitNewMessages() -}; - - -// Handle messages from window/client. -const onClientMessage = ( - _event: IpcMainEvent, - payload: ClientMessage -): void => { - - console.log('[IPC]: client-message'); - - sendMessage(payload); -} - - -export default function useSessionListeners(): void { - - console.log('[IPC]: Init session listeners.'); - - // Attach listeners for frontend. - ipcMain.removeAllListeners("client-message"); - ipcMain.on("client-message", onClientMessage); - - // Attack browser window init listener. - ipcMain.removeAllListeners("app-mounted"); - ipcMain.on("app-mounted", onAppMounted); - - ipcMain.removeAllListeners("init-session"); - ipcMain.on("init-session", onSessionInit); - -} diff --git a/src/background/session.ts b/src/background/session.ts deleted file mode 100644 index ebfbc26..0000000 --- a/src/background/session.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Creates a websocket session with Crimata Servers. - * - * Connects to Servers and attempts key authentication. Server will respond - * with key and user profile. We send the profile to the browser. We also - * resend this information on new broser window. We then serve as a - * communication interface between the window and the servers. It will - * automatically try to reconnect on websocket disconnect. - */ - -import { backgroundMitt } from "@/modules/emitter"; -import { ipcEmit, loadState } from './helpers'; -import WebSocket from 'ws'; - -import useWebSockets from "./websockets"; - -import { play } from "./audio"; -import { renderMessage } from "@/modules/message"; -import { SessionState } from "@/types"; - -let win = true; - -// Info saved to json on quit (key, newMessages). -let state: SessionState; - -let socket: WebSocket | null = null; - - -// Calls appropriate endpoint for a server message. -const onMessage = (data: string): void => { - let message = JSON.parse(data); - if (message === "CLOSE_AUTH_FAIL") { - ipcEmit("session-auth-fail", null) - return; - } - - // Standard message. - if (message.content) { - - // Convert to render message - message = renderMessage( - message.content.text, - message.content.audio, - message.context, - message.modifier - ); - - if (win) { - console.log("SESS:Emitting standard message.") - if (message.audio) { - play(message.audio) - } - ipcEmit("render-message", message) - } - - else { - console.log("SESS:No window: saving message.") - state.newMessages.push(message); - } - } - - else { - if (win) { - ipcEmit("render-message", message) - } - } - -}; - - -// Websockets module. -const { createSocket, send } = useWebSockets(onMessage); - - -export const emitNewMessages = (): void => { - if (state) { - ipcEmit("update-state", { - newMessages: state.newMessages, - }); - } - -} - - -export const sendMessage = (payload: Record): void => { - try { - send(payload) - } catch(e) { - console.log("Unable to send message: ", payload); - } - -} - -export const endSession = (): void => { - if (socket) { - socket.close(); - socket = null; - } -} - - -// Call this to initialize session with Crimata servers. -export const initSession = (authPayload: { - token: string; - crimataId: string; -}): void => { - console.log("SESS:Creating new session.") - - // Load Json or createState. - state = loadState("session.json"); - - // Open socket connection. - if (!socket) - socket = createSocket(authPayload); - - // Keep win up-to-date. - backgroundMitt.on('window-active', (state: boolean) => { - win = state; - }); - -} diff --git a/src/background/websockets.ts b/src/background/websockets.ts deleted file mode 100644 index 36eae6f..0000000 --- a/src/background/websockets.ts +++ /dev/null @@ -1,96 +0,0 @@ - -"use strict"; - -import WebSocket from 'ws'; - -let socket: WebSocket; - -const socketUrl = "ws://127.0.0.1:8760" - -// Run every time we want to connect to backend. -export default function useWebSockets( - receiveCallback: (s: string) => void, - openCallback?: () => void -) { - - // Returns bool (sucess or fail). - const sendMessage = (data: any) => { - console.log("WS:Sending message: ", data) - - if (socket.readyState !== 1) { - return false - } - - else { - socket.send(JSON.stringify(data)) - return true - } - - } - - const send = async (data: Record): Promise => ( - new Promise((resolve, reject) => { - if (socket.readyState !== 1) { - reject(false); - } - socket.send(JSON.stringify(data)) - resolve(true); - - })) - - - const onOpen = (_event: WebSocket.OpenEvent) => { - - console.log("WS:Connected to WS Server!"); - if (openCallback) openCallback(); - - } - - const onServerMessage = (event: WebSocket.MessageEvent) => { - - console.log("WS:Message received: ", event.data); - - receiveCallback(event.data.toString()) - - } - - const onClose = (event: WebSocket.CloseEvent) => { - console.log("WS:Socket closed normally.") - } - - // Reconnect automatically on error. - const onError = (event: WebSocket.ErrorEvent) => { - console.log("WS:WebSocket error: ", event.message); - - console.log("Attempting reconnect in 1s.") - setTimeout(createSocket, 1000) - } - - const createSocket = (authPayload: { - token: string; - crimataId: string; - }):WebSocket => { - socket = new WebSocket(socketUrl) - - // Add listeners. - socket.addEventListener("open", (_event: WebSocket.OpenEvent) => { - socket.send(JSON.stringify({ - key: authPayload.token, - crimata_id: authPayload.crimataId - })); - }) - socket.addEventListener("message", onServerMessage) - socket.addEventListener("close", onClose) - socket.addEventListener("error", onError) - - return socket; - - } - - return { - createSocket, - sendMessage, - send - } - -} diff --git a/src/components/controllers/audioCtrl.ts b/src/components/controllers/audioCtrl.ts deleted file mode 100644 index f635c0b..0000000 --- a/src/components/controllers/audioCtrl.ts +++ /dev/null @@ -1,118 +0,0 @@ -import anime from "animejs"; -import useMitt from "@/modules/mitt"; -import { useIpc } from '@/modules/ipc'; -import { onMounted, onUnmounted, ref, Ref } from "vue"; -import keyboardNameMap from "../keyBoardMaps/keyboardNameMap"; -import { renderMessage, clientMessage } from '@/modules/message'; -import { postMessage } from "@/ipcRend/session"; -import { invokeStopRecord } from "@/ipcRend/audio"; - - -function showRecIcon () { - - anime({ - targets: '#recIcon', - opacity: [0, 0.75], - scale: [0.0, 1], - duration: 250, - easing: 'linear', - }) - -} - -function hideRecIcon () { - - anime({ - targets: '#recIcon', - opacity: [0.75, 0], - scale: [1, 0], - duration: 250, - easing: 'linear', - }) - -} - - -export default function useAudioInputController (typing: Ref) { - - // For sending messages. - const { post, invoke } = useIpc(); - const { emitter } = useMitt(); - - // Keepp track of when we are recording. - const recording = ref(false); - - //---Callbacks----------------------------------------------- - - const onKeyDown = (e: KeyboardEvent) => { - const cmd = keyboardNameMap[e.keyCode]; - // console.log(cmd) - - // Start recording on space bar. - if (cmd == "SPACE" && !recording.value && !typing.value) { - - console.log("INPT:Starting record.") - post("start-recording", null); - - showRecIcon() - recording.value = true; - - } - - } - - const onKeyUp = async (e: KeyboardEvent) => { - const cmd = keyboardNameMap[e.keyCode]; - - // Stop recording on space up. - if (cmd == "SPACE" && recording.value) { - - // Create a message. - const message = renderMessage( - "", - "", - "", - "sf" - ) - - // Render it immediately. - emitter.emit("self-message", message); - - // Stop recording and get audio from recorder. - console.log("INPT:Stopping record.") - try { - const audio = await invokeStopRecord() as string; - // Send message to the backend for processing. - const clientM = clientMessage("", audio, message.uid); - postMessage(clientM); - } catch(e) { - console.log('Failed to fetch audio.') - } finally { - hideRecIcon(); - recording.value = false; - } - - - - } - - } - - //----------------------------------------------------------- - - onMounted(() => { - window.addEventListener("keydown", onKeyDown); - window.addEventListener("keyup", onKeyUp); - }) - - onUnmounted(() => { - window.removeEventListener("keydown", onKeyDown); - window.removeEventListener("keyup", onKeyUp); - }); - - - return { - recording - } - -} diff --git a/src/components/keyBoardMaps/keyboardCharMap.ts b/src/components/keyBoardMaps/keyboardCharMap.ts deleted file mode 100644 index 102e9c3..0000000 --- a/src/components/keyBoardMaps/keyboardCharMap.ts +++ /dev/null @@ -1,263 +0,0 @@ -// This has the UnShifted and Shifted characters that each key maps to -// Ones that are to be ignored for character input are empty. -const keyboardCharMap = [ - ["", ""], // [0] - ["", ""], // [1] - ["", ""], // [2] - ["", ""], // [3] - ["", ""], // [4] - ["", ""], // [5] - ["", ""], // [6] - ["", ""], // [7] - ["", ""], // [8] - ["", ""], // [9] - ["", ""], // [10] - ["", ""], // [11] - ["", ""], // [12] - ["\r", "\r"], // [13] - MOST control characters are ignored. This one (Carriage Return, or "Enter") is significant! - ["", ""], // [14] - ["", ""], // [15] - ["", ""], // [16] - ["", ""], // [17] - ["", ""], // [18] - ["", ""], // [19] - ["", ""], // [20] - ["", ""], // [21] - ["", ""], // [22] - ["", ""], // [23] - ["", ""], // [24] - ["", ""], // [25] - ["", ""], // [26] - ["", ""], // [27] - ["", ""], // [28] - ["", ""], // [29] - ["", ""], // [30] - ["", ""], // [31] - [" ", " "], // [32] // SPACE! Don't "clean it up" and remove the space! - ["", ""], // [33] - ["", ""], // [34] - ["", ""], // [35] - ["", ""], // [36] - ["", ""], // [37] - ["", ""], // [38] - ["", ""], // [39] - ["", ""], // [40] - ["", ""], // [41] - ["", ""], // [42] - ["", ""], // [43] - ["", ""], // [44] - ["", ""], // [45] - ["", ""], // [46] - ["", ""], // [47] - ["0", ")"], // [48] - ["1", "!"], // [49] - ["2", "@"], // [50] - ["3", "#"], // [51] - ["4", "$"], // [52] - ["5", "%"], // [53] - ["6", "^"], // [54] - ["7", "&"], // [55] - ["8", "*"], // [56] - ["9", "("], // [57] - ["", ""], // [58] - [";", ":"], // [59] - ["<", ""], // [60] - ["=", ""], // [61] - [">", ""], // [62] - ["?", ""], // [63] shifted; else "/" - ["", ""], // [64] - ["a", "A"], // [65] - ["b", "B"], // [66] - ["c", "C"], // [67] - ["d", "D"], // [68] - ["e", "E"], // [69] - ["f", "F"], // [70] - ["g", "G"], // [71] - ["h", "H"], // [72] - ["i", "I"], // [73] - ["j", "J"], // [74] - ["k", "K"], // [75] - ["l", "L"], // [76] - ["m", "M"], // [77] - ["n", "N"], // [78] - ["o", "O"], // [79] - ["p", "P"], // [80] - ["q", "Q"], // [81] - ["r", "R"], // [82] - ["s", "S"], // [83] - ["t", "T"], // [84] - ["u", "U"], // [85] - ["v", "V"], // [86] - ["w", "W"], // [87] - ["x", "X"], // [88] - ["y", "Y"], // [89] - ["z", "Z"], // [90] - ["", ""], // [91] Windows Key (Windows) or Command Key (Mac) - ["", ""], // [92] - ["", ""], // [93] - ["", ""], // [94] - ["", ""], // [95] - // Number Keypad Entries... - ["0", ""], // [96] - ["1", ""], // [97] - ["2", ""], // [98] - ["3", ""], // [99] - ["4", ""], // [100] - ["5", ""], // [101] - ["6", ""], // [102] - ["7", ""], // [103] - ["8", ""], // [104] - ["9", ""], // [105] - ["*", ""], // [106] - ["+", ""], // [107] - ["", ""], // [108] - ["-", ""], // [109] - [".", ""], // [110] - ["/", ""], // [111] - - ["", ""], // [112] - ["", ""], // [113] - ["", ""], // [114] - ["", ""], // [115] - ["", ""], // [116] - ["", ""], // [117] - ["", ""], // [118] - ["", ""], // [119] - ["", ""], // [120] - ["", ""], // [121] - ["", ""], // [122] - ["", ""], // [123] - ["", ""], // [124] - ["", ""], // [125] - ["", ""], // [126] - ["", ""], // [127] - ["", ""], // [128] - ["", ""], // [129] - ["", ""], // [130] - ["", ""], // [131] - ["", ""], // [132] - ["", ""], // [133] - ["", ""], // [134] - ["", ""], // [135] - ["", ""], // [136] - ["", ""], // [137] - ["", ""], // [138] - ["", ""], // [139] - ["", ""], // [140] - ["", ""], // [141] - ["", ""], // [142] - ["", ""], // [143] - ["", ""], // [144] - ["", ""], // [145] - ["", ""], // [146] - ["", ""], // [147] - ["", ""], // [148] - ["", ""], // [149] - ["", ""], // [150] - ["", ""], // [151] - ["", ""], // [152] - ["", ""], // [153] - ["", ""], // [154] - ["", ""], // [155] - ["", ""], // [156] - ["", ""], // [157] - ["", ""], // [158] - ["", ""], // [159] - ["", ""], // [160] - ["", ""], // [161] - ["", ""], // [162] - ["", ""], // [163] - ["", ""], // [164] - ["", ""], // [165] - ["", ""], // [166] - ["", ""], // [167] - ["", ""], // [168] - ["", ""], // [169] - ["", ""], // [170] - ["", ""], // [171] - ["", ""], // [172] - ["", ""], // [173] - ["", ""], // [174] - ["", ""], // [175] - ["", ""], // [176] - ["", ""], // [177] - ["", ""], // [178] - ["", ""], // [179] - ["", ""], // [180] - ["", ""], // [181] - ["", ""], // [182] - ["", ""], // [183] - ["", ""], // [184] - ["", ""], // [185] - [";", ":"], // [186] - ["=", "+"], // [187] - [",", "<"], // [188] - ["-", "_"], // [189] - [".", ">"], // [190] - ["/", "?"], // [191] - ["`", "~"], // [192] - ["", ""], // [193] - ["", ""], // [194] - ["", ""], // [195] - ["", ""], // [196] - ["", ""], // [197] - ["", ""], // [198] - ["", ""], // [199] - ["", ""], // [200] - ["", ""], // [201] - ["", ""], // [202] - ["", ""], // [203] - ["", ""], // [204] - ["", ""], // [205] - ["", ""], // [206] - ["", ""], // [207] - ["", ""], // [208] - ["", ""], // [209] - ["", ""], // [210] - ["", ""], // [211] - ["", ""], // [212] - ["", ""], // [213] - ["", ""], // [214] - ["", ""], // [215] - ["", ""], // [216] - ["", ""], // [217] - ["", ""], // [218] - ["[", "{"], // [219] - ["\\", "|"], // [220] - ["]", "}"], // [221] - ["'", '"'], // [222] - ["", ""], // [223] - ["", ""], // [224] - ["", ""], // [225] - ["", ""], // [226] - ["", ""], // [227] - ["", ""], // [228] - ["", ""], // [229] - ["", ""], // [230] - ["", ""], // [231] - ["", ""], // [232] - ["", ""], // [233] - ["", ""], // [234] - ["", ""], // [235] - ["", ""], // [236] - ["", ""], // [237] - ["", ""], // [238] - ["", ""], // [239] - ["", ""], // [240] - ["", ""], // [241] - ["", ""], // [242] - ["", ""], // [243] - ["", ""], // [244] - ["", ""], // [245] - ["", ""], // [246] - ["", ""], // [247] - ["", ""], // [248] - ["", ""], // [249] - ["", ""], // [250] - ["", ""], // [251] - ["", ""], // [252] - ["", ""], // [253] - ["", ""], // [254] - ["", ""] // [255] -]; -export default keyboardCharMap; diff --git a/src/components/keyBoardMaps/keyboardNameMap.ts b/src/components/keyBoardMaps/keyboardNameMap.ts deleted file mode 100644 index 8f385ae..0000000 --- a/src/components/keyBoardMaps/keyboardNameMap.ts +++ /dev/null @@ -1,261 +0,0 @@ -// names of known key codes (0-255) -const keyboardNameMap = [ - "", // [0] - "", // [1] - "", // [2] - "CANCEL", // [3] - "", // [4] - "", // [5] - "HELP", // [6] - "", // [7] - "BACK_SPACE", // [8] - "TAB", // [9] - "", // [10] - "", // [11] - "CLEAR", // [12] - "ENTER", // [13] - "ENTER_SPECIAL", // [14] - "", // [15] - "SHIFT", // [16] - "CONTROL", // [17] - "ALT", // [18] - "PAUSE", // [19] - "CAPS_LOCK", // [20] - "KANA", // [21] - "EISU", // [22] - "JUNJA", // [23] - "FINAL", // [24] - "HANJA", // [25] - "", // [26] - "ESCAPE", // [27] - "CONVERT", // [28] - "NONCONVERT", // [29] - "ACCEPT", // [30] - "MODECHANGE", // [31] - "SPACE", // [32] - "PAGE_UP", // [33] - "PAGE_DOWN", // [34] - "END", // [35] - "HOME", // [36] - "LEFT", // [37] - "UP", // [38] - "RIGHT", // [39] - "DOWN", // [40] - "SELECT", // [41] - "PRINT", // [42] - "EXECUTE", // [43] - "PRINTSCREEN", // [44] - "INSERT", // [45] - "DELETE", // [46] - "", // [47] - "0", // [48] - "1", // [49] - "2", // [50] - "3", // [51] - "4", // [52] - "5", // [53] - "6", // [54] - "7", // [55] - "8", // [56] - "9", // [57] - "COLON", // [58] - "SEMICOLON", // [59] - "LESS_THAN", // [60] - "EQUALS", // [61] - "GREATER_THAN", // [62] - "QUESTION_MARK", // [63] - "AT", // [64] - "A", // [65] - "B", // [66] - "C", // [67] - "D", // [68] - "E", // [69] - "F", // [70] - "G", // [71] - "H", // [72] - "I", // [73] - "J", // [74] - "K", // [75] - "L", // [76] - "M", // [77] - "N", // [78] - "O", // [79] - "P", // [80] - "Q", // [81] - "R", // [82] - "S", // [83] - "T", // [84] - "U", // [85] - "V", // [86] - "W", // [87] - "X", // [88] - "Y", // [89] - "Z", // [90] - "OS_KEY", // [91] Windows Key (Windows) or Command Key (Mac) - "", // [92] - "CONTEXT_MENU", // [93] - "", // [94] - "SLEEP", // [95] - "NUMPAD0", // [96] - "NUMPAD1", // [97] - "NUMPAD2", // [98] - "NUMPAD3", // [99] - "NUMPAD4", // [100] - "NUMPAD5", // [101] - "NUMPAD6", // [102] - "NUMPAD7", // [103] - "NUMPAD8", // [104] - "NUMPAD9", // [105] - "MULTIPLY", // [106] - "ADD", // [107] - "SEPARATOR", // [108] - "SUBTRACT", // [109] - "DECIMAL", // [110] - "DIVIDE", // [111] - "F1", // [112] - "F2", // [113] - "F3", // [114] - "F4", // [115] - "F5", // [116] - "F6", // [117] - "F7", // [118] - "F8", // [119] - "F9", // [120] - "F10", // [121] - "F11", // [122] - "F12", // [123] - "F13", // [124] - "F14", // [125] - "F15", // [126] - "F16", // [127] - "F17", // [128] - "F18", // [129] - "F19", // [130] - "F20", // [131] - "F21", // [132] - "F22", // [133] - "F23", // [134] - "F24", // [135] - "", // [136] - "", // [137] - "", // [138] - "", // [139] - "", // [140] - "", // [141] - "", // [142] - "", // [143] - "NUM_LOCK", // [144] - "SCROLL_LOCK", // [145] - "WIN_OEM_FJ_JISHO", // [146] - "WIN_OEM_FJ_MASSHOU", // [147] - "WIN_OEM_FJ_TOUROKU", // [148] - "WIN_OEM_FJ_LOYA", // [149] - "WIN_OEM_FJ_ROYA", // [150] - "", // [151] - "", // [152] - "", // [153] - "", // [154] - "", // [155] - "", // [156] - "", // [157] - "", // [158] - "", // [159] - "CIRCUMFLEX", // [160] - "EXCLAMATION", // [161] - "DOUBLE_QUOTE", // [162] - "HASH", // [163] - "DOLLAR", // [164] - "PERCENT", // [165] - "AMPERSAND", // [166] - "UNDERSCORE", // [167] - "OPEN_PAREN", // [168] - "CLOSE_PAREN", // [169] - "ASTERISK", // [170] - "PLUS", // [171] - "PIPE", // [172] - "HYPHEN_MINUS", // [173] - "OPEN_CURLY_BRACKET", // [174] - "CLOSE_CURLY_BRACKET", // [175] - "TILDE", // [176] - "", // [177] - "", // [178] - "", // [179] - "", // [180] - "VOLUME_MUTE", // [181] - "VOLUME_DOWN", // [182] - "VOLUME_UP", // [183] - "", // [184] - "", // [185] - "SEMICOLON", // [186] - "EQUALS", // [187] - "COMMA", // [188] - "MINUS", // [189] - "PERIOD", // [190] - "SLASH", // [191] - "BACK_QUOTE", // [192] - "", // [193] - "", // [194] - "", // [195] - "", // [196] - "", // [197] - "", // [198] - "", // [199] - "", // [200] - "", // [201] - "", // [202] - "", // [203] - "", // [204] - "", // [205] - "", // [206] - "", // [207] - "", // [208] - "", // [209] - "", // [210] - "", // [211] - "", // [212] - "", // [213] - "", // [214] - "", // [215] - "", // [216] - "", // [217] - "", // [218] - "OPEN_BRACKET", // [219] - "BACK_SLASH", // [220] - "CLOSE_BRACKET", // [221] - "QUOTE", // [222] - "", // [223] - "META", // [224] - "ALTGR", // [225] - "", // [226] - "WIN_ICO_HELP", // [227] - "WIN_ICO_00", // [228] - "", // [229] - "WIN_ICO_CLEAR", // [230] - "", // [231] - "", // [232] - "WIN_OEM_RESET", // [233] - "WIN_OEM_JUMP", // [234] - "WIN_OEM_PA1", // [235] - "WIN_OEM_PA2", // [236] - "WIN_OEM_PA3", // [237] - "WIN_OEM_WSCTRL", // [238] - "WIN_OEM_CUSEL", // [239] - "WIN_OEM_ATTN", // [240] - "WIN_OEM_FINISH", // [241] - "WIN_OEM_COPY", // [242] - "WIN_OEM_AUTO", // [243] - "WIN_OEM_ENLW", // [244] - "WIN_OEM_BACKTAB", // [245] - "ATTN", // [246] - "CRSEL", // [247] - "EXSEL", // [248] - "EREOF", // [249] - "PLAY", // [250] - "ZOOM", // [251] - "", // [252] - "PA1", // [253] - "WIN_OEM_CLEAR", // [254] - "" // [255] -]; - -export default keyboardNameMap; diff --git a/src/components/message.vue b/src/components/message.vue deleted file mode 100644 index e69f29c..0000000 --- a/src/components/message.vue +++ /dev/null @@ -1,419 +0,0 @@ - - - - - diff --git a/src/components/messenger.vue b/src/components/messenger.vue deleted file mode 100644 index f5512a9..0000000 --- a/src/components/messenger.vue +++ /dev/null @@ -1,114 +0,0 @@ - - - - - diff --git a/src/components/textInput.vue b/src/components/textInput.vue deleted file mode 100644 index 45e1af7..0000000 --- a/src/components/textInput.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/composables/audio.ts b/src/composables/audio.ts new file mode 100644 index 0000000..844e791 --- /dev/null +++ b/src/composables/audio.ts @@ -0,0 +1,187 @@ +/* eslint @typescript-eslint/no-var-requires: "off" */ + +"use strict"; + +// where the audio goes +let buffer: ArrayBuffer[] = []; + +// place audio data in buffer +export function collect (chunk: ArrayBuffer) { + buffer.push(chunk); +} + +// return audio and clear buffer +export function flush () { + const bufferCopy = buffer; + buffer = []; + return bufferCopy; +} + +// import { backgroundMitt } from '@/modules/emitter'; +// const portAudio = require('naudiodon'); + +// // Audio in and out stream objects. +// let ai: typeof portAudio.AudioIO | boolean = false; +// let ao: typeof portAudio.AudioIO | boolean = false; + +// // Whether activly recording. +// let record = false; + +// const audioContainer = { +// input: '', +// } + +// const audioOptions = { +// channelCount: 1, +// sampleFormat: 16, +// sampleRate: 16000, +// deviceId: -1, +// closeOnError: false, +// } + +// export const toggleRecord = (): void => { record = !record }; + + +// export const fetchAudioInput = (): Promise => ( + +// new Promise((resolve, reject) => { + +// try { +// resolve(audioContainer.input); +// toggleRecord(); +// } catch (e) { +// reject(new Error('Failed to fetch the audio.')) +// } + +// }) +// ) + + +// // Main audio function run by run.ts module. +// export function initAudioIO(): void { +// console.log("AUDIO:Starting io streams.") + +// if (!ai) { + +// // Initialize and start input stream. +// ai = new portAudio.AudioIO({ inOptions: audioOptions }); +// ai.setEncoding("hex"); +// ai.start(); + +// // On each data chunk... +// ai.on('data', (chunk: string) => { + +// // If recording, we capture the data. +// if (record) { +// console.log('AUDIO:Recording...') +// audioContainer.input += chunk; +// } + +// // Else, we don't capture and also clear audioContainer. +// else { +// if (audioContainer.input.length) { +// audioContainer.input = ""; +// } +// } + +// }); +// } + +// if (!ao) { + +// // Initialize and start input stream. +// ao = new portAudio.AudioIO({ outOptions: audioOptions }); +// ao.start(); + +// } +// } + + +// // ---Audio playback-------------------------------------------- + +// // Split Buffer into an array of len-sized Buffers. +// function bufSplit(buf: Buffer, len: number): Array { +// const chunks = []; +// let i = 0; +// let L = len; + +// while(i < buf.byteLength) { +// chunks.push(buf.slice(i, L)); +// i = L; +// L += len; +// } + +// return chunks; +// } + +// // Audio playback. +// export function play(input: string): void { + +// // Format the audio. +// const audio = bufSplit( +// Buffer.from(input as string, 'hex'), +// 8192 +// ); + +// // Called on end of write. +// const callback = () => { + +// // We stop audio playback anim. +// backgroundMitt.emit('ipc-renderer', { +// endpoint: 'stop-playback-anim' +// }); + +// } + +// write(); + +// // Iterate through audio array and write buffers to portAudio writable. +// function write() { +// let chunk: Buffer; +// let ok = true; +// let i = 0; + +// do { +// chunk = audio[i]; +// if (i === audio.length - 1) { +// // write last chunk. +// ao.write(chunk, null, callback); +// } else { +// // check for backpreassure. +// ok = ao.write(chunk, null); +// } +// i++; +// } while (i < audio.length && ok); + +// if (i < audio.length) { +// // Had to stop early! +// // Write some more once it drains. +// ao.once('drain', write); +// } +// } +// } + +// // ------------------------------------------------------------- + +// // Get's called on window close. +// export async function stopStream() { +// console.log("AUDIO:Stopping audio stream.") +// if (ai) { +// try { +// await ai.quit() +// } catch(e){ +// console.log('AUDIO: Failed to shutdown audio input.'); +// throw e; +// } +// } +// if (ao) { +// try { +// await ao.quit() +// } catch(e){ +// console.log('AUDIO: Failed to shutdown audio output.'); +// throw e; +// } +// } +// } + + diff --git a/src/composables/autoUpdate.ts b/src/composables/autoUpdate.ts new file mode 100644 index 0000000..1a6b780 --- /dev/null +++ b/src/composables/autoUpdate.ts @@ -0,0 +1,31 @@ +const { autoUpdater } = require('electron-updater'); + +let win: boolean; + +// Listen for window creation. +backgroundMitt.on('window-active', (state: boolean) => { + win = state; +}); + +// Auto updating. +autoUpdater.requestHeaders = { 'PRIVATE-TOKEN': 'mvvgWYwWnot4bisiQMh_' } + +autoUpdater.on('update-available', (info: any) => { + console.log(`Update available: ${info.version}`) +}) + +autoUpdater.on('update-downloaded', (info: any) => { + + const updateDialog = { + type: 'info', + buttons: ['Restart', 'Later'], + title: 'Application Update', + message: info.version, + detail: 'A new version has been downloaded. Restart the application to apply the updates.' + } + + dialog.showMessageBox(updateDialog).then((returnValue) => { + if (returnValue.response === 0) autoUpdater.quitAndInstall() + }) + +}) \ No newline at end of file diff --git a/src/modules/emitter.ts b/src/composables/emitter.ts similarity index 51% rename from src/modules/emitter.ts rename to src/composables/emitter.ts index bc91693..ee6bb5e 100644 --- a/src/modules/emitter.ts +++ b/src/composables/emitter.ts @@ -1,13 +1,15 @@ /* eslint-disable */ -import { Emitter } from "mitt"; - // Backend emitter -type Mitt = Emitter; - const EventEmitter = require('events'); - class BackgroundMitt extends EventEmitter { } export const backgroundMitt = new BackgroundMitt(); + +export default function ipcEmit (channel: string, payload: any) { + backgroundMitt.emit('ipc-renderer', { + endpoint: channel, + message: payload + }); +} diff --git a/src/modules/http.ts b/src/composables/http.ts similarity index 100% rename from src/modules/http.ts rename to src/composables/http.ts diff --git a/src/composables/json.ts b/src/composables/json.ts new file mode 100644 index 0000000..43b3804 --- /dev/null +++ b/src/composables/json.ts @@ -0,0 +1,9 @@ +export const saveToJson = (fileName: string, data: any) => { + + fs.writeFile(configPath + fileName, JSON.stringify(data), (err) => { + if (err) { + console.log("Error when saving to json.") + } + }) + +} \ No newline at end of file diff --git a/src/background/store.ts b/src/composables/store.ts similarity index 100% rename from src/background/store.ts rename to src/composables/store.ts diff --git a/src/composables/websockets.ts b/src/composables/websockets.ts new file mode 100644 index 0000000..779f00b --- /dev/null +++ b/src/composables/websockets.ts @@ -0,0 +1,73 @@ + +"use strict"; + +import WebSocket from 'ws'; + +export default function useWebSockets(receiveCallback: (s: string) => void, openCallback?: () => void) { + + let socket: WebSocket | null = null; + + const send = async (data: Record): Promise => { + return new Promise((resolve, reject) => { + if (socket) { + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify(data)); + resolve(true); + } + } + reject(false); + }); + } + + const onOpen = (_event: WebSocket.OpenEvent) => { + console.log("WS:Connected to WS Server!"); + if (openCallback) openCallback(); + } + + const onServerMessage = (event: WebSocket.MessageEvent) => { + console.log("WS:Message received: ", event.data); + receiveCallback(event.data.toString()) + } + + const onClose = (event: WebSocket.CloseEvent) => { + console.log("WS:Socket closed normally.") + } + + // Reconnect automatically on error. + const onError = (event: WebSocket.ErrorEvent) => { + console.log("WS:WebSocket error: ", event.message); + console.log("Attempting reconnect in 1s.") + setTimeout(createSocket, 1000); + } + + const createSocket = (socketUrl: string) => { + + socket = new WebSocket(socketUrl) + + // Add listeners. + socket.addEventListener("open", onOpen); + socket.addEventListener("message", onServerMessage); + socket.addEventListener("close", onClose); + socket.addEventListener("error", onError); + + } + + const close = () => { + if (socket) { + socket.close(); + socket = null; + } + } + + const checkConnection = () => { + return true; + } + + return { + createSocket, + send, + close, + checkConnection + }; + +} diff --git a/src/init.ts b/src/init.ts new file mode 100644 index 0000000..3d0b721 --- /dev/null +++ b/src/init.ts @@ -0,0 +1,46 @@ +/** + * Entry point for Crimata electron app. + * "Look on my Works, ye Mighty, and despair!" + */ + +"use strict"; + +import { app, protocol } from "electron"; +import createWindow from "./window"; +import main from "./main"; + +require('dotenv').config(); + +console.log('Starting Crimata electron app.'); + +// Scheme must be registered before the app is ready +protocol.registerSchemesAsPrivileged([ + { scheme: "app", privileges: { secure: true, standard: true } } +]); + +const isDev = require('electron-is-dev'); + +/* Start main process on ready */ +app.on("ready", async () => { + await main(); +}); + +// Must keep to ensure app doesn't quit on close. +app.on("before-quit", async () => { +}); + +// Must keep to ensure app doesn't quit on close. +app.on("window-all-closed", () => { +}); + +// When user clicks app icon (re-open) +app.on("activate", () => { + if (!win) createWindow(); +}); + +// Exit cleanly on request from parent process in development mode. +if (isDev) { + process.on("SIGTERM", () => { + app.quit(); + }); +} \ No newline at end of file diff --git a/src/background/ipc/account.ts b/src/ipc/account.ts similarity index 84% rename from src/background/ipc/account.ts rename to src/ipc/account.ts index 5898509..3860019 100644 --- a/src/background/ipc/account.ts +++ b/src/ipc/account.ts @@ -1,11 +1,9 @@ "use strict"; -import { Profile } from "@/types"; -import { submit, fetchProfile, logout } from "@/api/account"; +import { submit, fetchProfile, logout } from "../api/account"; import { ipcMain, IpcMainInvokeEvent } from "electron"; -import { store } from "@/background/store"; -import { endSession } from "@/background/session"; +import { store } from "@/composables/store"; const parseAuthRes = (authRes: any) => { @@ -18,7 +16,13 @@ const parseAuthRes = (authRes: any) => { }; -const onProfile = async ( + + + +/** + * Get user profile from store and try to login with it. + */ +const onTokenLogin = async ( _event: IpcMainInvokeEvent, _payload: null ): Promise => ( @@ -33,12 +37,11 @@ const onProfile = async ( // authenticate and fetch profile try { - const res = await fetchProfile( - crimataId, - token - ); - + // attempt login with email token + const res = await fetchProfile(crimataId, token); const parsed = parseAuthRes(res); + + // return profile to renderer resolve(parsed.profile); } catch(e) { @@ -60,6 +63,8 @@ const onLogin = async ( if ( account.password && account.email ) { try { + + // attempt login with email password const res = await submit(account.email, account.password); const parsed = parseAuthRes(res); @@ -67,6 +72,8 @@ const onLogin = async ( store.set('key', parsed.token); store.set('crimataId', parsed.profile.crimataId); + // init session + // return profile to renderer resolve(parsed.profile); @@ -95,7 +102,7 @@ const onLogout = async ( store.delete('crimataId'); // TODO: kill crimata platform session - endSession(); + // endSession(); resolve(); } catch(e) { diff --git a/src/ipc/audio.ts b/src/ipc/audio.ts new file mode 100644 index 0000000..31c16ce --- /dev/null +++ b/src/ipc/audio.ts @@ -0,0 +1,34 @@ + +"use strict"; + +import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron"; +import { collect, flush } from "@/composbales/audio"; + +// handle the new audio data +const onAudioChunk = ( + _e: IpcMainEvent, + payload: ArrayBuffer +) => { + console.log('[IPC]: audio-buffer'); + collect(payload); +} + +// Returns recorded audio to frontend and sets record to false. +const onGetAudio = async ( + _event: IpcMainInvokeEvent, + _payload: null +): Promise => { + console.log('[IPC]: stop-recording'); + return await flush() +}; + + +export default function useAudioListeners(): void { + + ipcMain.removeAllListeners("audio-chunk"); + ipcMain.on("audio-chunk", onAudioChunk); + + ipcMain.removeHandler("get-audio"); + ipcMain.handle("get-audio", onGetAudio); + +} diff --git a/src/background/ipc/index.ts b/src/ipc/index.ts similarity index 74% rename from src/background/ipc/index.ts rename to src/ipc/index.ts index 933bda1..9152ef2 100644 --- a/src/background/ipc/index.ts +++ b/src/ipc/index.ts @@ -3,7 +3,7 @@ import useAccountListeners from "./account"; import useSessionListeners from "./session"; -import useAudioListeners from "./audio"; +// import useAudioListeners from "./audio"; export default function useIpc(): void { @@ -12,6 +12,6 @@ export default function useIpc(): void { useSessionListeners(); - useAudioListeners(); + // useAudioListeners(); } diff --git a/src/ipc/session.ts b/src/ipc/session.ts new file mode 100644 index 0000000..fa50aa1 --- /dev/null +++ b/src/ipc/session.ts @@ -0,0 +1,20 @@ + +"use strict"; + +import { ipcMain, IpcMainEvent, IpcMainInvokeEvent } from "electron"; +import { sendMessage } from '@/session'; + +// Handle messages from window/client. +function onSendMessage(_event: IpcMainEvent, payload: Message): void { + sendMessage(payload); +} + +// Login attempt, returns success or not. +function onLogin(_event: IpcMainEvent, payload: LoginPayload) => { + authenticate(payload.email, payload.password); +} + +export default function useSessionListeners(): void { + ipcMain.removeAllListeners("client-message"); + ipcMain.on("client-message", onSendMessage); +} \ No newline at end of file diff --git a/src/ipcRend/account.ts b/src/ipcRend/account.ts deleted file mode 100644 index 9300aa6..0000000 --- a/src/ipcRend/account.ts +++ /dev/null @@ -1,27 +0,0 @@ - -import { useIpc } from "@/modules/ipc"; -import { Profile } from "@/types"; - -const { invoke } = useIpc(); - -interface LoginPayload { - email: string; - password: string; -} - - -export const invokeProfile = async (): Promise => ( - await invoke('user-profile', null) -); - - -export const invokeLogin = async ( - payload: LoginPayload -): Promise => ( - await invoke('user-login', JSON.stringify(payload)) -); - - -export const invokeLogout = async (): Promise => ( - await invoke("user-logout", null) -); diff --git a/src/ipcRend/audio.ts b/src/ipcRend/audio.ts deleted file mode 100644 index 6f25d29..0000000 --- a/src/ipcRend/audio.ts +++ /dev/null @@ -1,15 +0,0 @@ - -import { useIpc } from "@/modules/ipc"; - - -const { post, invoke } = useIpc(); - - -export const postStartRecord = (): void => ( - post("start-recording", null) -); - - -export const invokeStopRecord = async (): Promise => ( - await invoke("stop-recording", null) -); diff --git a/src/ipcRend/session.ts b/src/ipcRend/session.ts deleted file mode 100644 index 2eb4405..0000000 --- a/src/ipcRend/session.ts +++ /dev/null @@ -1,23 +0,0 @@ - -import { useIpc } from "@/modules/ipc"; - -import { ClientMessage } from "@/types"; - -const { post } = useIpc(); - - -export const postMount = (): void => ( - post("app-mounted", null) -); - - -export const postInitSession = (cid: string): void => ( - post("init-session", cid) -); - - -export const postMessage = (payload: ClientMessage): void => ( - post('client-message', payload) -); - - diff --git a/src/main.ts b/src/main.ts index 68cc98e..744643d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,16 +1,72 @@ -// src/main.ts +/** + * Where the background logic really begins, gets called by app.onReady(). + * + * Handles authentication. If profile is set, we launch a session, which consis + * of opening a connection with the platform, initializing the audio streams. + * + * The session is primarily an interface between the frontend and the platform, + * relaying messages from one to the other. + * + */ -import App from "./App.vue"; +import { fetchAccount, submit } from "@/api/account"; +import { launchSession, endSession } from "@/session"; +import useIpc from "@/ipc/index"; +import store from "@/composables/store"; -import mitt from "mitt"; -import { createApp } from "vue"; -require('dotenv').config() +/* user profile, signals whether user is logged in */ +let auth: Profile | null = null; +/* authenticate the user */ +export async function authenticate(email: string, password: string) { -// Handle events. -const emitter = mitt(); + /* attempt normal login */ + try { + auth = await submit(email, password); + } catch (e) { + console.log(e); + } -const app = createApp(App) + /* launch if profile */ + if (auth) { + launchSession(auth); + } -app.provide("mitt", emitter) -app.mount("#app"); +} + +/* logout the user, end the session */ +export function deauthenticate() { + + /* set profile back to null */ + auth = null; + + /* terminate the session */ + endSession(); + +} + +export default async function main() { + + /* launch browser window */ + // await createWindow(); + + /* attempt key-based authentication with business api */ + const token = store.get('key', null); + const crimataId = store.get('crimataId', null); + + try { + const res = await fetchAccount(crimataId, token); + auth = parseAuthRes(res); + } catch (e) { + console.log('[MAIN]', e); + } + + /* connect to Crimata, or listen for manual login req */ + if (auth) { + launchSession(auth); + } + + /* initiate controls for frontend to use when needed */ + useIpc(); + +} \ No newline at end of file diff --git a/src/modules/message.ts b/src/modules/message.ts deleted file mode 100644 index 71beb70..0000000 --- a/src/modules/message.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - RenderMessage, - ClientMessage, - ClientRequest, - AuthRequest, - LogoutRequest -} from "@/types"; - -import { v4 as uuidv4 } from 'uuid'; - -function getTimeStamp(): number { - const currentdate = new Date(); - return currentdate.getTime(); -} - -// Create a RenderMessage object. -export const renderMessage = (text: boolean | string, audio: boolean | string, context: string, modifier: string): RenderMessage => ( - { - content: { - text: text, - audio: audio - }, - context: context, - modifier: modifier, - time: getTimeStamp(), - uid: uuidv4(), - isChild: "none", - seen: false, - newMessage: false - } -) - -export const clientMessage = (text: string, audio: string | boolean, uid: string): ClientMessage => ( - { - audio, - text, - uid - } -) - -export const clientRequest = (intent: string, params: object, epic: string | boolean): ClientRequest => ( - { - intent: intent, - params: params, - epic: epic, - confidence: 1.0 - } -) - -export const authRequest = (key: boolean | string, usr: boolean | string, pwd: boolean | string): AuthRequest => ( - { - key, - usr, - pwd - } -) - -export const logoutRequest = (): LogoutRequest => ( - { - logout: true - } -) \ No newline at end of file diff --git a/src/modules/messages.ts b/src/modules/messages.ts deleted file mode 100644 index 1d2c4b4..0000000 --- a/src/modules/messages.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { ref } from 'vue'; -import useScroll from "@/modules/scroll"; -import { RenderMessage, Annotation } from "@/types"; - -const messages = ref(new Map()); - -const addMessage = (message: RenderMessage) => { - messages.value.set(message.uid, message) -} - -const updateMessage = (annotation: Annotation) => { - const message = messages.value.get(annotation.uid) - message.context = annotation.context - message.content.text = annotation.text -} - -const loadSavedMessages = () => { - const rawData = window.localStorage.getItem("crimata_messages"); - if (rawData) { - const messageData = JSON.parse(rawData) - messages.value = new Map(Object.entries(messageData)); - } -} - -const saveMessages = () => { - const messageData = Object.fromEntries(messages.value); - window.localStorage.setItem("crimata_messages", JSON.stringify(messageData)); -} - -const isSimmilar = (messageA: RenderMessage, messageB: RenderMessage) => { - if ((Math.abs(messageA.time - messageB.time) < 20000) && (messageA.modifier == messageB.modifier) && (messageA.context == messageB.context)) { - return true - } - return false -} - -const updateGrouping = () => { - console.log("updating grouping") - const refs = Array.from(messages.value.keys()) - - // Get the last three messages. - const first = messages.value.get(refs[refs.length - 1]) - const second = messages.value.get(refs[refs.length - 2]) - const third = messages.value.get(refs[refs.length - 3]) - - // If messages are simmilar, update the classes. - if ((first) && (second)) { - if (isSimmilar(first, second)) { - first.isChild = "last" // i.e. last in group. - second.isChild = "first" - - if (third) { - if ((third.isChild == "first") || (third.isChild == "middle")) { - second.isChild = "middle" - } - } - } - } -} - - -export default function useMessages() { - - // Scroll controller. - const { setScroll, updateScrollRef, adjustScroll } = useScroll("messenger"); - - // Main function for updating the message view. - const updateMessageView = (message: RenderMessage | Annotation) => { - - // Step 1: See if user is scrolled down. - updateScrollRef() - - // Step 2: Add the new content to the view. - if ("content" in message) { - addMessage(message) - } else { - updateMessage(message) - } - - // Step 3: Pop off oldest message (if > 200). - if (messages.value.size >= 200) { - const oldest = Array.from(messages.value.keys()).shift(); - messages.value.delete(oldest); - } - - // Step 4: Update grouping. - updateGrouping() - - // Setp 5: Scroll the view (if scrolled down). - setTimeout(adjustScroll, 20); - - // Step 6: Save the view data. - saveMessages() - - } - - // Seed message view with message history. - const prepMessageView = (newMessages: RenderMessage[]) => { - console.log("MSGR:Prepping messenger view.") - - // Load and render saved messages and immediately scroll to bottom. - loadSavedMessages() - setTimeout(setScroll.bind(false), 10); - - // Render new messages, then wait 1s to scroll. - if (newMessages.length) { - - console.log("MSGR:Adding new messages") - - newMessages.forEach(message => { - message.newMessage = true; - addMessage(message) - }) - - setTimeout(setScroll.bind(true), 1000); - - } - } - - return { - messages, - prepMessageView, - updateMessageView - } -} \ No newline at end of file diff --git a/src/modules/mitt.ts b/src/modules/mitt.ts deleted file mode 100644 index 4f3f448..0000000 --- a/src/modules/mitt.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { inject } from "vue"; -import { Emitter } from "mitt"; - -// Frontend emitter - -type Mitt = Emitter; - -let emitter: Mitt; - -export default function useMitt() { - - const emitterInject: Mitt | undefined = inject("mitt"); - if (emitterInject) { - emitter = emitterInject; - } - return { - emitter - } -} \ No newline at end of file diff --git a/src/modules/scroll.ts b/src/modules/scroll.ts deleted file mode 100644 index 58370f7..0000000 --- a/src/modules/scroll.ts +++ /dev/null @@ -1,50 +0,0 @@ - - -export default function useScroll(element: string) { - - let isScrolledToBottom: boolean; - - // Set the initial scroll position. - const setScroll = (smooth: boolean) => { - const view = document.getElementById(element) - - if (view) { - view.scrollTo({ - top: view.scrollHeight - view.clientHeight, - behavior: (smooth) ? 'smooth' : 'auto' - }); - } - } - - // Update isScrolledToBottom - const updateScrollRef = () => { - const view = document.getElementById(element) - - if (view) { - isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1 - } - - } - - // Adjust scroll after we add content to the messenger. - const adjustScroll = () => { - const view = document.getElementById(element) - - if (view) { - if (isScrolledToBottom) { - view.scrollTo({ - top: view.scrollHeight - view.clientHeight, - behavior: 'smooth' - }); - } - } - - } - - return { - updateScrollRef, - adjustScroll, - setScroll, - } - -} diff --git a/src/render/App.vue b/src/render/App.vue new file mode 100644 index 0000000..e5c477d --- /dev/null +++ b/src/render/App.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/render/components/bubble.vue b/src/render/components/bubble.vue new file mode 100644 index 0000000..6a62e62 --- /dev/null +++ b/src/render/components/bubble.vue @@ -0,0 +1,330 @@ + + + + + + + diff --git a/src/render/components/controllers/bubble.control.ts b/src/render/components/controllers/bubble.control.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/render/components/controllers/helpers.ts b/src/render/components/controllers/helpers.ts new file mode 100644 index 0000000..7509bf0 --- /dev/null +++ b/src/render/components/controllers/helpers.ts @@ -0,0 +1,102 @@ +import { ref } from "vue"; +import anime from "animejs"; +import { v4 as uuidv4 } from 'uuid'; + + +export function animateTextInput () { + + const side = ref("right"); + + function show () { + const t1 = (side.value === "right") ? 50 : -70; + const t2 = (side.value === "right") ? 110 : -130; + + anime({ + targets: '#textInput', + opacity: [0, 1], + translateX: [t1, t2], + scale: [0.3, 1], + duration: 500, + easing: 'easeOutExpo', + }) + + } + + function hide () { + const t = (side.value === "right") ? 50 : -80; + + anime({ + targets: '#textInput', + opacity: [1, 0], + translateX: t, + scale: 0.3, + duration: 500, + easing: 'easeOutExpo', + }) + + } + + function switchSide () { + const t = (side.value === "right") ? -130 : 110; + + anime({ + targets: '#textInput', + translateX: t, + duration: 500, + easing: 'easeOutExpo', + }) + + } + + return { + side, + show, + hide, + switchSide + }; + +} + +export function animateAudioInput () { + + function show () { + anime({ + targets: '#recIcon', + opacity: [0, 0.75], + scale: [0.0, 1], + duration: 250, + easing: 'linear', + }) + } + + function hide () { + anime({ + targets: '#recIcon', + opacity: [0.75, 0], + scale: [1, 0], + duration: 250, + easing: 'linear', + }) + } + + return { + show, + hide + }; + +} + + +export function newMessage ({ + text=false, + audio=false, + context=false, + uid=uuidv4() +}): Message { + return { + text: text, + audio: audio, + context: context, + uid: uid + }; +} diff --git a/src/render/components/controllers/inputItem.control.audio.ts b/src/render/components/controllers/inputItem.control.audio.ts new file mode 100644 index 0000000..b699405 --- /dev/null +++ b/src/render/components/controllers/inputItem.control.audio.ts @@ -0,0 +1,79 @@ +import anime from "animejs"; +import { useIpc } from '@/modules/ipc'; +import { onMounted, onUnmounted, ref, Ref } from "vue"; +import { postMessage } from "@/ipc/session"; +import { newMessage, animateAudioInput } from "./helpers"; +import { invokeStopRecord, postAudioChunk } from "@/ipc/audio"; + +export default function useAudioInputController (typing: Ref) { + + const recording = ref(false); + let mediaRecorder: MediaRecorder; + + const { show, hide } = animateAudioInput(); + + // initialize audio + const conf = {audio: true, video: false} + navigator.mediaDevices.getUserMedia(conf).then((stream: MediaStream) => { + + const options = {mimeType: 'audio/webm'}; + mediaRecorder = new MediaRecorder(stream, options); + + // post any mew audio to backend + mediaRecorder.addEventListener('dataavailable', (e: BlobEvent) => { + e.data.arrayBuffer().then((buff: ArrayBuffer) => { + postAudioChunk(buff); + }); + }); + + // get audio and post new message to backend + mediaRecorder.addEventListener('stop', (_e: Event) => { + invokeStopRecord().then((audio: ArrayBuffer[] | Error) => { + console.log(audio); + // postMessage(newMessage({audio: audio})); + }); + }); + + }); + + // start recording on space bar + const record = () => { + console.log("INPT:Capturing audio...") + mediaRecorder.start(); + recording.value = true; + show() + } + + // stop recording and send on release + const stop = () => { + console.log("INPT:Stopping record.") + mediaRecorder.stop(); + recording.value = false; + hide(); + } + + const onKeyDown = (e: KeyboardEvent) => { + if (e.keyCode == 32 && !typing.value) record(); + } + + const onKeyUp = (e: KeyboardEvent) => { + if (e.keyCode == 32 && recording.value) stop(); + } + + //----------------------------------------------------------- + + onMounted(() => { + window.addEventListener("keydown", onKeyDown); + window.addEventListener("keyup", onKeyUp); + }); + + onUnmounted(() => { + window.removeEventListener("keydown", onKeyDown); + window.removeEventListener("keyup", onKeyUp); + }) + + return { + recording + } + +} diff --git a/src/components/controllers/textCtrl.ts b/src/render/components/controllers/inputItem.control.text.ts similarity index 53% rename from src/components/controllers/textCtrl.ts rename to src/render/components/controllers/inputItem.control.text.ts index ced8aaf..21ada72 100644 --- a/src/components/controllers/textCtrl.ts +++ b/src/render/components/controllers/inputItem.control.text.ts @@ -1,72 +1,20 @@ -import anime from "animejs"; -import useMitt from "@/modules/mitt"; -import { useIpc } from '@/modules/ipc'; import { Ref, ref, watch, onMounted, onUnmounted } from "vue"; -import keyboardNameMap from "../keyBoardMaps/keyboardNameMap"; -import { clientMessage, renderMessage } from '@/modules/message'; -import { postMessage } from "@/ipcRend/session"; - - -//---Animations----------------------------------------------- - -let side = "right"; // Side of parent we are on. - -function showTextInput () { - const t1 = (side === "right") ? 50 : -70; - const t2 = (side === "right") ? 110 : -130; - - anime({ - targets: '#textInput', - opacity: [0, 1], - translateX: [t1, t2], - scale: [0.3, 1], - duration: 500, - easing: 'easeOutExpo', - }) - -} - -function hideTextInput() { - const t = (side === "right") ? 50 : -80; - - anime({ - targets: '#textInput', - opacity: [1, 0], - translateX: t, - scale: 0.3, - duration: 500, - easing: 'easeOutExpo', - }) - -} - -function switchSide(currentSide: string) { - const t = (currentSide === "right") ? -130 : 110; - - anime({ - targets: '#textInput', - translateX: t, - duration: 500, - easing: 'easeOutExpo', - }) - -} - -//------------------------------------------------------------ +import { postMessage } from "@/ipc/session"; +import { newMessage, animateTextInput } from "./helpers"; export default function useTextInputController(elementX: Ref) { + let textInput: HTMLInputElement | null; - const { post } = useIpc(); - const { emitter } = useMitt(); + const { side, show, hide, switchSide } = animateTextInput(); let firstKey = true; const typing = ref(false); // Prep inputItem for typing. const prepInput = () => { - showTextInput() + show() typing.value = true } @@ -78,7 +26,7 @@ export default function useTextInputController(elementX: Ref) { textInput.blur(); } - hideTextInput() + hide() firstKey = true; typing.value = false; } @@ -87,28 +35,20 @@ export default function useTextInputController(elementX: Ref) { const sendMessage = () => { if (textInput) { - // Create the message. - const message = renderMessage( - textInput.value, - false, - "", - "sf" - ) - - emitter.emit("self-message", message); - // Send it to the backend for processing. - const clientM = clientMessage(textInput.value, false, message.uid); - postMessage(clientM); + const message = newMessage({ + text: textInput.value + }); + + postMessage(message); clearInput() } } // Keys that are capable of opening the text input (numbers and letters). - const hotKeyRange = keyboardNameMap.slice(47, 91) - const isHotKey = (key: string) => { - if (hotKeyRange.includes(key)) { + const isHotKey = (key: number) => { + if (key >= 47 && key <= 91) { // a letter return true } } @@ -116,7 +56,7 @@ export default function useTextInputController(elementX: Ref) { //---Callbacks----------------------------------------------- const onKeyDown = (e: KeyboardEvent) => { - const key = keyboardNameMap[e.keyCode] + const key = e.keyCode; if (textInput) { @@ -133,18 +73,18 @@ export default function useTextInputController(elementX: Ref) { textInput.focus(); // Close input when no text or on ESC. - if ((textInput.value == "") && (!firstKey) && (key === "BACK_SPACE")) { + if ((textInput.value == "") && (!firstKey) && (key === 8)) { // backspace clearInput() return } - if (key === "ESCAPE") { + if (key === 27) { // escape clearInput() return } // Close and send on enter. - if (key === "ENTER") { + if (key === 13) { if (textInput.value) { sendMessage() return @@ -162,17 +102,17 @@ export default function useTextInputController(elementX: Ref) { const winW = window.innerWidth // Logic depends on the side we are on. - if (side === "right") { + if (side.value === "right") { if (winW - elementX < 230) { - switchSide(side) - side = "left" + switchSide() + side.value = "left" } } else { if (winW - elementX > 230) { - switchSide(side) - side = "right" + switchSide() + side.value = "right" } } diff --git a/src/render/components/controllers/messenger.control.ts b/src/render/components/controllers/messenger.control.ts new file mode 100644 index 0000000..63f15ee --- /dev/null +++ b/src/render/components/controllers/messenger.control.ts @@ -0,0 +1,143 @@ +import { ref } from 'vue'; +import invokeSavedMessages from "@/render/ipc"; +import useScroll from "@/render/composables/scroll"; + +const messages = ref(new Map()); + +function getTimeStamp(): number { + const currentdate = new Date(); + return currentdate.getTime(); +} + +const newViewMessage = (message: Message): ViewMessage => { + return { + text: message.text, + context: message.context, + audio: message.audio, + from: message.from, + uid: message.uid, + time: getTimeStamp(), + isChild: "none", + seen: false, + newMessage: false + }; +} + +const addMessage = (message: Message, newMessage=false) => { + const viewMessage = newViewMessage(message); + if (newMessage) viewMessage.newMessage = true; + messages.value.set(viewMessage.uid, viewMessage); +} + +const updateMessage = (message: Message) => { + const viewMessage = messages.value.get(message.uid); + viewMessage.context = message.context; + viewMessage.text = message.text; +} + +const loadSavedMessages = async () => { + const messageData = await invokeSavedMessages(); + messages.value = new Map(Object.entries(messageData)); +} + +const saveMessages = () => { + const messageData = Object.fromEntries(messages.value); + // must save to json. +} + +const pruneMessages = (limit=200) => { + if (messages.value.size >= limit) { + const oldest = Array.from(messages.value.keys()).shift(); + messages.value.delete(oldest); + } +} + +const updateGrouping = () => { + + const isSimmilar = (messageA: ViewMessage, messageB: ViewMessage) => { + if ((Math.abs(messageA.time - messageB.time) < 20000) && (messageA.from == messageB.from) && (messageA.context == messageB.context)) { + return true + } + return false + } + + const refs = Array.from(messages.value.keys()) + + // Get the last three messages. + const first = messages.value.get(refs[refs.length - 1]) + const second = messages.value.get(refs[refs.length - 2]) + const third = messages.value.get(refs[refs.length - 3]) + + // If messages are simmilar, update the classes. + if ((first) && (second)) { + if (isSimmilar(first, second)) { + first.isChild = "last" // i.e. last in group. + second.isChild = "first" + + if (third) { + if ((third.isChild == "first") || (third.isChild == "middle")) { + second.isChild = "middle" + } + } + } + } +} + +export default function useMessages() { + + const { updateScrollRef, adjustScroll } = useScroll("messenger"); + + /* Given new message object, update the view accordingly */ + const updateMessageView = (newMessages: Message[]) => { + + const bottom = updateScrollRef(); // see if the user is scrolled down + + // take each message and apply view + newMessages.forEach((message: Message) => { + + // add or update message depending + if (messages.value.has(message.uid)) { + updateMessage(message); + } else addMessage(message); + + pruneMessages(); // pop off old messages from view + updateGrouping(); // group like message together + + if (bottom) adjustScroll(); // only scroll if user was at bottom + + saveMessages(); + + }); + + } + + return { + messages, + updateMessageView, + loadSavedMessages + } + +} + + + // // Seed message view with message history. + // const prepMessageView = async (newMessages: Message[]) => { + // console.log("MSGR:Prepping messenger view.") + + // // Load and render saved messages and immediately scroll to bottom. + // await loadSavedMessages(); + // setTimeout(setScroll.bind(false), 10); + + // // Render new messages, then wait 1s to scroll. + // if (newMessages.length) { + + // console.log("MSGR:Adding new messages") + + // newMessages.forEach(message => { + // addMessage(message, true); + // }) + + // setTimeout(setScroll.bind(true), 1000); + + // } + // } \ No newline at end of file diff --git a/src/render/components/header.vue b/src/render/components/header.vue new file mode 100644 index 0000000..f0afc8a --- /dev/null +++ b/src/render/components/header.vue @@ -0,0 +1,81 @@ + + + + + \ No newline at end of file diff --git a/src/components/inputItem.vue b/src/render/components/inputItem.vue similarity index 87% rename from src/components/inputItem.vue rename to src/render/components/inputItem.vue index 861d7c1..30978b3 100644 --- a/src/components/inputItem.vue +++ b/src/render/components/inputItem.vue @@ -13,7 +13,10 @@ - + @@ -29,9 +32,9 @@ import draggify from "@/modules/draggify"; import TextInput from "@/components/textInput.vue"; import useTextInputController from - "@/components/controllers/textCtrl"; + "@/components/controllers/inputItem.control.audio"; import useAudioInputController from - "@/components/controllers/audioCtrl"; + "@/components/controllers/inputItem.control.text"; export default defineComponent({ name: "InputItem", @@ -191,4 +194,32 @@ export default defineComponent({ } } +#textInput { + position: absolute; + + opacity: 0; + + min-width: 150px; + height: 16px; + + border-radius: 18px; + + padding: 10px; + margin-right: 10px; + margin-left: 10px; + + outline: none; + border: none; + pointer-events: none; + + background-color: white; + + z-index: -1; + + box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15); + + transform: translateX(50px) scale(0.3); + +} + diff --git a/src/components/login.vue b/src/render/components/login.vue similarity index 89% rename from src/components/login.vue rename to src/render/components/login.vue index 2cb82f8..5ed0ad0 100644 --- a/src/components/login.vue +++ b/src/render/components/login.vue @@ -39,7 +39,6 @@ import { useIpc } from "@/modules/ipc"; import { authRequest } from '@/modules/message'; import { useProfile } from '@/modules/auth'; import { invokeLogin } from "@/ipcRend/account"; -import { Profile } from "@/types"; import { postInitSession } from "@/ipcRend/session"; export default defineComponent({ @@ -47,8 +46,6 @@ export default defineComponent({ setup() { - const { profile, setProfile } = useProfile(); - const usr = ref(""); const pwd = ref(""); @@ -56,23 +53,17 @@ export default defineComponent({ const submitForm = async () => { try { + const profile = await invokeLogin({ email: usr.value, password: pwd.value - }) as Profile; + }); - setProfile(profile); + /* emit event to app.vue */ + window.postMessage(profile); - } catch(e) { - console.log('Error login in.') - } finally{ + } catch (e) console.log(e); - if (profile.value.crimataId) { - // start session - postInitSession(profile.value.crimataId); - } - - } } return { diff --git a/src/render/components/messenger.vue b/src/render/components/messenger.vue new file mode 100644 index 0000000..77d54f3 --- /dev/null +++ b/src/render/components/messenger.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/components/settings.vue b/src/render/components/settings.vue similarity index 100% rename from src/components/settings.vue rename to src/render/components/settings.vue diff --git a/src/components/splash.vue b/src/render/components/splash.vue similarity index 100% rename from src/components/splash.vue rename to src/render/components/splash.vue diff --git a/src/modules/auth.ts b/src/render/composables/auth.ts similarity index 90% rename from src/modules/auth.ts rename to src/render/composables/auth.ts index 5fb40c1..dea4bd1 100644 --- a/src/modules/auth.ts +++ b/src/render/composables/auth.ts @@ -1,6 +1,4 @@ import { ref } from "vue"; -import { Profile } from "@/types"; - const profile = ref(); diff --git a/src/modules/draggify.ts b/src/render/composables/draggify.ts similarity index 100% rename from src/modules/draggify.ts rename to src/render/composables/draggify.ts diff --git a/src/modules/ipc.ts b/src/render/composables/ipc.ts similarity index 91% rename from src/modules/ipc.ts rename to src/render/composables/ipc.ts index 36658de..0d067d3 100644 --- a/src/modules/ipc.ts +++ b/src/render/composables/ipc.ts @@ -1,5 +1,5 @@ -export const useIpc = () => { +export default function useIpc () { const invoke = async (endpoint: string, payload: any) => { try { diff --git a/src/render/composables/scroll.ts b/src/render/composables/scroll.ts new file mode 100644 index 0000000..811e7fc --- /dev/null +++ b/src/render/composables/scroll.ts @@ -0,0 +1,29 @@ + + +export default function useScroll(element: string) { + + let isScrolledToBottom: boolean; + const view = document.getElementById(element) + + // Update isScrolledToBottom + const updateScrollRef = () => { + if (view) isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1; + return isScrolledToBottom; + } + + // Adjust scroll after we add content to the messenger. + const adjustScroll = () => { + if (view) { + view.scrollTo({ + top: view.scrollHeight - view.clientHeight, + behavior: 'smooth' + }); + } + } + + return { + updateScrollRef, + adjustScroll, + }; + +} \ No newline at end of file diff --git a/src/render/ipc.ts b/src/render/ipc.ts new file mode 100644 index 0000000..a232f7f --- /dev/null +++ b/src/render/ipc.ts @@ -0,0 +1,52 @@ + +import useIpc from "@/render/composables/ipc"; + +const { post, invoke } = useIpc(); + +/** + * + * Account and auth related endpoints + * + */ + +export const invokeProfile = async (): Promise => ( + await invoke('user-profile', null) +); + +export const invokeLogin = async ( + payload: LoginPayload +): Promise => ( + await invoke('user-login', JSON.stringify(payload)) +); + +export const invokeLogout = async (): Promise => ( + await invoke("user-logout", null) +); + +/** + * + * Audio endpoints + * + */ + +export const postAudioChunk = (chunk: ArrayBuffer): void => ( + post("audio-chunk", chunk) +); + +export const invokeReturnAudio = async (): Promise => ( + await invoke("get-audio", null) +); + +/** + * + * Crimata Platform (session) endpoints + * + */ + +export const invokeSession = async (cid: string): Promise => ( + await invoke("messenger-init", cid) +); + +export const postMessage = (payload: Message): void => ( + post('client-message', payload) +); \ No newline at end of file diff --git a/src/preload.ts b/src/render/preload.ts similarity index 100% rename from src/preload.ts rename to src/render/preload.ts diff --git a/src/shims-vue.d.ts b/src/render/shims-vue.d.ts similarity index 100% rename from src/shims-vue.d.ts rename to src/render/shims-vue.d.ts diff --git a/src/session.ts b/src/session.ts new file mode 100644 index 0000000..0a893d8 --- /dev/null +++ b/src/session.ts @@ -0,0 +1,64 @@ + + + + +import useAudio from "@/audio"; +import { loadState, saveState, emitState } from "@/state"; + +/* start and stop audio functionality */ +const { initAudio, closeAudio } = useAudio(); + +/** + * Controls for interfacing with the platform. + * Takes an onMessage callback which we define below. + */ +const { connect, send, close } = usePlatform((message: Message) => { + + /* add the message to the state */ + addMessage(message); + + /* push the message to the browser */ + if (win) emit("new-message", message); + +}); + +/* send a message to the platform */ +export function sendMessage(message: Message) { + + /* add the message to the state */ + addMessage(message); + + /* push the message to the browser */ + if (win) emit("new-message", message); + + /* socket send */ + send(message); + +} + +/* launch a new session (the main process for authenticated users) */ +export function launchSession(profile: Profile) { + + /* load any previously saved state for that user */ + loadState(profile); + + /* connect to the platform */ + connect(profile); + + /* initialize the audio streams */ + // initAudio(); + + /* finally we can push state to browser */ + if (win) emitState(); + +} + +export function endSession() { + + closeAudioStreams(); + + closeSocket(); + + state.clear(); + +} \ No newline at end of file diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..1d6ce02 --- /dev/null +++ b/src/state.ts @@ -0,0 +1,29 @@ +const Store = require('electron-store'); + +/* simple data persistance */ +const store = new Store; + +/* state of the session (e.g. profile and messages for now) */ +let state: State | null = null; + +/* load saved state in electron store for given user */ +export function loadState(profile: Profile) { + state = store.get("state", null); +} + +/* add a message to state.messages */ +export function addMessage(message: Message) { + if (state) { + state.messages.push(message); + saveState(); + } +} + +export function saveState() { + store.set("state", state); +} + +export function emitState() { + emit("update-state", state); +} + diff --git a/src/types.ts b/src/types.ts index 2e35fc5..b437ffa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,76 +1,37 @@ -export interface RenderMessage { - content: { - text: boolean | string; - audio: boolean | string; - }; - context: string; - modifier: string; +interface Message { + text: boolean | string; + context: boolean | string; + audio: boolean | string; + type: 1 | 2 | 3; time: number; uid: string; - isChild: string; +} + +interface ViewMessage extends Message { + child: string; seen: boolean; newMessage: boolean; } -export interface ClientMessage { - text: string; - audio: boolean | string; - uid: string; -} - -export interface ClientRequest { - intent: string; - params: object; - epic: string | boolean; - confidence: number; -} - -export interface SessionState { - key: string | boolean; - newMessages: RenderMessage[]; -} - -export interface WindowState { +interface WindowState { width: number; height: number; x: number | null; y: number | null; } -export interface AuthRequest { - key: boolean | string; - usr: boolean | string; - pwd: boolean | string; -} - -export interface Profile { +interface Profile { crimataId: string; alias: string; initials: string; } -export interface AuthProtocol { - token: null | string; - profile: null | Profile; - password?: string; - email?: string; +interface State { + profile: Profile | null; + messages } -export interface LogoutRequest { - logout: boolean; -} - -export interface Annotation { - text: string; - context: string; - uid: string; -} - -export interface StandardMessage { - content: { - text: boolean | string; - audio: boolean | string; - }; - context: string; - modifier: string; +interface LoginPayload { + email: string; + password: string; } diff --git a/src/background/window.ts b/src/window.ts similarity index 86% rename from src/background/window.ts rename to src/window.ts index 162b3e1..4c69387 100644 --- a/src/background/window.ts +++ b/src/window.ts @@ -2,20 +2,39 @@ import { BrowserWindow, ipcMain } from "electron"; import { createProtocol } from "vue-cli-plugin-electron-builder/lib"; -import { backgroundMitt } from '@/modules/emitter'; -import { RenderMessage, WindowState } from "@/types"; -import { loadWinState, saveToJson } from "./helpers"; +import { backgroundMitt } from './coposables/emitter'; +import { saveToJson } from "./coposables/json"; import * as path from "path"; const { autoUpdater } = require('electron-updater'); interface IpcRendererPayload { endpoint: string; - message: RenderMessage | null; + message: Message | null; } let win: BrowserWindow | null; let winState: WindowState; +const loadWinState = (fileName: string): WindowState => { + let state: WindowState; + + try { + state = JSON.parse(fs.readFileSync(configPath + fileName).toString()); + } + + catch (error) { + state = { + width: 600, + height: 500, + x: null, + y: null, + } + } + + return state + +} + // Called when a NavBar button is pressed. const onNavBar = (_event: any, action: string): void => { if (win) { @@ -78,7 +97,7 @@ const onWindowDismount = (): void => { } // function used by run.ts to create the main window. -export async function createWindow(): Promise { +export default async function createWindow(): Promise { return new Promise((resolve, _reject) => { // avoid creating duplicate windows. diff --git a/tsconfig.json b/tsconfig.json index 307539b..5a01f4d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,7 @@ ] }, "include": [ + "**/*.ts", "src/*.ts", "src/**/*.ts", "src/**/*.tsx", From 9e8641b4def1a49c8e607475b1dfe1d8a2176b4f Mon Sep 17 00:00:00 2001 From: Andrew Gundersen Date: Sat, 12 Jun 2021 09:49:41 -0500 Subject: [PATCH 153/163] more changes --- src/api/account.ts | 27 +++- src/composables/canvas.ts | 36 +++++ src/composables/store.ts | 16 -- src/composables/websockets.ts | 54 +++---- src/main.ts | 60 ++++---- src/render/App.vue | 12 +- .../controllers/messenger.control.ts | 139 +++--------------- src/render/components/messenger.vue | 20 ++- src/session.ts | 58 ++++---- src/state.ts | 29 ---- src/types.ts | 7 - 11 files changed, 170 insertions(+), 288 deletions(-) create mode 100644 src/composables/canvas.ts delete mode 100644 src/composables/store.ts delete mode 100644 src/state.ts diff --git a/src/api/account.ts b/src/api/account.ts index de1335b..98d3c69 100644 --- a/src/api/account.ts +++ b/src/api/account.ts @@ -4,20 +4,33 @@ import axios from "axios"; const { post } = useHttp(); -export const submit = async (email: string, password: string) => ( - await post('/account/login', { email, password }) -) +export const usrPwdAuth = async (email: string, password: string) => { -export const fetchAccount = async (email: string, token: string) => ( - await axios({ + try { + return await post('/account/login', { email, password }) + + } catch (e) { + return null; + } + +} + +export const tokenAuth = async (cid: string, token: string) => { + + try { + return await axios({ url: "http://127.0.0.1:3000/api/account/profile", headers: { Cookie: `jwt=${token}` }, method: 'GET', data: { - email, + cid, } }) -) + } catch (e) { + return null; + } + +} \ No newline at end of file diff --git a/src/composables/canvas.ts b/src/composables/canvas.ts new file mode 100644 index 0000000..c85f7ed --- /dev/null +++ b/src/composables/canvas.ts @@ -0,0 +1,36 @@ + + + +export default class Canvas { + + messages: Message[]; + + /* seed canvas with messages on init */ + constructor(messages: Message[]) { + this.messages = messages; + ipcEmit("seed-view", this.messages); + } + + /* add a new message to the canvas */ + add(message: Message) { + this.messages.push(message); + ipcEmit("update-view", message); + } + + /* update an existing message */ + update(message: Message) { + + /* get the target message */ + let target_message = this.messages.filter((m: Message) => { + return m.uid = message.uid; + })[0]; + + /* replace the target message */ + if (target_message) { + target_message = message; + ipcEmit("update-view", message); + } + + } + +} diff --git a/src/composables/store.ts b/src/composables/store.ts deleted file mode 100644 index 16f8409..0000000 --- a/src/composables/store.ts +++ /dev/null @@ -1,16 +0,0 @@ - -const Store = require('electron-store'); - -const schema = { - key: { - type: 'string', - }, - crimataId: { - type: 'string' - } -}; - -export const store = new Store({ - schema, - encryptionKey: "super user test" -}); diff --git a/src/composables/websockets.ts b/src/composables/websockets.ts index 779f00b..d034027 100644 --- a/src/composables/websockets.ts +++ b/src/composables/websockets.ts @@ -3,7 +3,7 @@ import WebSocket from 'ws'; -export default function useWebSockets(receiveCallback: (s: string) => void, openCallback?: () => void) { +export default function useWebSockets(onMessageCallback: (s: string) => void) { let socket: WebSocket | null = null; @@ -19,36 +19,25 @@ export default function useWebSockets(receiveCallback: (s: string) => void, open }); } - const onOpen = (_event: WebSocket.OpenEvent) => { - console.log("WS:Connected to WS Server!"); - if (openCallback) openCallback(); - } - - const onServerMessage = (event: WebSocket.MessageEvent) => { - console.log("WS:Message received: ", event.data); - receiveCallback(event.data.toString()) - } - - const onClose = (event: WebSocket.CloseEvent) => { - console.log("WS:Socket closed normally.") - } - - // Reconnect automatically on error. - const onError = (event: WebSocket.ErrorEvent) => { - console.log("WS:WebSocket error: ", event.message); - console.log("Attempting reconnect in 1s.") - setTimeout(createSocket, 1000); - } - - const createSocket = (socketUrl: string) => { + const connect = (socketUrl: string, secret: string) => { + /* create a new socket */ socket = new WebSocket(socketUrl) - // Add listeners. - socket.addEventListener("open", onOpen); - socket.addEventListener("message", onServerMessage); - socket.addEventListener("close", onClose); - socket.addEventListener("error", onError); + /* add event listeners */ + + socket.on("open", () => { + if (socket) + socket.send(secret); + }); + + socket.on("message", (event: WebSocket.MessageEvent) => { + onMessageCallback(event.data.toString()) + }); + + socket.on("close", () => { + return + }); } @@ -59,15 +48,10 @@ export default function useWebSockets(receiveCallback: (s: string) => void, open } } - const checkConnection = () => { - return true; - } - return { - createSocket, + connect, send, - close, - checkConnection + close }; } diff --git a/src/main.ts b/src/main.ts index 744643d..9d6414c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,37 +9,29 @@ * */ -import { fetchAccount, submit } from "@/api/account"; +import { tokenAuth, usrPwdAuth } from "@/api/account"; import { launchSession, endSession } from "@/session"; import useIpc from "@/ipc/index"; import store from "@/composables/store"; -/* user profile, signals whether user is logged in */ -let auth: Profile | null = null; - /* authenticate the user */ export async function authenticate(email: string, password: string) { /* attempt normal login */ - try { - auth = await submit(email, password); - } catch (e) { - console.log(e); - } + const platformKey, token, crimataId = await usrPwdAuth(email, password); - /* launch if profile */ - if (auth) { - launchSession(auth); - } + /* launch if successful */ + if (platformKey) + launchSession(platformKey, crimataId); + + /* save the token */ + store.set("token", token); } /* logout the user, end the session */ export function deauthenticate() { - /* set profile back to null */ - auth = null; - /* terminate the session */ endSession(); @@ -47,26 +39,24 @@ export function deauthenticate() { export default async function main() { - /* launch browser window */ - // await createWindow(); - - /* attempt key-based authentication with business api */ - const token = store.get('key', null); - const crimataId = store.get('crimataId', null); - - try { - const res = await fetchAccount(crimataId, token); - auth = parseAuthRes(res); - } catch (e) { - console.log('[MAIN]', e); - } - - /* connect to Crimata, or listen for manual login req */ - if (auth) { - launchSession(auth); - } - /* initiate controls for frontend to use when needed */ useIpc(); + /* launch browser window */ + await createWindow(); + + /* attempt to get a login token from the store */ + const token = store.get("token"); + + /* try to login with it, returns platform secret and new token on success */ + if (token) + const newToken, profile = await tokenAuth(token); + + /* if secret, we launch a session */ + if (newToken) + launchSession(newToken, profile); + + /* finally, save the most recent token */ + store.set("token", newToken); + } \ No newline at end of file diff --git a/src/render/App.vue b/src/render/App.vue index e5c477d..d90bcc0 100644 --- a/src/render/App.vue +++ b/src/render/App.vue @@ -6,8 +6,9 @@ @@ -39,13 +40,13 @@ export default defineComponent({ setup() { - const state: Ref; + const crimataId = ref(false); onMounted(async () => { console.log("[APP]:mounted."); /* listen for auth related messages */ - window.addEventListener("update-state", (event: any) => { + window.addEventListener("update-auth", (event: any) => { state.value = event.data; }); @@ -56,7 +57,7 @@ export default defineComponent({ }); return { - profile + crimataId } } }) @@ -68,7 +69,6 @@ export default defineComponent({ html, body { margin: 0; padding: 0; - // Background color set in window.ts } #app { diff --git a/src/render/components/controllers/messenger.control.ts b/src/render/components/controllers/messenger.control.ts index 63f15ee..9ae7d47 100644 --- a/src/render/components/controllers/messenger.control.ts +++ b/src/render/components/controllers/messenger.control.ts @@ -1,143 +1,38 @@ import { ref } from 'vue'; -import invokeSavedMessages from "@/render/ipc"; import useScroll from "@/render/composables/scroll"; -const messages = ref(new Map()); +const canvas = ref(); -function getTimeStamp(): number { - const currentdate = new Date(); - return currentdate.getTime(); +/* seed the canvas with messages */ +const seedCanvas = (messages: Message[]) => { + canvas.value = messages; } -const newViewMessage = (message: Message): ViewMessage => { - return { - text: message.text, - context: message.context, - audio: message.audio, - from: message.from, - uid: message.uid, - time: getTimeStamp(), - isChild: "none", - seen: false, - newMessage: false - }; -} - -const addMessage = (message: Message, newMessage=false) => { - const viewMessage = newViewMessage(message); - if (newMessage) viewMessage.newMessage = true; - messages.value.set(viewMessage.uid, viewMessage); +const addMessage = (message: Message) => { + canvas.value.push(message); } const updateMessage = (message: Message) => { - const viewMessage = messages.value.get(message.uid); - viewMessage.context = message.context; - viewMessage.text = message.text; -} -const loadSavedMessages = async () => { - const messageData = await invokeSavedMessages(); - messages.value = new Map(Object.entries(messageData)); -} + let target_message = canvas.value.filter((m: Message) => { + return m.uid = message.uid; + })[0]; -const saveMessages = () => { - const messageData = Object.fromEntries(messages.value); - // must save to json. -} - -const pruneMessages = (limit=200) => { - if (messages.value.size >= limit) { - const oldest = Array.from(messages.value.keys()).shift(); - messages.value.delete(oldest); - } -} - -const updateGrouping = () => { - - const isSimmilar = (messageA: ViewMessage, messageB: ViewMessage) => { - if ((Math.abs(messageA.time - messageB.time) < 20000) && (messageA.from == messageB.from) && (messageA.context == messageB.context)) { - return true - } - return false + if (target_message) { + target_message = message; } - const refs = Array.from(messages.value.keys()) - - // Get the last three messages. - const first = messages.value.get(refs[refs.length - 1]) - const second = messages.value.get(refs[refs.length - 2]) - const third = messages.value.get(refs[refs.length - 3]) - - // If messages are simmilar, update the classes. - if ((first) && (second)) { - if (isSimmilar(first, second)) { - first.isChild = "last" // i.e. last in group. - second.isChild = "first" - - if (third) { - if ((third.isChild == "first") || (third.isChild == "middle")) { - second.isChild = "middle" - } - } - } - } } export default function useMessages() { const { updateScrollRef, adjustScroll } = useScroll("messenger"); - /* Given new message object, update the view accordingly */ - const updateMessageView = (newMessages: Message[]) => { - - const bottom = updateScrollRef(); // see if the user is scrolled down - - // take each message and apply view - newMessages.forEach((message: Message) => { - - // add or update message depending - if (messages.value.has(message.uid)) { - updateMessage(message); - } else addMessage(message); - - pruneMessages(); // pop off old messages from view - updateGrouping(); // group like message together - - if (bottom) adjustScroll(); // only scroll if user was at bottom - - saveMessages(); - - }); - - } - return { - messages, - updateMessageView, - loadSavedMessages - } + canvas, + seedCanvas, + addMessage, + updateMessage + }; -} - - - // // Seed message view with message history. - // const prepMessageView = async (newMessages: Message[]) => { - // console.log("MSGR:Prepping messenger view.") - - // // Load and render saved messages and immediately scroll to bottom. - // await loadSavedMessages(); - // setTimeout(setScroll.bind(false), 10); - - // // Render new messages, then wait 1s to scroll. - // if (newMessages.length) { - - // console.log("MSGR:Adding new messages") - - // newMessages.forEach(message => { - // addMessage(message, true); - // }) - - // setTimeout(setScroll.bind(true), 1000); - - // } - // } \ No newline at end of file +} \ No newline at end of file diff --git a/src/render/components/messenger.vue b/src/render/components/messenger.vue index 77d54f3..4ea2d5b 100644 --- a/src/render/components/messenger.vue +++ b/src/render/components/messenger.vue @@ -11,6 +11,7 @@ v-for="message in messages" :text="message.text" :context="message.context" + :child :key="message[0]" />
@@ -28,7 +29,7 @@ import useMessages from "@/render/composables/messages"; export default defineComponent({ name: "Messenger", - props: ["state"], + props: ["profile", "messages"], components: { Message, @@ -43,12 +44,19 @@ export default defineComponent({ onMounted(() => { - /* populate the message view with existing messages */ - updateMessageView(state.savedMessages, state.newMessages); + /* seed messages */ + window.ipcRenderer.on("init-messages", (e_: any, payload: any) => { + seedMessages(payload.messages); + }); - /* wait and listen for new messages to come in */ - window.ipcRenderer.on("new-message", (_e: any, payload: any) => { - updateMessageView(payload.message); + /* add a new message */ + window.ipcRenderer.on("add-message", (_e: any, payload: any) => { + addMessage(payload.message); + }); + + /* update an existing message */ + window.ipcRenderer.on("update-message", (_e: any, payload: any) => { + updateMessage(payload.message); }); }); diff --git a/src/session.ts b/src/session.ts index 0a893d8..afebe5b 100644 --- a/src/session.ts +++ b/src/session.ts @@ -3,7 +3,11 @@ import useAudio from "@/audio"; -import { loadState, saveState, emitState } from "@/state"; +import Canvas from "@/composables/convas"; +import ipcEmit from "@/composables/emitter"; + +/* data structure of messages that's tied to the UI */ +let msgrState: UIState | null = null; /* start and stop audio functionality */ const { initAudio, closeAudio } = useAudio(); @@ -12,53 +16,57 @@ const { initAudio, closeAudio } = useAudio(); * Controls for interfacing with the platform. * Takes an onMessage callback which we define below. */ -const { connect, send, close } = usePlatform((message: Message) => { +const { connect, send, close } = useWebsockets((content: any) => { - /* add the message to the state */ - addMessage(message); + /* if the platform fails to authenticate, we must back down */ + if (content === "auth_error") { + deauthenticate(); + return; + } - /* push the message to the browser */ - if (win) emit("new-message", message); + /* on init, platform sends state, used to init canvas */ + if (isInitMessage(content)) { + uiState.set(content); + } + + + else if (isAddMessage(content)) { + uiState.add(content); + } + + else { + uiState.update(content); + } }); /* send a message to the platform */ export function sendMessage(message: Message) { - /* add the message to the state */ - addMessage(message); - - /* push the message to the browser */ - if (win) emit("new-message", message); - /* socket send */ send(message); } /* launch a new session (the main process for authenticated users) */ -export function launchSession(profile: Profile) { - - /* load any previously saved state for that user */ - loadState(profile); +export function launchSession(platformKey: string, crimata_id: string) { /* connect to the platform */ - connect(profile); + connect(PLATFORM_URL, platformKey); /* initialize the audio streams */ - // initAudio(); + initAudio(); - /* finally we can push state to browser */ - if (win) emitState(); + /* push profile to window */ + if (win) + ipcEmit("update-auth", crimata_id); } export function endSession() { - closeAudioStreams(); + closeAudio(); - closeSocket(); + close(); - state.clear(); - -} \ No newline at end of file +} diff --git a/src/state.ts b/src/state.ts deleted file mode 100644 index 1d6ce02..0000000 --- a/src/state.ts +++ /dev/null @@ -1,29 +0,0 @@ -const Store = require('electron-store'); - -/* simple data persistance */ -const store = new Store; - -/* state of the session (e.g. profile and messages for now) */ -let state: State | null = null; - -/* load saved state in electron store for given user */ -export function loadState(profile: Profile) { - state = store.get("state", null); -} - -/* add a message to state.messages */ -export function addMessage(message: Message) { - if (state) { - state.messages.push(message); - saveState(); - } -} - -export function saveState() { - store.set("state", state); -} - -export function emitState() { - emit("update-state", state); -} - diff --git a/src/types.ts b/src/types.ts index b437ffa..e8682b8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,8 +9,6 @@ interface Message { interface ViewMessage extends Message { child: string; - seen: boolean; - newMessage: boolean; } interface WindowState { @@ -26,11 +24,6 @@ interface Profile { initials: string; } -interface State { - profile: Profile | null; - messages -} - interface LoginPayload { email: string; password: string; From d851db2fccf166927d862756eb7ecaf0db3934d3 Mon Sep 17 00:00:00 2001 From: riqo Date: Mon, 14 Jun 2021 08:52:01 -0500 Subject: [PATCH 154/163] update sockets, account, auth --- build/config.gypi | 79 ------------------- package.json | 4 +- public/index.html | 6 +- src/account.ts | 105 +++++++++++++++++++++++++ src/api/account.ts | 49 ++++++------ src/auth.ts | 13 ++++ src/composables/http.ts | 7 +- src/composables/ipcHandler.ts | 50 ++++++++++++ src/composables/json.ts | 10 ++- src/composables/store.ts | 19 +++++ src/composables/websockets.ts | 49 +++++++++--- src/config.ts | 17 +++++ src/init.ts | 4 +- src/ipc/account.ts | 140 ++++++---------------------------- src/ipc/index.ts | 23 +++++- src/ipc/session.ts | 6 +- src/main.ts | 60 +++++---------- src/session.ts | 49 +++++++----- src/types.ts | 23 ++++++ src/window.ts | 20 ++--- 20 files changed, 409 insertions(+), 324 deletions(-) delete mode 100644 build/config.gypi create mode 100644 src/account.ts create mode 100644 src/auth.ts create mode 100644 src/composables/ipcHandler.ts create mode 100644 src/composables/store.ts create mode 100644 src/config.ts diff --git a/build/config.gypi b/build/config.gypi deleted file mode 100644 index 6f84ed7..0000000 --- a/build/config.gypi +++ /dev/null @@ -1,79 +0,0 @@ -# Do not edit. File was generated by node-gyp's "configure" step -{ - "target_defaults": { - "cflags": [], - "default_configuration": "Release", - "defines": [], - "include_dirs": [], - "libraries": [] - }, - "variables": { - "asan": 0, - "build_v8_with_gn": "false", - "coverage": "false", - "dcheck_always_on": 0, - "debug_nghttp2": "false", - "debug_node": "false", - "enable_lto": "false", - "enable_pgo_generate": "false", - "enable_pgo_use": "false", - "error_on_warn": "false", - "force_dynamic_crt": 0, - "host_arch": "x64", - "icu_data_in": "../../deps/icu-tmp/icudt67l.dat", - "icu_endianness": "l", - "icu_gyp_path": "tools/icu/icu-generic.gyp", - "icu_path": "deps/icu-small", - "icu_small": "false", - "icu_ver_major": "67", - "is_debug": 0, - "llvm_version": "0.0", - "napi_build_version": "6", - "node_byteorder": "little", - "node_debug_lib": "false", - "node_enable_d8": "false", - "node_install_npm": "true", - "node_module_version": 83, - "node_no_browser_globals": "false", - "node_prefix": "/", - "node_release_urlbase": "https://nodejs.org/download/release/", - "node_shared": "false", - "node_shared_brotli": "false", - "node_shared_cares": "false", - "node_shared_http_parser": "false", - "node_shared_libuv": "false", - "node_shared_nghttp2": "false", - "node_shared_openssl": "false", - "node_shared_zlib": "false", - "node_tag": "", - "node_target_type": "executable", - "node_use_bundled_v8": "true", - "node_use_dtrace": "true", - "node_use_etw": "false", - "node_use_node_code_cache": "true", - "node_use_node_snapshot": "true", - "node_use_openssl": "true", - "node_use_v8_platform": "true", - "node_with_ltcg": "false", - "node_without_node_options": "false", - "openssl_fips": "", - "openssl_is_fips": "false", - "shlib_suffix": "83.dylib", - "target_arch": "x64", - "v8_enable_31bit_smis_on_64bit_arch": 0, - "v8_enable_gdbjit": 0, - "v8_enable_i18n_support": 1, - "v8_enable_inspector": 1, - "v8_enable_pointer_compression": 0, - "v8_no_strict_aliasing": 1, - "v8_optimized_debug": 1, - "v8_promise_internal_field_count": 1, - "v8_random_seed": 0, - "v8_trace_maps": 0, - "v8_use_siphash": 1, - "want_separate_host_toolset": 0, - "xcode_version": "11.0", - "nodedir": "/Users/Enrique/Library/Caches/node-gyp/14.4.0", - "standalone_static_library": 1 - } -} diff --git a/package.json b/package.json index 38c1348..2b4b7fd 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "postinstall": "electron-builder install-app-deps", "postuninstall": "electron-builder install-app-deps" }, - "main": "background.js", + "main": "init.js", "dependencies": { "@google-cloud/speech": "^4.2.0", "@types/animejs": "^3.1.2", @@ -73,7 +73,7 @@ "lintOnSave": false, "pluginOptions": { "electronBuilder": { - "preload": "src/preload.ts", + "preload": "src/renderer/preload.ts", "builderOptions": { "appId": "com.crimata.ElectronUpdaterApp", "artifactName": "${productName}-${version}.${ext}", diff --git a/public/index.html b/public/index.html index 48809d8..8f79d27 100644 --- a/public/index.html +++ b/public/index.html @@ -11,11 +11,7 @@ - -
+
diff --git a/src/account.ts b/src/account.ts new file mode 100644 index 0000000..4884135 --- /dev/null +++ b/src/account.ts @@ -0,0 +1,105 @@ + +import { postAuth, postLogin, postLogout } from "@/api/account"; +import { endSession, launchSession } from "@/session"; +import { getToken, clearToken, setToken } from "@/composables/store"; +import { parseAuthRes } from "./auth"; + +export const accountAuth = async (): Promise => { + + /* attempt to get a login token from the store */ + const token = getToken(); + + /* try to login with it, returns platform secret and new token on success */ + if (token) { + try { + + const res = await postAuth(token); + + const parsed = parseAuthRes(res); + + setToken(parsed.token) + + return { + profile: parsed.profile, + token: parsed.token + }; + + } catch(e) { + console.log('[ACCOUNT]', e); + clearToken(); + throw(new Error('Failed to authenticate.')); + + } + } else { + throw(new Error('Unable to authenticate.')); + } +}; + +export const accountLogin: IpcHandlerCallback = async (payload) => { + const account = payload as AccountCredentials; + try { + + // attempt login with email password + const res = await postLogin(account.email, account.password); + const parsed = parseAuthRes(res); + + // save jwt token and profile + setToken(parsed.token) + + // launch session + launchSession(parsed.token) + + // return profile to renderer + return parsed.profile; + + } catch(e) { + throw e; + } +} + +// export const accountLogin = async (account: Account): Promise => { +// +// try { +// +// // attempt login with email password +// const res = await postLogin(account.email, account.password); +// const parsed = parseAuthRes(res); +// +// // save jwt token and profile +// setToken(parsed.token) +// +// // launch session +// launchSession(parsed.token) +// +// // return profile to renderer +// return parsed.profile; +// +// } catch(e) { +// console.log('[ACCOUNT]', e); +// throw (new Error('Failed to authenticate')); +// } +// +// } + +export const accountLogout = async (): Promise => { + + try { + // post logout to backend + await postLogout(); + + // remove key and crimataId + clearToken(); + + // kill crimata platform session + endSession(); + + return; + + } catch(e) { + console.log('[ACCOUNT]', e); + return (new Error('Failed to logout. Please try again.')); + } + +} + + diff --git a/src/api/account.ts b/src/api/account.ts index 98d3c69..a907dfb 100644 --- a/src/api/account.ts +++ b/src/api/account.ts @@ -1,36 +1,31 @@ import { useHttp } from "@/composables/http"; import axios from "axios"; +import {config} from "@/config"; const { post } = useHttp(); -export const usrPwdAuth = async (email: string, password: string) => { - - try { - return await post('/account/login', { email, password }) - - } catch (e) { - return null; - } - -} - -export const tokenAuth = async (cid: string, token: string) => { - - try { - return await axios({ - url: "http://127.0.0.1:3000/api/account/profile", - headers: { - Cookie: `jwt=${token}` - }, - method: 'GET', - data: { - cid, - } +export const postAuth = async (token: string) => ( + await axios({ + url: config.BUSINESS_URL + config.BUSINESS_PREFIX + '/account/authenticate', + headers: { + Cookie: `jwt=${token}` + }, + method: 'POST', }) +).data; + + +export const postLogin = async (email: string, password: string) => ( + await post('/account/login', { email, password }) +).data; + + +export const postLogout = + async (): Promise => (await post('/account/logout')); + + + + - } catch (e) { - return null; - } -} \ No newline at end of file diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..14ed36a --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,13 @@ + +export const parseAuthRes = (authRes: any) => { + const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string; + const profile = authRes.data as Profile; + return { + token, + profile + } +}; + + + + diff --git a/src/composables/http.ts b/src/composables/http.ts index a9519a8..e789afa 100644 --- a/src/composables/http.ts +++ b/src/composables/http.ts @@ -1,10 +1,8 @@ import axios, { AxiosRequestConfig } from 'axios'; +import {config} from "@/config"; -const preFix = '/api'; - -const baseURL = "http://127.0.0.1:3000" + preFix; - +const baseURL = config.BUSINESS_URL + config.BUSINESS_PREFIX; interface Request { endpoint: string; @@ -12,7 +10,6 @@ interface Request { config?: Record; } - const makeQuery = (reqQuery: Record) => { let result = ''; diff --git a/src/composables/ipcHandler.ts b/src/composables/ipcHandler.ts new file mode 100644 index 0000000..b588102 --- /dev/null +++ b/src/composables/ipcHandler.ts @@ -0,0 +1,50 @@ + +import { ipcMain, IpcMainInvokeEvent } from "electron"; + +export class IpcHandler implements IIpcHandler { + + readonly channel: string; + + readonly _handlerCallback: IpcHandlerCallback; + + constructor(options: { + channel: string; + handlerCallback: IpcHandlerCallback; + }) { + this.channel = options.channel; + this._handlerCallback = options.handlerCallback; + } + + handle() { + ipcMain.handle(this.channel, this._onInvoke); + } + + remove() { + ipcMain.removeHandler(this.channel); + } + + private async _onInvoke(_e: IpcMainInvokeEvent, payload?: string | null): Promise { + + return new Promise(async (resolve, reject) => { + + console.log(`[IPC]:${this.channel}`); + + try { + + const params = payload ? JSON.parse(payload) : null; + + const res = await this._handlerCallback(params); + + resolve(res as unknown as ReturnType); + + } catch(e) { + console.log(`[IPC]:${this.channel}`, e); + reject(e); + } + }); + } + +} + + + diff --git a/src/composables/json.ts b/src/composables/json.ts index 43b3804..06ab4b9 100644 --- a/src/composables/json.ts +++ b/src/composables/json.ts @@ -1,9 +1,13 @@ + +import {config} from "@/config"; +import fs from 'fs'; + export const saveToJson = (fileName: string, data: any) => { - fs.writeFile(configPath + fileName, JSON.stringify(data), (err) => { + fs.writeFile(config.configPath + fileName, JSON.stringify(data), (err) => { if (err) { console.log("Error when saving to json.") - } + } }) -} \ No newline at end of file +} diff --git a/src/composables/store.ts b/src/composables/store.ts new file mode 100644 index 0000000..88b9c17 --- /dev/null +++ b/src/composables/store.ts @@ -0,0 +1,19 @@ +const Store = require('electron-store'); + +const schema = { + key: { + type: 'string', + }, +}; + +const store = new Store({ + schema, + encryptionKey: "super user test" +}); + +export const getToken = (): string | undefined => (store.get("token")); + +export const clearToken = (): void => (store.delete("token")); + +export const setToken = (token: string): void => (store.set('token', token)); + diff --git a/src/composables/websockets.ts b/src/composables/websockets.ts index d034027..8208109 100644 --- a/src/composables/websockets.ts +++ b/src/composables/websockets.ts @@ -3,9 +3,18 @@ import WebSocket from 'ws'; -export default function useWebSockets(onMessageCallback: (s: string) => void) { - let socket: WebSocket | null = null; +const _connectionCheckTimeout = 4000; +const _reconnectTimeout = 1000; +let _connectionCheckInterval: ReturnType; + + +export default function useWebSockets( + messageCallback: (message: string) => void, + connectionStatusCallback: (alive: boolean) => void, +) { + + let socket: WebSocket; const send = async (data: Record): Promise => { return new Promise((resolve, reject) => { @@ -21,30 +30,52 @@ export default function useWebSockets(onMessageCallback: (s: string) => void) { const connect = (socketUrl: string, secret: string) => { + // avoid setting multiple interval; + if (_connectionCheckInterval) clearInterval(_connectionCheckInterval); + /* create a new socket */ - socket = new WebSocket(socketUrl) + socket = new WebSocket(socketUrl); /* add event listeners */ - socket.on("open", () => { - if (socket) + socket.send(secret); + + // ping server + _connectionCheckInterval = setInterval(() => { + + socket.ping(null, true, (e: Error) => { + if (e) { + socket.close(); + connectionStatusCallback(false); + setTimeout(() => connect(socketUrl, secret), _reconnectTimeout); + } + }); + + }, _connectionCheckTimeout); + }); socket.on("message", (event: WebSocket.MessageEvent) => { - onMessageCallback(event.data.toString()) + messageCallback(event.data.toString()) }); - socket.on("close", () => { - return + socket.on("close", (event: WebSocket.CloseEvent) => { + connectionStatusCallback(false); + clearInterval(_connectionCheckInterval); + if (!event.wasClean) { + setTimeout(() => connect(socketUrl, secret), _reconnectTimeout); + } + }); + socket.on("pong", () => connectionStatusCallback(true)); + } const close = () => { if (socket) { socket.close(); - socket = null; } } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..b0eb27e --- /dev/null +++ b/src/config.ts @@ -0,0 +1,17 @@ + +import { app } from "electron"; + +const env = process.env; + +const PLATFORM_PORT = env.PLATFORM_PORT || 8760; +const PLATFORM_IP = env.PLATFORM_IP || 'http://127.0.0.1'; + +const BUSINESS_PORT = env.BUSINESS_PORT || 3010; +const BUSINESS_IP = env.BUSINESS_IP || 'http://127.0.0.1'; + +export const config = { + PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`, + BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`, + BUSINESS_PREFIX: '/api', + configPath: app.getPath('userData') +} diff --git a/src/init.ts b/src/init.ts index 3d0b721..e68e766 100644 --- a/src/init.ts +++ b/src/init.ts @@ -9,8 +9,6 @@ import { app, protocol } from "electron"; import createWindow from "./window"; import main from "./main"; -require('dotenv').config(); - console.log('Starting Crimata electron app.'); // Scheme must be registered before the app is ready @@ -43,4 +41,4 @@ if (isDev) { process.on("SIGTERM", () => { app.quit(); }); -} \ No newline at end of file +} diff --git a/src/ipc/account.ts b/src/ipc/account.ts index 3860019..531d1a7 100644 --- a/src/ipc/account.ts +++ b/src/ipc/account.ts @@ -1,126 +1,32 @@ "use strict"; -import { submit, fetchProfile, logout } from "../api/account"; -import { ipcMain, IpcMainInvokeEvent } from "electron"; -import { store } from "@/composables/store"; +import { accountLogin, accountLogout } from "@/account"; +import {IpcHandler} from "@/composables/ipcHandler"; +const LOGIN_CHANNEL = "account-login"; +const LOGOUT_CHANNEL = "account-logout"; -const parseAuthRes = (authRes: any) => { - const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string; - const profile = authRes.data as Profile; - return { - token, - profile - } -}; +const loginHandler = new IpcHandler({ + channel: LOGIN_CHANNEL, + handlerCallback: accountLogin +}); +const logoutHandler = new IpcHandler({ + channel: LOGOUT_CHANNEL, + handlerCallback: accountLogout +}); +const handlers = [loginHandler, logoutHandler]; +export default handlers; - -/** - * Get user profile from store and try to login with it. - */ -const onTokenLogin = async ( - _event: IpcMainInvokeEvent, - _payload: null -): Promise => ( - - new Promise(async (resolve, reject) => { - console.log('[IPC]: user-profile'); - - // get jwt token and crimataId from store - const token = store.get('key'); - const crimataId = store.get('crimataId'); - - // authenticate and fetch profile - try { - - // attempt login with email token - const res = await fetchProfile(crimataId, token); - const parsed = parseAuthRes(res); - - // return profile to renderer - resolve(parsed.profile); - - } catch(e) { - reject(new Error('Failed to fetch profile.')); - } - }) -) - - -const onLogin = async ( - _event: IpcMainInvokeEvent, - payload: string -): Promise => ( - - new Promise(async (resolve, reject) => { - console.log('[IPC]: user-login'); - - const account = JSON.parse(payload); - - if ( account.password && account.email ) { - try { - - // attempt login with email password - const res = await submit(account.email, account.password); - const parsed = parseAuthRes(res); - - // save jwt token and profile - store.set('key', parsed.token); - store.set('crimataId', parsed.profile.crimataId); - - // init session - - // return profile to renderer - resolve(parsed.profile); - - } catch(e) { - console.log('[API]', e); - reject(new Error('Failed to authenticate')); - } - } - }) -) - -const onLogout = async ( - _event: IpcMainInvokeEvent, - _payload: null -): Promise => ( - - new Promise(async (resolve, reject) => { - console.log('[IPC]: user-logout'); - - try { - // post logout to backend - await logout(); - - // remove key and crimataId - store.delete('key'); - store.delete('crimataId'); - - // TODO: kill crimata platform session - // endSession(); - - resolve(); - } catch(e) { - reject(new Error('Failed to logout. Please try again.')); - } - }) -) - - -export default function useAccountListeners(): void { - - ipcMain.removeHandler("user-profile"); - ipcMain.handle("user-profile", onProfile); - - ipcMain.removeHandler("user-login"); - ipcMain.handle("user-login", onLogin); - - ipcMain.removeHandler("user-logout"); - ipcMain.handle("user-logout", onLogout); - -} +// export default function useAccountListeners(): void { +// +// ipcMain.removeHandler(LOGIN_HANDLER); +// ipcMain.handle(LOGIN_HANDLER, onLogin); +// +// ipcMain.removeHandler(LOGOUT_HANDLER); +// ipcMain.handle(LOGOUT_HANDLER, onLogout); +// +// } diff --git a/src/ipc/index.ts b/src/ipc/index.ts index 9152ef2..73a5901 100644 --- a/src/ipc/index.ts +++ b/src/ipc/index.ts @@ -1,17 +1,36 @@ "use strict"; -import useAccountListeners from "./account"; +import { IpcHandler } from "@/composables/ipcHandler"; +import handlers from "./account"; import useSessionListeners from "./session"; // import useAudioListeners from "./audio"; +interface IPCHandlers { + [channel: string]: IpcHandler; +} + +const ipcHandlers: IPCHandlers = {}; + +const _initHandlers = () => { + handlers.forEach((h) => { + if (!(h.channel in ipcHandlers)) { + ipcHandlers[h.channel] = h; + h.handle(); + } + }); +} export default function useIpc(): void { - useAccountListeners(); + _initHandlers(); + + // useAccountListeners(); useSessionListeners(); // useAudioListeners(); } + + diff --git a/src/ipc/session.ts b/src/ipc/session.ts index fa50aa1..bbaf0e9 100644 --- a/src/ipc/session.ts +++ b/src/ipc/session.ts @@ -10,11 +10,11 @@ function onSendMessage(_event: IpcMainEvent, payload: Message): void { } // Login attempt, returns success or not. -function onLogin(_event: IpcMainEvent, payload: LoginPayload) => { - authenticate(payload.email, payload.password); +function onLogin(_event: IpcMainEvent, payload: LoginPayload): void { + console.log('hello') } export default function useSessionListeners(): void { ipcMain.removeAllListeners("client-message"); ipcMain.on("client-message", onSendMessage); -} \ No newline at end of file +} diff --git a/src/main.ts b/src/main.ts index 9d6414c..b015cd9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,41 +1,21 @@ /** * Where the background logic really begins, gets called by app.onReady(). - * + * * Handles authentication. If profile is set, we launch a session, which consis * of opening a connection with the platform, initializing the audio streams. - * + * * The session is primarily an interface between the frontend and the platform, * relaying messages from one to the other. - * + * */ -import { tokenAuth, usrPwdAuth } from "@/api/account"; -import { launchSession, endSession } from "@/session"; import useIpc from "@/ipc/index"; -import store from "@/composables/store"; +import { accountAuth } from "./account"; +import { launchSession } from "./session"; +import ipcEmit from "./composables/emitter"; +import createWindow from "./window"; -/* authenticate the user */ -export async function authenticate(email: string, password: string) { - - /* attempt normal login */ - const platformKey, token, crimataId = await usrPwdAuth(email, password); - - /* launch if successful */ - if (platformKey) - launchSession(platformKey, crimataId); - - /* save the token */ - store.set("token", token); - -} - -/* logout the user, end the session */ -export function deauthenticate() { - - /* terminate the session */ - endSession(); - -} +let authState: AuthState | null; export default async function main() { @@ -45,18 +25,14 @@ export default async function main() { /* launch browser window */ await createWindow(); - /* attempt to get a login token from the store */ - const token = store.get("token"); + try { + authState = await accountAuth() as AuthState; + } catch(e) { + console.log('AUTH:', e); + authState = null; + } finally { + if (authState) launchSession(authState.token as string); + ipcEmit("set-profile", authState?.profile); + } - /* try to login with it, returns platform secret and new token on success */ - if (token) - const newToken, profile = await tokenAuth(token); - - /* if secret, we launch a session */ - if (newToken) - launchSession(newToken, profile); - - /* finally, save the most recent token */ - store.set("token", newToken); - -} \ No newline at end of file +} diff --git a/src/session.ts b/src/session.ts index afebe5b..158c41e 100644 --- a/src/session.ts +++ b/src/session.ts @@ -2,24 +2,34 @@ -import useAudio from "@/audio"; -import Canvas from "@/composables/convas"; +// import useAudio from "@/audio"; import ipcEmit from "@/composables/emitter"; +import useWebsockets from "./composables/websockets"; +import {config} from "@/config"; /* data structure of messages that's tied to the UI */ -let msgrState: UIState | null = null; +const uiState: any | null = null; /* start and stop audio functionality */ -const { initAudio, closeAudio } = useAudio(); +// const { initAudio, closeAudio } = useAudio(); + +let isInitMessage: any; + +let deauthenticate: any; +let isAddMessage: any /** * Controls for interfacing with the platform. * Takes an onMessage callback which we define below. */ -const { connect, send, close } = useWebsockets((content: any) => { + + +const onMessageCallback = (message: string) => { + + const content = JSON.parse(message); /* if the platform fails to authenticate, we must back down */ - if (content === "auth_error") { + if (content === "CLOSE_AUTH_FAIL") { deauthenticate(); return; } @@ -28,8 +38,8 @@ const { connect, send, close } = useWebsockets((content: any) => { if (isInitMessage(content)) { uiState.set(content); } - - + + else if (isAddMessage(content)) { uiState.add(content); } @@ -37,10 +47,15 @@ const { connect, send, close } = useWebsockets((content: any) => { else { uiState.update(content); } +} -}); +const onConnectionStatusCallback = (alive: boolean) => { + ipcEmit('connection-state', alive); +} -/* send a message to the platform */ +const { connect, send, close } = useWebsockets(onMessageCallback, onConnectionStatusCallback); + +/* send a message to the platform */ export function sendMessage(message: Message) { /* socket send */ @@ -48,24 +63,22 @@ export function sendMessage(message: Message) { } + /* launch a new session (the main process for authenticated users) */ -export function launchSession(platformKey: string, crimata_id: string) { +export function launchSession(platformKey: string) { /* connect to the platform */ - connect(PLATFORM_URL, platformKey); + connect(config.PLATFORM_URL, platformKey); /* initialize the audio streams */ - initAudio(); - - /* push profile to window */ - if (win) - ipcEmit("update-auth", crimata_id); + // initAudio(); } + export function endSession() { - closeAudio(); + // closeAudio(); close(); diff --git a/src/types.ts b/src/types.ts index e8682b8..583371e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ + interface Message { text: boolean | string; context: boolean | string; @@ -28,3 +29,25 @@ interface LoginPayload { email: string; password: string; } + +interface AuthState { + profile: Profile | null; + token: string | null; +} + +interface AccountCredentials { + email: string; + password: string; +} + +interface IpcHandlerCallback { + (payload: I | null): Promise; +} + +interface IIpcHandler { + handle(): void; + remove(): void; + readonly _handlerCallback: IpcHandlerCallback; +} + + diff --git a/src/window.ts b/src/window.ts index 4c69387..807064a 100644 --- a/src/window.ts +++ b/src/window.ts @@ -1,10 +1,12 @@ "use strict"; -import { BrowserWindow, ipcMain } from "electron"; +import { BrowserWindow, ipcMain, app } from "electron"; import { createProtocol } from "vue-cli-plugin-electron-builder/lib"; -import { backgroundMitt } from './coposables/emitter'; -import { saveToJson } from "./coposables/json"; +import { backgroundMitt } from './composables/emitter'; +import { saveToJson } from "./composables/json"; import * as path from "path"; +import fs from 'fs'; +import { config } from "@/config"; const { autoUpdater } = require('electron-updater'); interface IpcRendererPayload { @@ -19,8 +21,8 @@ const loadWinState = (fileName: string): WindowState => { let state: WindowState; try { - state = JSON.parse(fs.readFileSync(configPath + fileName).toString()); - } + state = JSON.parse(fs.readFileSync(config.configPath + fileName).toString()); + } catch (error) { state = { @@ -32,8 +34,8 @@ const loadWinState = (fileName: string): WindowState => { } return state - -} + +} // Called when a NavBar button is pressed. const onNavBar = (_event: any, action: string): void => { @@ -110,8 +112,8 @@ export default async function createWindow(): Promise { win = new BrowserWindow({ width: winState.width, height: winState.height, - x: winState.x, - y: winState.y, + x: winState.x as number, + y: winState.y as number, resizable: true, backgroundColor: '#EBEBEB', frame: false, From 0a3c9f71d81f54f0d1784c68b26b34e4f32ffe9b Mon Sep 17 00:00:00 2001 From: riqo Date: Tue, 15 Jun 2021 08:48:14 -0500 Subject: [PATCH 155/163] rename composable files to composable notation --- src/audio.ts | 187 ++++++++++++++++++ src/composables/audio.ts | 187 ------------------ src/composables/{emitter.ts => useEmitter.ts} | 0 src/composables/{http.ts => useHttp.ts} | 0 .../{ipcHandler.ts => useIpcMain.ts} | 0 .../{canvas.ts => useMessageCanvas.ts} | 0 src/composables/{json.ts => useSaveToJSON.ts} | 0 .../{websockets.ts => useWebsockets.ts} | 0 .../controllers/messenger.control.ts | 12 +- src/render/components/messenger.vue | 15 +- src/render/components/settings.vue | 2 +- .../{draggify.ts => useDraggify.ts} | 0 .../composables/{ipc.ts => useIpcRend.ts} | 0 src/render/composables/useMessages.ts | 0 .../composables/{auth.ts => useProfile.ts} | 0 .../composables/{scroll.ts => useScroll.ts} | 0 src/render/main.ts | 16 ++ src/{composables => }/store.ts | 0 18 files changed, 217 insertions(+), 202 deletions(-) delete mode 100644 src/composables/audio.ts rename src/composables/{emitter.ts => useEmitter.ts} (100%) rename src/composables/{http.ts => useHttp.ts} (100%) rename src/composables/{ipcHandler.ts => useIpcMain.ts} (100%) rename src/composables/{canvas.ts => useMessageCanvas.ts} (100%) rename src/composables/{json.ts => useSaveToJSON.ts} (100%) rename src/composables/{websockets.ts => useWebsockets.ts} (100%) rename src/render/composables/{draggify.ts => useDraggify.ts} (100%) rename src/render/composables/{ipc.ts => useIpcRend.ts} (100%) create mode 100644 src/render/composables/useMessages.ts rename src/render/composables/{auth.ts => useProfile.ts} (100%) rename src/render/composables/{scroll.ts => useScroll.ts} (100%) create mode 100644 src/render/main.ts rename src/{composables => }/store.ts (100%) diff --git a/src/audio.ts b/src/audio.ts index e69de29..844e791 100644 --- a/src/audio.ts +++ b/src/audio.ts @@ -0,0 +1,187 @@ +/* eslint @typescript-eslint/no-var-requires: "off" */ + +"use strict"; + +// where the audio goes +let buffer: ArrayBuffer[] = []; + +// place audio data in buffer +export function collect (chunk: ArrayBuffer) { + buffer.push(chunk); +} + +// return audio and clear buffer +export function flush () { + const bufferCopy = buffer; + buffer = []; + return bufferCopy; +} + +// import { backgroundMitt } from '@/modules/emitter'; +// const portAudio = require('naudiodon'); + +// // Audio in and out stream objects. +// let ai: typeof portAudio.AudioIO | boolean = false; +// let ao: typeof portAudio.AudioIO | boolean = false; + +// // Whether activly recording. +// let record = false; + +// const audioContainer = { +// input: '', +// } + +// const audioOptions = { +// channelCount: 1, +// sampleFormat: 16, +// sampleRate: 16000, +// deviceId: -1, +// closeOnError: false, +// } + +// export const toggleRecord = (): void => { record = !record }; + + +// export const fetchAudioInput = (): Promise => ( + +// new Promise((resolve, reject) => { + +// try { +// resolve(audioContainer.input); +// toggleRecord(); +// } catch (e) { +// reject(new Error('Failed to fetch the audio.')) +// } + +// }) +// ) + + +// // Main audio function run by run.ts module. +// export function initAudioIO(): void { +// console.log("AUDIO:Starting io streams.") + +// if (!ai) { + +// // Initialize and start input stream. +// ai = new portAudio.AudioIO({ inOptions: audioOptions }); +// ai.setEncoding("hex"); +// ai.start(); + +// // On each data chunk... +// ai.on('data', (chunk: string) => { + +// // If recording, we capture the data. +// if (record) { +// console.log('AUDIO:Recording...') +// audioContainer.input += chunk; +// } + +// // Else, we don't capture and also clear audioContainer. +// else { +// if (audioContainer.input.length) { +// audioContainer.input = ""; +// } +// } + +// }); +// } + +// if (!ao) { + +// // Initialize and start input stream. +// ao = new portAudio.AudioIO({ outOptions: audioOptions }); +// ao.start(); + +// } +// } + + +// // ---Audio playback-------------------------------------------- + +// // Split Buffer into an array of len-sized Buffers. +// function bufSplit(buf: Buffer, len: number): Array { +// const chunks = []; +// let i = 0; +// let L = len; + +// while(i < buf.byteLength) { +// chunks.push(buf.slice(i, L)); +// i = L; +// L += len; +// } + +// return chunks; +// } + +// // Audio playback. +// export function play(input: string): void { + +// // Format the audio. +// const audio = bufSplit( +// Buffer.from(input as string, 'hex'), +// 8192 +// ); + +// // Called on end of write. +// const callback = () => { + +// // We stop audio playback anim. +// backgroundMitt.emit('ipc-renderer', { +// endpoint: 'stop-playback-anim' +// }); + +// } + +// write(); + +// // Iterate through audio array and write buffers to portAudio writable. +// function write() { +// let chunk: Buffer; +// let ok = true; +// let i = 0; + +// do { +// chunk = audio[i]; +// if (i === audio.length - 1) { +// // write last chunk. +// ao.write(chunk, null, callback); +// } else { +// // check for backpreassure. +// ok = ao.write(chunk, null); +// } +// i++; +// } while (i < audio.length && ok); + +// if (i < audio.length) { +// // Had to stop early! +// // Write some more once it drains. +// ao.once('drain', write); +// } +// } +// } + +// // ------------------------------------------------------------- + +// // Get's called on window close. +// export async function stopStream() { +// console.log("AUDIO:Stopping audio stream.") +// if (ai) { +// try { +// await ai.quit() +// } catch(e){ +// console.log('AUDIO: Failed to shutdown audio input.'); +// throw e; +// } +// } +// if (ao) { +// try { +// await ao.quit() +// } catch(e){ +// console.log('AUDIO: Failed to shutdown audio output.'); +// throw e; +// } +// } +// } + + diff --git a/src/composables/audio.ts b/src/composables/audio.ts deleted file mode 100644 index 844e791..0000000 --- a/src/composables/audio.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* eslint @typescript-eslint/no-var-requires: "off" */ - -"use strict"; - -// where the audio goes -let buffer: ArrayBuffer[] = []; - -// place audio data in buffer -export function collect (chunk: ArrayBuffer) { - buffer.push(chunk); -} - -// return audio and clear buffer -export function flush () { - const bufferCopy = buffer; - buffer = []; - return bufferCopy; -} - -// import { backgroundMitt } from '@/modules/emitter'; -// const portAudio = require('naudiodon'); - -// // Audio in and out stream objects. -// let ai: typeof portAudio.AudioIO | boolean = false; -// let ao: typeof portAudio.AudioIO | boolean = false; - -// // Whether activly recording. -// let record = false; - -// const audioContainer = { -// input: '', -// } - -// const audioOptions = { -// channelCount: 1, -// sampleFormat: 16, -// sampleRate: 16000, -// deviceId: -1, -// closeOnError: false, -// } - -// export const toggleRecord = (): void => { record = !record }; - - -// export const fetchAudioInput = (): Promise => ( - -// new Promise((resolve, reject) => { - -// try { -// resolve(audioContainer.input); -// toggleRecord(); -// } catch (e) { -// reject(new Error('Failed to fetch the audio.')) -// } - -// }) -// ) - - -// // Main audio function run by run.ts module. -// export function initAudioIO(): void { -// console.log("AUDIO:Starting io streams.") - -// if (!ai) { - -// // Initialize and start input stream. -// ai = new portAudio.AudioIO({ inOptions: audioOptions }); -// ai.setEncoding("hex"); -// ai.start(); - -// // On each data chunk... -// ai.on('data', (chunk: string) => { - -// // If recording, we capture the data. -// if (record) { -// console.log('AUDIO:Recording...') -// audioContainer.input += chunk; -// } - -// // Else, we don't capture and also clear audioContainer. -// else { -// if (audioContainer.input.length) { -// audioContainer.input = ""; -// } -// } - -// }); -// } - -// if (!ao) { - -// // Initialize and start input stream. -// ao = new portAudio.AudioIO({ outOptions: audioOptions }); -// ao.start(); - -// } -// } - - -// // ---Audio playback-------------------------------------------- - -// // Split Buffer into an array of len-sized Buffers. -// function bufSplit(buf: Buffer, len: number): Array { -// const chunks = []; -// let i = 0; -// let L = len; - -// while(i < buf.byteLength) { -// chunks.push(buf.slice(i, L)); -// i = L; -// L += len; -// } - -// return chunks; -// } - -// // Audio playback. -// export function play(input: string): void { - -// // Format the audio. -// const audio = bufSplit( -// Buffer.from(input as string, 'hex'), -// 8192 -// ); - -// // Called on end of write. -// const callback = () => { - -// // We stop audio playback anim. -// backgroundMitt.emit('ipc-renderer', { -// endpoint: 'stop-playback-anim' -// }); - -// } - -// write(); - -// // Iterate through audio array and write buffers to portAudio writable. -// function write() { -// let chunk: Buffer; -// let ok = true; -// let i = 0; - -// do { -// chunk = audio[i]; -// if (i === audio.length - 1) { -// // write last chunk. -// ao.write(chunk, null, callback); -// } else { -// // check for backpreassure. -// ok = ao.write(chunk, null); -// } -// i++; -// } while (i < audio.length && ok); - -// if (i < audio.length) { -// // Had to stop early! -// // Write some more once it drains. -// ao.once('drain', write); -// } -// } -// } - -// // ------------------------------------------------------------- - -// // Get's called on window close. -// export async function stopStream() { -// console.log("AUDIO:Stopping audio stream.") -// if (ai) { -// try { -// await ai.quit() -// } catch(e){ -// console.log('AUDIO: Failed to shutdown audio input.'); -// throw e; -// } -// } -// if (ao) { -// try { -// await ao.quit() -// } catch(e){ -// console.log('AUDIO: Failed to shutdown audio output.'); -// throw e; -// } -// } -// } - - diff --git a/src/composables/emitter.ts b/src/composables/useEmitter.ts similarity index 100% rename from src/composables/emitter.ts rename to src/composables/useEmitter.ts diff --git a/src/composables/http.ts b/src/composables/useHttp.ts similarity index 100% rename from src/composables/http.ts rename to src/composables/useHttp.ts diff --git a/src/composables/ipcHandler.ts b/src/composables/useIpcMain.ts similarity index 100% rename from src/composables/ipcHandler.ts rename to src/composables/useIpcMain.ts diff --git a/src/composables/canvas.ts b/src/composables/useMessageCanvas.ts similarity index 100% rename from src/composables/canvas.ts rename to src/composables/useMessageCanvas.ts diff --git a/src/composables/json.ts b/src/composables/useSaveToJSON.ts similarity index 100% rename from src/composables/json.ts rename to src/composables/useSaveToJSON.ts diff --git a/src/composables/websockets.ts b/src/composables/useWebsockets.ts similarity index 100% rename from src/composables/websockets.ts rename to src/composables/useWebsockets.ts diff --git a/src/render/components/controllers/messenger.control.ts b/src/render/components/controllers/messenger.control.ts index 9ae7d47..a0f9b09 100644 --- a/src/render/components/controllers/messenger.control.ts +++ b/src/render/components/controllers/messenger.control.ts @@ -1,20 +1,20 @@ import { ref } from 'vue'; import useScroll from "@/render/composables/scroll"; -const canvas = ref(); +const messagesRef = ref(); /* seed the canvas with messages */ const seedCanvas = (messages: Message[]) => { - canvas.value = messages; + messagesRef.value = messages; } const addMessage = (message: Message) => { - canvas.value.push(message); + messagesRef.value.push(message); } const updateMessage = (message: Message) => { - let target_message = canvas.value.filter((m: Message) => { + let target_message = messagesRef.value.filter((m: Message) => { return m.uid = message.uid; })[0]; @@ -29,10 +29,10 @@ export default function useMessages() { const { updateScrollRef, adjustScroll } = useScroll("messenger"); return { - canvas, + messagesRef, seedCanvas, addMessage, updateMessage }; -} \ No newline at end of file +} diff --git a/src/render/components/messenger.vue b/src/render/components/messenger.vue index 4ea2d5b..9bc6448 100644 --- a/src/render/components/messenger.vue +++ b/src/render/components/messenger.vue @@ -8,10 +8,9 @@
@@ -24,7 +23,7 @@ import { defineComponent, onMounted, onUnmounted } from "vue"; import Message from "@/render/components/message.vue"; import InputItem from "@/render/components/inputItem.vue"; import Settings from "@/render/components/settings.vue"; -import useMessages from "@/render/composables/messages"; +import useMessages from "./controllers/messenger.control"; export default defineComponent({ name: "Messenger", @@ -40,23 +39,23 @@ export default defineComponent({ setup(props) { // Handle messages in view. - const { messages, updateMessageView } = useMessages(); + const { messagesRef, updateMessageView } = useMessages(); onMounted(() => { /* seed messages */ window.ipcRenderer.on("init-messages", (e_: any, payload: any) => { - seedMessages(payload.messages); + // seedMessages(payload.messages); }); /* add a new message */ window.ipcRenderer.on("add-message", (_e: any, payload: any) => { - addMessage(payload.message); + // addMessage(payload.message); }); /* update an existing message */ window.ipcRenderer.on("update-message", (_e: any, payload: any) => { - updateMessage(payload.message); + // updateMessage(payload.message); }); }); @@ -66,7 +65,7 @@ export default defineComponent({ }); return { - messages + messagesRef }; }, diff --git a/src/render/components/settings.vue b/src/render/components/settings.vue index 7c4b9ce..9b4f3e5 100644 --- a/src/render/components/settings.vue +++ b/src/render/components/settings.vue @@ -31,7 +31,7 @@ import { useIpc } from "@/modules/ipc"; import { logoutRequest } from '@/modules/message'; import { useProfile } from "@/modules/auth" - import { invokeLogout } from "@/ipcRend/account"; + import { invokeLogout } from "@/render/ipc"; export default defineComponent({ name: "Settings", diff --git a/src/render/composables/draggify.ts b/src/render/composables/useDraggify.ts similarity index 100% rename from src/render/composables/draggify.ts rename to src/render/composables/useDraggify.ts diff --git a/src/render/composables/ipc.ts b/src/render/composables/useIpcRend.ts similarity index 100% rename from src/render/composables/ipc.ts rename to src/render/composables/useIpcRend.ts diff --git a/src/render/composables/useMessages.ts b/src/render/composables/useMessages.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/render/composables/auth.ts b/src/render/composables/useProfile.ts similarity index 100% rename from src/render/composables/auth.ts rename to src/render/composables/useProfile.ts diff --git a/src/render/composables/scroll.ts b/src/render/composables/useScroll.ts similarity index 100% rename from src/render/composables/scroll.ts rename to src/render/composables/useScroll.ts diff --git a/src/render/main.ts b/src/render/main.ts new file mode 100644 index 0000000..50a594a --- /dev/null +++ b/src/render/main.ts @@ -0,0 +1,16 @@ + +// src/main.ts + +import App from "./App.vue"; + +import mitt from "mitt"; +import { createApp } from "vue"; + + +// Handle events. +const emitter = mitt(); + +const app = createApp(App) + +app.provide("mitt", emitter) +app.mount("#app"); diff --git a/src/composables/store.ts b/src/store.ts similarity index 100% rename from src/composables/store.ts rename to src/store.ts From d963f7d8a8e6badf4b376d1300772fbc8354202d Mon Sep 17 00:00:00 2001 From: riqo Date: Wed, 16 Jun 2021 08:42:47 -0500 Subject: [PATCH 156/163] initial working version --- package.json | 4 ++- src/account.ts | 25 +------------------ src/api/account.ts | 2 +- src/composables/useEmitter.ts | 9 ++++--- src/composables/useHttp.ts | 2 +- src/init.ts | 8 ++++++ src/ipc/account.ts | 2 +- src/main.ts | 10 +++++--- src/render/App.vue | 6 ++--- src/render/components/controllers/helpers.ts | 2 +- .../controllers/inputItem.control.audio.ts | 8 +++--- .../controllers/inputItem.control.text.ts | 8 +++--- .../controllers/messenger.control.ts | 2 +- src/render/components/header.vue | 18 +++++++++---- src/render/components/inputItem.vue | 23 +++++++---------- src/render/components/login.vue | 19 +++++++------- src/render/components/messenger.vue | 4 +-- src/render/components/settings.vue | 8 ++---- src/render/composables/useProfile.ts | 2 ++ src/render/ipc.ts | 18 ++++++------- src/session.ts | 4 +-- src/types.ts | 1 - src/window.ts | 4 +-- 23 files changed, 89 insertions(+), 100 deletions(-) diff --git a/package.json b/package.json index 2b4b7fd..7fff91a 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,9 @@ "lintOnSave": false, "pluginOptions": { "electronBuilder": { - "preload": "src/renderer/preload.ts", + "mainProcessFile": "./src/init.ts", + "rendererProcessFile": "./src/render/main.ts", + "preload": "./src/render/preload.ts", "builderOptions": { "appId": "com.crimata.ElectronUpdaterApp", "artifactName": "${productName}-${version}.${ext}", diff --git a/src/account.ts b/src/account.ts index 4884135..95fe629 100644 --- a/src/account.ts +++ b/src/account.ts @@ -1,7 +1,7 @@ import { postAuth, postLogin, postLogout } from "@/api/account"; import { endSession, launchSession } from "@/session"; -import { getToken, clearToken, setToken } from "@/composables/store"; +import { getToken, clearToken, setToken } from "./store"; import { parseAuthRes } from "./auth"; export const accountAuth = async (): Promise => { @@ -57,29 +57,6 @@ export const accountLogin: IpcHandlerCallback = asy } } -// export const accountLogin = async (account: Account): Promise => { -// -// try { -// -// // attempt login with email password -// const res = await postLogin(account.email, account.password); -// const parsed = parseAuthRes(res); -// -// // save jwt token and profile -// setToken(parsed.token) -// -// // launch session -// launchSession(parsed.token) -// -// // return profile to renderer -// return parsed.profile; -// -// } catch(e) { -// console.log('[ACCOUNT]', e); -// throw (new Error('Failed to authenticate')); -// } -// -// } export const accountLogout = async (): Promise => { diff --git a/src/api/account.ts b/src/api/account.ts index a907dfb..59872c8 100644 --- a/src/api/account.ts +++ b/src/api/account.ts @@ -1,5 +1,5 @@ -import { useHttp } from "@/composables/http"; +import useHttp from "@/composables/useHttp"; import axios from "axios"; import {config} from "@/config"; diff --git a/src/composables/useEmitter.ts b/src/composables/useEmitter.ts index ee6bb5e..240c557 100644 --- a/src/composables/useEmitter.ts +++ b/src/composables/useEmitter.ts @@ -1,15 +1,16 @@ -/* eslint-disable */ // Backend emitter - +// const EventEmitter = require('events'); class BackgroundMitt extends EventEmitter { } export const backgroundMitt = new BackgroundMitt(); -export default function ipcEmit (channel: string, payload: any) { +export const ipcEmit = (channel: string, payload: any) => { backgroundMitt.emit('ipc-renderer', { endpoint: channel, message: payload }); -} +}; + + diff --git a/src/composables/useHttp.ts b/src/composables/useHttp.ts index e789afa..6b9ee56 100644 --- a/src/composables/useHttp.ts +++ b/src/composables/useHttp.ts @@ -22,7 +22,7 @@ const makeQuery = (reqQuery: Record) => { }; -export const useHttp = () => { +export default function useHttp() { const api = axios.create({ baseURL, diff --git a/src/init.ts b/src/init.ts index e68e766..9a0fdf6 100644 --- a/src/init.ts +++ b/src/init.ts @@ -8,6 +8,7 @@ import { app, protocol } from "electron"; import createWindow from "./window"; import main from "./main"; +import { backgroundMitt } from '@/composables/useEmitter'; console.log('Starting Crimata electron app.'); @@ -18,6 +19,13 @@ protocol.registerSchemesAsPrivileged([ const isDev = require('electron-is-dev'); +let win: boolean; + +// Listen for window creation. +backgroundMitt.on('window-active', (state: boolean) => { + win = state; +}); + /* Start main process on ready */ app.on("ready", async () => { await main(); diff --git a/src/ipc/account.ts b/src/ipc/account.ts index 531d1a7..7759d12 100644 --- a/src/ipc/account.ts +++ b/src/ipc/account.ts @@ -2,7 +2,7 @@ "use strict"; import { accountLogin, accountLogout } from "@/account"; -import {IpcHandler} from "@/composables/ipcHandler"; +import {IpcHandler} from "@/composables/useIpcMain"; const LOGIN_CHANNEL = "account-login"; const LOGOUT_CHANNEL = "account-logout"; diff --git a/src/main.ts b/src/main.ts index b015cd9..9103cae 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,7 +12,7 @@ import useIpc from "@/ipc/index"; import { accountAuth } from "./account"; import { launchSession } from "./session"; -import ipcEmit from "./composables/emitter"; +import { ipcEmit } from "./composables/useEmitter"; import createWindow from "./window"; let authState: AuthState | null; @@ -31,8 +31,12 @@ export default async function main() { console.log('AUTH:', e); authState = null; } finally { - if (authState) launchSession(authState.token as string); - ipcEmit("set-profile", authState?.profile); + let profile = null; + if (authState) { + launchSession(authState.token as string); + profile = authState.profile; + } + ipcEmit("set-profile", profile); } } diff --git a/src/render/App.vue b/src/render/App.vue index d90bcc0..dfdaeb9 100644 --- a/src/render/App.vue +++ b/src/render/App.vue @@ -21,8 +21,8 @@ @@ -78,4 +86,4 @@ .minimizeButton:active { background-color: #c08e38; } - \ No newline at end of file + diff --git a/src/render/components/inputItem.vue b/src/render/components/inputItem.vue index 30978b3..edd844f 100644 --- a/src/render/components/inputItem.vue +++ b/src/render/components/inputItem.vue @@ -6,16 +6,16 @@ :style="{ top: `${elementY}px`, left: `${elementX}px` }" >
{{ initials }}
- + - @@ -27,24 +27,19 @@ - - diff --git a/src/authPayload.ts b/src/authPayload.ts deleted file mode 100644 index 61d68b6..0000000 --- a/src/authPayload.ts +++ /dev/null @@ -1,13 +0,0 @@ - -import { store } from "@/background/store"; - -interface PlatformAuthProtocol { - key: string; - crimata_id: string; -} - -export const getAuthPayload = (): PlatformAuthProtocol => ({ - key: store.get('key'), - crimata_id: store.get('crimataId') -}); - diff --git a/src/background.ts b/src/background.ts deleted file mode 100644 index 977c265..0000000 --- a/src/background.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Entry point for Crimata electron app. - * "Look on my Works, ye Mighty, and despair!" - */ - -"use strict"; - -import { initApp } from './background/init'; -import { protocol } from "electron"; - -// Scheme must be registered before the app is ready -protocol.registerSchemesAsPrivileged([ - { scheme: "app", privileges: { secure: true, standard: true } } -]); - -// Load environment variable -const isDev = require('electron-is-dev'); - -// NOTE Program Begins Here -(() => { - - console.log('Starting Crimata electron app.'); - initApp(isDev); - -})(); diff --git a/src/background/ipc/account.ts b/src/background/ipc/account.ts deleted file mode 100644 index 4bf6411..0000000 --- a/src/background/ipc/account.ts +++ /dev/null @@ -1,119 +0,0 @@ - -"use strict"; - -import { Profile } from "@/types"; -import { submit, fetchProfile, logout } from "@/api/account"; -import { ipcMain, IpcMainInvokeEvent } from "electron"; -import { store } from "@/background/store"; -import { endSession } from "@/background/session"; - - -const parseAuthRes = (authRes: any) => { - const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string; - const profile = authRes.data as Profile; - return { - token, - profile - } -}; - - -const onProfile = async ( - _event: IpcMainInvokeEvent, - _payload: null -): Promise => ( - - new Promise(async (resolve, reject) => { - console.log('[IPC]: user-profile'); - - // get jwt token and crimataId from store - const token = store.get('key'); - const crimataId = store.get('crimataId'); - - // authenticate and fetch profile - try { - - const res = await fetchProfile( - crimataId, - token - ); - - const parsed = parseAuthRes(res); - resolve(parsed.profile); - - } catch(e) { - reject(new Error('Unable to authenticate and fetch account profile.')); - } - }) -) - - -const onLogin = async ( - _event: IpcMainInvokeEvent, - payload: string -): Promise => ( - - new Promise(async (resolve, reject) => { - console.log('[IPC]: user-login'); - - const account = JSON.parse(payload); - - if ( account.password && account.email ) { - try { - const res = await submit(account.email, account.password); - const parsed = parseAuthRes(res); - - // save jwt token and profile - store.set('key', parsed.token); - store.set('crimataId', parsed.profile.crimataId); - - // return profile to renderer - resolve(parsed.profile); - - } catch(e) { - console.log('[API]', e); - reject(new Error('Failed to authenticate')); - } - } - }) -) - -const onLogout = async ( - _event: IpcMainInvokeEvent, - _payload: null -): Promise => ( - - new Promise(async (resolve, reject) => { - console.log('[IPC]: user-logout'); - - try { - // post logout to backend - await logout(); - - // remove key and crimataId - store.delete('key'); - store.delete('crimataId'); - - // TODO: kill crimata platform session - endSession(); - - resolve(); - } catch(e) { - reject(new Error('Failed to logout. Please try again.')); - } - }) -) - - -export default function useAccountListeners(): void { - - ipcMain.removeHandler("user-profile"); - ipcMain.handle("user-profile", onProfile); - - ipcMain.removeHandler("user-login"); - ipcMain.handle("user-login", onLogin); - - ipcMain.removeHandler("user-logout"); - ipcMain.handle("user-logout", onLogout); - -} diff --git a/src/background/ipc/session.ts b/src/background/ipc/session.ts deleted file mode 100644 index 7c42ea2..0000000 --- a/src/background/ipc/session.ts +++ /dev/null @@ -1,62 +0,0 @@ - -"use strict"; - -import { initSession, emitNewMessages, sendMessage } from '@/background/session'; -import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron"; -import { initAudioIO } from "@/background/audio"; -import { ClientMessage } from "@/types"; - - -// Instantiate socket session with crimata-platorm. -const onSessionInit = ( - _event: IpcMainInvokeEvent, - _payload: null -): void => { - - console.log('[IPC]: init-session'); - - initSession(); - - initAudioIO(); -} - - -const onAppMounted = ( - _event: IpcMainInvokeEvent, - _payload: null -): void => { - - console.log('[IPC]: app-mounted'); - - emitNewMessages() -}; - - -// Handle messages from window/client. -const onClientMessage = ( - _event: IpcMainEvent, - payload: ClientMessage -): void => { - - console.log('[IPC]: client-message'); - - sendMessage(payload); -} - - -export default function useSessionListeners(): void { - - console.log('[IPC]: Init session listeners.'); - - // Attach listeners for frontend. - ipcMain.removeAllListeners("client-message"); - ipcMain.on("client-message", onClientMessage); - - // Attack browser window init listener. - ipcMain.removeAllListeners("app-mounted"); - ipcMain.on("app-mounted", onAppMounted); - - ipcMain.removeAllListeners("init-session"); - ipcMain.on("init-session", onSessionInit); - -} diff --git a/src/background/session.ts b/src/background/session.ts deleted file mode 100644 index c90d32a..0000000 --- a/src/background/session.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Creates a websocket session with Crimata Servers. - * - * Connects to Servers and attempts key authentication. Server will respond - * with key and user profile. We send the profile to the browser. We also - * resend this information on new broser window. We then serve as a - * communication interface between the window and the servers. It will - * automatically try to reconnect on websocket disconnect. - */ - -import { backgroundMitt } from "@/modules/emitter"; -import { ipcEmit, loadState } from './helpers'; -import WebSocket from 'ws'; - -import useWebSockets from "./websockets"; - -import { play } from "./audio"; -import { renderMessage } from "@/modules/message"; -import { SessionState } from "@/types"; - -let win = true; - -// Info saved to json on quit (key, newMessages). -let state: SessionState; - -let socket: WebSocket | null = null; - - -// Calls appropriate endpoint for a server message. -const onMessage = (data: string): void => { - let message = JSON.parse(data); - - if (message === "CLOSE_AUTH_FAIL") { - ipcEmit("session-auth-fail", null) - return; - } - - // Standard message. - if (message.content) { - - // Convert to render message - message = renderMessage( - message.content.text, - message.content.audio, - message.context, - message.modifier - ); - - if (win) { - console.log("SESS:Emitting standard message.") - if (message.audio) { - play(message.audio) - } - ipcEmit("render-message", message) - } - - else { - console.log("SESS:No window: saving message.") - state.newMessages.push(message); - } - } - - else { - if (win) { - ipcEmit("render-message", message) - } - } - -}; - - -// Websockets module. -const { createSocket, send } = useWebSockets(onMessage); - - -export const emitNewMessages = (): void => { - if (state) { - ipcEmit("update-state", { - newMessages: state.newMessages, - }); - } - -} - - -export const sendMessage = (payload: Record): void => { - try { - send(payload) - } catch(e) { - console.log("Unable to send message: ", payload); - } - -} - -export const endSession = (): void => { - if (socket) { - socket.close(); - socket = null; - } -} - - -// Call this to initialize session with Crimata servers. -export const initSession = (): void => { - console.log("SESS:Creating new session.") - - // Load Json or createState. - state = loadState("session.json"); - - // Open socket connection. - if (!socket) - socket = createSocket(); - - // Keep win up-to-date. - backgroundMitt.on('window-active', (state: boolean) => { - win = state; - }); - -} diff --git a/src/background/websockets.ts b/src/background/websockets.ts deleted file mode 100644 index dec0203..0000000 --- a/src/background/websockets.ts +++ /dev/null @@ -1,122 +0,0 @@ - -"use strict"; - -import WebSocket from 'ws'; -import { getAuthPayload } from "./authPayload"; -import { ipcEmit } from './helpers'; - -let socket: WebSocket; - -const socketUrl = "ws://127.0.0.1:8760" - -const _connectionCheckTimeout = 4000; -const _reconnectTimeout = 1000; -let _connectionCheckInterval: ReturnType; - - -// Run every time we want to connect to backend. -export default function useWebSockets( - receiveCallback: (s: string) => void, - openCallback?: () => void -) { - - // Returns bool (sucess or fail). - const sendMessage = (data: any) => { - console.log("WS:Sending message: ", data) - - if (socket.readyState !== 1) { - return false - } - - else { - socket.send(JSON.stringify(data)) - return true - } - - } - - const send = async (data: Record): Promise => ( - new Promise((resolve, reject) => { - if (socket.readyState !== 1) { - reject(false); - } - socket.send(JSON.stringify(data)) - resolve(true); - - })) - - - const onOpen = (_event: WebSocket.OpenEvent) => { - - console.log("WS:Connected to WS Server!"); - const jwt = getAuthPayload(); - socket.send(JSON.stringify(jwt)); - - // ping server - _connectionCheckInterval = setInterval(() => { - - if (socket) socket.ping(null, true, (e: Error) => { - if (e) { - ipcEmit('connection-alive', false); - socket.close(); - setTimeout(createSocket, 1000); - } - }); - - }, _connectionCheckTimeout); - - if (openCallback) openCallback(); - - } - - const onServerMessage = (event: WebSocket.MessageEvent) => { - - console.log("WS:Message received: ", event.data); - - receiveCallback(event.data.toString()) - - } - - const onClose = (event: WebSocket.CloseEvent) => { - console.log("WS:Socket closed normally.", event.wasClean) - - clearInterval(_connectionCheckInterval); - - if (!event.wasClean) { - ipcEmit('connection-alive', false); - setTimeout(createSocket, 1000); - } - } - - // Reconnect automatically on error. - const onError = (event: WebSocket.ErrorEvent) => { - console.log("WS:WebSocket error: ", event.message); - } - - - const createSocket = (): WebSocket => { - - if (_connectionCheckInterval) clearInterval(_connectionCheckInterval); - - socket = new WebSocket(socketUrl); - - // Add listeners. - socket.addEventListener("open", onOpen); - socket.addEventListener("message", onServerMessage); - socket.addEventListener("close", onClose); - socket.addEventListener("error", onError); - socket.addEventListener("pong", () => { - ipcEmit('connection-alive', true); - }); - - return socket; - - } - - return { - createSocket, - sendMessage, - send - } - -} diff --git a/src/ipcRend/session.ts b/src/ipcRend/session.ts deleted file mode 100644 index a5ff5fc..0000000 --- a/src/ipcRend/session.ts +++ /dev/null @@ -1,23 +0,0 @@ - -import { useIpc } from "@/modules/ipc"; - -import { ClientMessage } from "@/types"; - -const { post } = useIpc(); - - -export const postMount = (): void => ( - post("app-mounted", null) -); - - -export const postInitSession = (): void => ( - post("init-session", null) -); - - -export const postMessage = (payload: ClientMessage): void => ( - post('client-message', payload) -); - - From 56196bcbcf3d75d5402eeb9aeb5b50268047e064 Mon Sep 17 00:00:00 2001 From: riqo Date: Sat, 19 Jun 2021 15:22:17 -0500 Subject: [PATCH 161/163] refactor frontend ipc --- src/account.ts | 19 +++++++++++--- src/composables/useIpcMain.ts | 18 +++++-------- src/ipc/handlers.ts | 3 ++- src/ipc/listeners.ts | 7 +++++ src/main.ts | 7 ++--- src/render/App.vue | 24 ++++------------- src/render/composables/useIpcRend.ts | 39 +++++++++++++++++++++++++--- src/render/composables/useProfile.ts | 16 +++++++++--- src/render/ipc.ts | 31 ++++++++++++++++++++++ src/render/listeners.ts | 32 +++++++++++++++++++++++ src/render/main.ts | 9 +++++-- src/store.ts | 14 +++++++++- src/types.ts | 10 ++++--- 13 files changed, 177 insertions(+), 52 deletions(-) create mode 100644 src/render/listeners.ts diff --git a/src/account.ts b/src/account.ts index 9f867f6..581da0e 100644 --- a/src/account.ts +++ b/src/account.ts @@ -1,8 +1,9 @@ import { postAuth, postLogin, postLogout } from "@/api/account"; import { endSession, launchSession } from "@/session"; -import { getToken, clearToken, setToken } from "./store"; +import { getToken, setToken, setProfile, getProfile, clearStore } from "./store"; import { parseAuthRes } from "./auth"; +import { ipcEmit } from "@/composables/useEmitter"; export const accountAuth = async (): Promise => { @@ -18,6 +19,7 @@ export const accountAuth = async (): Promise => { const parsed = parseAuthRes(res); setToken(parsed.token) + setProfile(parsed.profile); return { profile: parsed.profile, @@ -26,7 +28,7 @@ export const accountAuth = async (): Promise => { } catch(e) { console.log('[ACCOUNT]', e); - clearToken(); + clearStore(); throw(new Error('Failed to authenticate.')); } @@ -45,6 +47,7 @@ export const accountLogin: IpcHandlerCallback = asy // save jwt token and profile setToken(parsed.token); + setProfile(parsed.profile); // launch session launchSession(parsed.token); @@ -53,6 +56,7 @@ export const accountLogin: IpcHandlerCallback = asy return parsed.profile; } catch(e) { + clearStore(); throw e; } } @@ -65,7 +69,7 @@ export const accountLogout = async (): Promise => { await postLogout(); // remove key and crimataId - clearToken(); + clearStore(); // kill crimata platform session endSession(); @@ -79,4 +83,13 @@ export const accountLogout = async (): Promise => { } +export const updateAppState = (): void => { + + const profile = getProfile(); + + ipcEmit("set-profile", profile); + + // ipcEmit('messages') etc + +} diff --git a/src/composables/useIpcMain.ts b/src/composables/useIpcMain.ts index 57a2cb3..ebfc4bc 100644 --- a/src/composables/useIpcMain.ts +++ b/src/composables/useIpcMain.ts @@ -1,22 +1,21 @@ import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron"; -export class IpcHandler implements IIpcHandler { +export class IpcHandler implements IIpcHandler { readonly channel: string; - readonly _handlerCallback: IpcHandlerCallback; + readonly _handlerCallback: IpcHandlerCallback; constructor(options: { channel: string; - handlerCallback: IpcHandlerCallback; + handlerCallback: IpcHandlerCallback; }) { this.channel = options.channel; this._handlerCallback = options.handlerCallback; } handle() { - console.log(`[IPC] INIT: ${this.channel}`); this.remove(); ipcMain.handle(this.channel, this._onInvoke); } @@ -49,22 +48,21 @@ export class IpcHandler implements IIpcHandler implements IIpcListener { +export class IpcListener implements IIpcListener { readonly channel: string; - readonly _listenerCallback: IpcListenerCallback; + readonly _listenerCallback: IpcListenerCallback; constructor(options: { channel: string; - listenerCallback: IpcListenerCallback; + listenerCallback: IpcListenerCallback; }) { this.channel = options.channel; this._listenerCallback = options.listenerCallback; } listen() { - console.log(`[IPC] Init: ${this.channel}`); this.remove(); ipcMain.on(this.channel, this._onPost); } @@ -82,7 +80,3 @@ export class IpcListener implements IIpcListener { } } - - - - diff --git a/src/ipc/handlers.ts b/src/ipc/handlers.ts index bc44fc2..5529662 100644 --- a/src/ipc/handlers.ts +++ b/src/ipc/handlers.ts @@ -1,10 +1,11 @@ "use strict"; -import { accountLogin, accountLogout } from "@/account"; +import { accountLogin, accountLogout, accountProfile } from "@/account"; import { IpcHandler } from "@/composables/useIpcMain"; import { flush } from "@/audio"; + const LOGIN_CHANNEL = "invoke-account-login"; const LOGOUT_CHANNEL = "invoke-account-logout"; const GET_AUDIO_CHANNEL = "invoke-audio-flush"; diff --git a/src/ipc/listeners.ts b/src/ipc/listeners.ts index f88887a..8ef5d45 100644 --- a/src/ipc/listeners.ts +++ b/src/ipc/listeners.ts @@ -2,9 +2,11 @@ import { IpcListener } from "@/composables/useIpcMain" import { sendMessage } from '@/session'; import { collect } from "@/audio"; +import { updateAppState } from "@/account"; const CLIENT_MESSAGE_CHANNEL = "post-session-send" const GET_AUDIO_CHANNEL = "post-audio-collect"; +const APP_MOUNT_CHANNEL = "post-app-mount"; export const messageListener = new IpcListener({ channel: CLIENT_MESSAGE_CHANNEL, @@ -15,3 +17,8 @@ export const audioChunkListener = new IpcListener({ channel: GET_AUDIO_CHANNEL, listenerCallback: collect }); + +export const appMountListener = new IpcListener({ + channel: APP_MOUNT_CHANNEL, + listenerCallback: updateAppState +}); diff --git a/src/main.ts b/src/main.ts index 40dd4da..a360835 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,9 +10,8 @@ */ import initIpcMain from "@/ipc/index"; -import { accountAuth } from "./account"; +import { accountAuth, updateAppState } from "./account"; import { launchSession } from "./session"; -import { ipcEmit } from "./composables/useEmitter"; import createWindow from "./window"; let authState: AuthState | null; @@ -31,12 +30,10 @@ export default async function main() { console.log('AUTH:', e); authState = null; } finally { - let profile = null; if (authState) { launchSession(authState.token as string); - profile = authState.profile; } - ipcEmit("set-profile", profile); + updateAppState(); } } diff --git a/src/render/App.vue b/src/render/App.vue index d53788f..a7645d1 100644 --- a/src/render/App.vue +++ b/src/render/App.vue @@ -23,14 +23,16 @@