From: Andrew Gundersen Date: Wed, 9 Jun 2021 02:21:11 +0000 (-0500) Subject: beginnings of refactor X-Git-Tag: v2.0.1~19^2~9 X-Git-Url: https://andrewgundersen.net/repos?a=commitdiff_plain;h=3b5da6124fba4696bb87f5255dae7a7612a1ce0b;p=mime-chat beginnings of refactor --- diff --git a/src/App.vue b/src/App.vue deleted file mode 100644 index 52e44e6..0000000 --- a/src/App.vue +++ /dev/null @@ -1,213 +0,0 @@ - - - - - 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); + }); - } catch(e) { - console.log('Error login in.') - } finally{ + /* emit event to app.vue */ + window.postMessage(profile); - if (profile.value.crimataId) { - // start session - postInitSession(profile.value.crimataId); - } + } catch (e) console.log(e); - } } 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; - time: number; - uid: string; - isChild: string; - seen: boolean; - newMessage: boolean; -} - -export interface ClientMessage { - text: string; +interface Message { + text: boolean | string; + context: boolean | string; audio: boolean | string; + type: 1 | 2 | 3; + time: number; uid: string; } -export interface ClientRequest { - intent: string; - params: object; - epic: string | boolean; - confidence: number; -} - -export interface SessionState { - key: string | boolean; - newMessages: RenderMessage[]; +interface ViewMessage extends Message { + child: string; + seen: boolean; + newMessage: boolean; } -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; -} - -export interface LogoutRequest { - logout: boolean; -} - -export interface Annotation { - text: string; - context: string; - uid: string; +interface State { + profile: Profile | null; + messages } -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",