diff --git a/build/icon.icns b/build/icon.icns deleted file mode 100644 index b173533..0000000 Binary files a/build/icon.icns and /dev/null differ diff --git a/build/icon_128x128.png b/build/icon_128x128.png deleted file mode 100644 index bc835c0..0000000 Binary files a/build/icon_128x128.png and /dev/null differ diff --git a/build/icon_128x128@2x.png b/build/icon_128x128@2x.png deleted file mode 100644 index 0e47443..0000000 Binary files a/build/icon_128x128@2x.png and /dev/null differ diff --git a/build/icon_16x16.png b/build/icon_16x16.png deleted file mode 100644 index b925c06..0000000 Binary files a/build/icon_16x16.png and /dev/null differ diff --git a/build/icon_16x16@2x.png b/build/icon_16x16@2x.png deleted file mode 100644 index 7bf880c..0000000 Binary files a/build/icon_16x16@2x.png and /dev/null differ diff --git a/build/icon_256x256.png b/build/icon_256x256.png deleted file mode 100644 index 0e47443..0000000 Binary files a/build/icon_256x256.png and /dev/null differ diff --git a/build/icon_256x256@2x.png b/build/icon_256x256@2x.png deleted file mode 100644 index 3126e1d..0000000 Binary files a/build/icon_256x256@2x.png and /dev/null differ diff --git a/build/icon_32x32.png b/build/icon_32x32.png deleted file mode 100644 index 7bf880c..0000000 Binary files a/build/icon_32x32.png and /dev/null differ diff --git a/build/icon_32x32@2x.png b/build/icon_32x32@2x.png deleted file mode 100644 index 2a9f925..0000000 Binary files a/build/icon_32x32@2x.png and /dev/null differ diff --git a/build/icon_512x512.png b/build/icon_512x512.png deleted file mode 100644 index 3126e1d..0000000 Binary files a/build/icon_512x512.png and /dev/null differ diff --git a/build/icon_512x512@2x.png b/build/icon_512x512@2x.png deleted file mode 100644 index 7568cb1..0000000 Binary files a/build/icon_512x512@2x.png and /dev/null differ diff --git a/package.json b/package.json index 1498116..24d13b1 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "Crimata", - "version": "1.0.0-beta.0", + "version": "0.9.9", "private": true, "description": "Cross-platform messenger application built with electron, vue3, and TS.", "author": { - "name": "Andrew Gundersen" + "name": "Enrique Hernandez" }, "scripts": { "build": "vue-cli-service electron:build", @@ -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", @@ -27,10 +27,8 @@ "electron-is-dev": "^2.0.0", "electron-store": "^8.0.0", "electron-updater": "^4.3.8", - "iohook": "^0.9.3", "mitt": "^2.1.0", "naudiodon": "^2.3.2", - "node-abi": "^2.30.0", "node-record-lpcm16": "^1.0.1", "update-electron-app": "^2.0.1", "uuid": "^8.3.2", @@ -68,8 +66,25 @@ "spectron": "11.0.0", "typescript": "~3.9.3", "vue-cli-plugin-electron-builder": "~2.0.0-rc.6", - "vue-jest": "^5.0.0-0", - "webpack-node-externals": "^3.0.0" + "vue-jest": "^5.0.0-0" + }, + "vue": { + "lintOnSave": false, + "pluginOptions": { + "electronBuilder": { + "mainProcessFile": "./src/init.ts", + "rendererProcessFile": "./src/render/main.ts", + "preload": "./src/render/preload.ts", + "builderOptions": { + "appId": "com.crimata.ElectronUpdaterApp", + "artifactName": "${productName}-${version}.${ext}", + "publish": { + "provider": "generic", + "url": "https://gitlab.com/api/v4/projects/25637892/jobs/artifacts/main/raw/dist_electron?job=build" + } + } + } + } }, "gitHooks": { "pre-commit": "lint-staged" @@ -85,24 +100,5 @@ "type": "git", "url": "https://gitlab.com/crimata/electron-app.git", "release": "latest" - }, - "build": { - "asarUnpack": "**/*.node" - }, - "iohook": { - "targets": [ - "node-88", - "electron-80" - ], - "platforms": [ - "win32", - "darwin", - "linux" - ], - "arches": [ - "x64", - "ia32", - "i386" - ] } } diff --git a/src/account.ts b/src/account.ts index 6ea1ac4..581da0e 100644 --- a/src/account.ts +++ b/src/account.ts @@ -1,92 +1,75 @@ -import { app } from "electron"; -import { parseAuthRes } from "@/auth"; import { postAuth, postLogin, postLogout } from "@/api/account"; -import { updateAppUI, launchSession, endSession } from "@/session"; -import { getToken, setToken, clearToken } from "./store"; -import { backgroundMitt, ipcEmit } from "@/composables/useEmitter"; +import { endSession, launchSession } from "@/session"; +import { getToken, setToken, setProfile, getProfile, clearStore } from "./store"; +import { parseAuthRes } from "./auth"; +import { ipcEmit } from "@/composables/useEmitter"; - -/* Either null or a crimataId */ -let account: string | null = null; - - -export const accountAuth = async (): Promise => { +export const accountAuth = async (): Promise => { /* attempt to get a login token from the store */ const token = getToken(); - try { + /* try to login with it, returns platform secret and new token on success */ + if (token) { + try { - if (token) { - - // attempt to login with token const res = await postAuth(token); + const parsed = parseAuthRes(res); - // save jwt token and profile - setToken(parsed.token); - account = parsed.crimataId + setToken(parsed.token) + setProfile(parsed.profile); - // launch session - launchSession(parsed.token); + return { + profile: parsed.profile, + token: parsed.token + }; - } else throw(new Error('Failed to authenticate (no token).')); - - } catch (e) { - console.log(e); - clearToken(); - - } finally { - - // push state changes to the frontend - updateAppState(); + } catch(e) { + console.log('[ACCOUNT]', e); + clearStore(); + throw(new Error('Failed to authenticate.')); + } + } else { + throw(new Error('Unable to authenticate.')); } - }; -export const accountLogin = async (payload: any): Promise => { - - const creds = payload as AccountCredentials; - +export const accountLogin: IpcHandlerCallback = async (payload) => { + const account = payload as AccountCredentials; try { // attempt login with email password - const res = await postLogin(creds.email, creds.password); + const res = await postLogin(account.email, account.password); const parsed = parseAuthRes(res); // save jwt token and profile setToken(parsed.token); - account = parsed.crimataId; + setProfile(parsed.profile); // launch session launchSession(parsed.token); - // push state changes to the frontend - updateAppState(); - - return; + // return profile to renderer + return parsed.profile; } catch(e) { - clearToken(); + clearStore(); throw e; } - } + export const accountLogout = async (): Promise => { try { // post logout to backend await postLogout(); - // remove key and account - clearToken(); - account = null; - - // push account state to browser - ipcEmit("set-account", account); + // remove key and crimataId + clearStore(); // kill crimata platform session endSession(); @@ -100,24 +83,13 @@ export const accountLogout = async (): Promise => { } -// logout when auth fails on platform end -backgroundMitt.on("close-auth-fail", async (_payload: any) => { +export const updateAppState = (): void => { - await accountLogout(); + const profile = getProfile(); -}); + ipcEmit("set-profile", profile); -// emits state of account, version, and UI -export const updateAppState = () => { - - ipcEmit("set-account", account); - - ipcEmit("set-version", app.getVersion()); - - updateAppUI(); + // ipcEmit('messages') etc } - - - diff --git a/src/audio.ts b/src/audio.ts new file mode 100644 index 0000000..7ae2562 --- /dev/null +++ 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 const collect: IpcListenerCallback = (chunk) => { + if (chunk) buffer.push(chunk); +} + +// return audio and clear buffer +export const flush = async () => { + 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/auth.ts b/src/auth.ts index 31c66e0..14ed36a 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,5 +1,13 @@ + export const parseAuthRes = (authRes: any) => { const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string; - const crimataId = authRes.data as string; - return {token, crimataId} -}; \ No newline at end of file + const profile = authRes.data as Profile; + return { + token, + profile + } +}; + + + + diff --git a/src/composables/useIpcMain.ts b/src/composables/useIpcMain.ts index 4c3ba24..ebfc4bc 100644 --- a/src/composables/useIpcMain.ts +++ b/src/composables/useIpcMain.ts @@ -71,10 +71,12 @@ export class IpcListener implements IIpcListener { ipcMain.removeAllListeners(this.channel); } - private _onPost = (_e: IpcMainEvent, payload: InputParam): void => { + private _onPost = (_e: IpcMainEvent, payload?: string | null): void => { console.log(`[IPC] Post: ${this.channel}`); - this._listenerCallback(payload); + + const params = payload ? JSON.parse(payload) : null; + this._listenerCallback(params); } } diff --git a/src/composables/useMessageCanvas.ts b/src/composables/useMessageCanvas.ts new file mode 100644 index 0000000..c85f7ed --- /dev/null +++ b/src/composables/useMessageCanvas.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/useWebsockets.ts b/src/composables/useWebsockets.ts index 3b9f2c9..d1cfc02 100644 --- a/src/composables/useWebsockets.ts +++ b/src/composables/useWebsockets.ts @@ -11,13 +11,7 @@ let _connectionCheckInterval: ReturnType; export default function useWebSockets( messageCallback: (message: string) => void, - connectionStatusCallback: (status: string) => void, - statusOptions?: { - openMessage: string; - pongMessage: string; - closeMessage: string; - pingErrorMessage: string; - }, + connectionStatusCallback: (alive: boolean) => void, ) { let socket: WebSocket; @@ -47,15 +41,13 @@ export default function useWebSockets( socket.send(JSON.stringify({key: secret})); - connectionStatusCallback(statusOptions ? statusOptions.openMessage : "Connection Opened"); - // ping server _connectionCheckInterval = setInterval(() => { socket.ping(null, true, (e: Error) => { if (e) { socket.close(); - connectionStatusCallback(statusOptions ? statusOptions.pingErrorMessage : "Connection Lost"); + connectionStatusCallback(false); setTimeout(() => connect(socketUrl, secret), _reconnectTimeout); } }); @@ -69,28 +61,24 @@ export default function useWebSockets( messageCallback(event.toString()) }); - socket.on("close", (code: number, reason: string) => { - connectionStatusCallback(statusOptions ? statusOptions.closeMessage : "Connection Closed"); + socket.on("close", (event: WebSocket.CloseEvent) => { + connectionStatusCallback(false); clearInterval(_connectionCheckInterval); - if (code !== 1000 || reason !== 'session-logout') { - setTimeout(() => { - connect(socketUrl, secret), _reconnectTimeout - }); + if (!event.wasClean) { + setTimeout(() => connect(socketUrl, secret), _reconnectTimeout); } }); - socket.on("pong", () => connectionStatusCallback(statusOptions ? statusOptions.pongMessage : "Pong")); - - socket.on('error', () => {}); + socket.on("pong", () => connectionStatusCallback(true)); } - const close = (code: number, reason: string) => { + const close = () => { if (socket) { - socket.close(code, reason); + socket.close(); } - }; + } return { connect, diff --git a/src/init.ts b/src/init.ts index ad43998..9a0fdf6 100644 --- a/src/init.ts +++ b/src/init.ts @@ -26,7 +26,6 @@ backgroundMitt.on('window-active', (state: boolean) => { win = state; }); - /* Start main process on ready */ app.on("ready", async () => { await main(); diff --git a/src/ipc/handlers.ts b/src/ipc/handlers.ts index f35e283..5529662 100644 --- a/src/ipc/handlers.ts +++ b/src/ipc/handlers.ts @@ -1,12 +1,14 @@ "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"; export const loginHandler = new IpcHandler({ channel: LOGIN_CHANNEL, @@ -17,3 +19,10 @@ export const logoutHandler = new IpcHandler({ channel: LOGOUT_CHANNEL, handlerCallback: accountLogout }); + +export const getAudioHandler = new IpcHandler({ + channel: GET_AUDIO_CHANNEL, + handlerCallback: flush +}); + + diff --git a/src/ipc/listeners.ts b/src/ipc/listeners.ts index f92bead..8ef5d45 100644 --- a/src/ipc/listeners.ts +++ b/src/ipc/listeners.ts @@ -1,24 +1,24 @@ import { IpcListener } from "@/composables/useIpcMain" import { sendMessage } from '@/session'; +import { collect } from "@/audio"; import { updateAppState } from "@/account"; -import { onNavBar } from "@/window"; const CLIENT_MESSAGE_CHANNEL = "post-session-send" +const GET_AUDIO_CHANNEL = "post-audio-collect"; const APP_MOUNT_CHANNEL = "post-app-mount"; -const NAV_BAR_CHANNEL = "post-nav-bar"; -export const messageListener = new IpcListener({ +export const messageListener = new IpcListener({ channel: CLIENT_MESSAGE_CHANNEL, listenerCallback: sendMessage }); +export const audioChunkListener = new IpcListener({ + channel: GET_AUDIO_CHANNEL, + listenerCallback: collect +}); + export const appMountListener = new IpcListener({ channel: APP_MOUNT_CHANNEL, listenerCallback: updateAppState }); - -export const navBarListener = new IpcListener({ - channel: NAV_BAR_CHANNEL, - listenerCallback: onNavBar -}); diff --git a/src/main.ts b/src/main.ts index ca37a2d..a360835 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,51 +10,30 @@ */ import initIpcMain from "@/ipc/index"; -import { accountAuth } from "./account"; +import { accountAuth, updateAppState } from "./account"; +import { launchSession } from "./session"; import createWindow from "./window"; -// Short cut functionality dependencies -import { globalShortcut } from 'electron' -const nodeAbi = require('node-abi') -// const ioHook = require('iohook'); -// import ioHook from '../node_modules/iohook/builds/electron-v80-darwin-x64/build/Release/iohook.node' -// import ioHook from 'iohook' - -// TODO: load from store -const recordShortcut = 'CommandOrControl+S'; +let authState: AuthState | null; export default async function main() { - // need to get abi for iohook keyboard event listener - console.log('node abi', nodeAbi.getAbi('7.2.0', 'node')) - // '51' - console.log('electron abi', nodeAbi.getAbi('1.4.10', 'electron')) - - // listen for keyup event - // ioHook.on('keyup', (event: any) => { - // // emit pause recording - // // send audio message - // console.log('KEYUP') - // console.log(event); - // }); - - // start keyup hook; pass true for DEBUG mode. - // ioHook.start(true); - - globalShortcut.register(recordShortcut, () => { - // emit begin recording - console.log('begin recording.') - }); - - - /* initiate controls for frontend to use when needed */ initIpcMain(); /* launch browser window */ await createWindow(); - /* try to authenticate with token */ - await accountAuth(); + try { + authState = await accountAuth() as AuthState; + } catch(e) { + console.log('AUTH:', e); + authState = null; + } finally { + if (authState) { + launchSession(authState.token as string); + } + updateAppState(); + } } diff --git a/src/profile.ts b/src/profile.ts deleted file mode 100644 index 0c5428d..0000000 --- a/src/profile.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ipcEmit } from "@/composables/useEmitter"; - - -export default class ProfileHandler { - - profile: Profile; - - constructor(profile: Profile) { - this.profile = profile; - this.emit(); - } - - emit() { - ipcEmit("set-profile", this.profile); - } - - update(update: Update) { - this.profile = update.data as Profile; - this.emit() - } - -} \ No newline at end of file diff --git a/src/render/App.vue b/src/render/App.vue index 3e5cf13..686591b 100644 --- a/src/render/App.vue +++ b/src/render/App.vue @@ -1,10 +1,15 @@