diff --git a/build/icon.icns b/build/icon.icns new file mode 100644 index 0000000..b173533 Binary files /dev/null and b/build/icon.icns differ diff --git a/build/icon_128x128.png b/build/icon_128x128.png new file mode 100644 index 0000000..bc835c0 Binary files /dev/null and b/build/icon_128x128.png differ diff --git a/build/icon_128x128@2x.png b/build/icon_128x128@2x.png new file mode 100644 index 0000000..0e47443 Binary files /dev/null and b/build/icon_128x128@2x.png differ diff --git a/build/icon_16x16.png b/build/icon_16x16.png new file mode 100644 index 0000000..b925c06 Binary files /dev/null and b/build/icon_16x16.png differ diff --git a/build/icon_16x16@2x.png b/build/icon_16x16@2x.png new file mode 100644 index 0000000..7bf880c Binary files /dev/null and b/build/icon_16x16@2x.png differ diff --git a/build/icon_256x256.png b/build/icon_256x256.png new file mode 100644 index 0000000..0e47443 Binary files /dev/null and b/build/icon_256x256.png differ diff --git a/build/icon_256x256@2x.png b/build/icon_256x256@2x.png new file mode 100644 index 0000000..3126e1d Binary files /dev/null and b/build/icon_256x256@2x.png differ diff --git a/build/icon_32x32.png b/build/icon_32x32.png new file mode 100644 index 0000000..7bf880c Binary files /dev/null and b/build/icon_32x32.png differ diff --git a/build/icon_32x32@2x.png b/build/icon_32x32@2x.png new file mode 100644 index 0000000..2a9f925 Binary files /dev/null and b/build/icon_32x32@2x.png differ diff --git a/build/icon_512x512.png b/build/icon_512x512.png new file mode 100644 index 0000000..3126e1d Binary files /dev/null and b/build/icon_512x512.png differ diff --git a/build/icon_512x512@2x.png b/build/icon_512x512@2x.png new file mode 100644 index 0000000..7568cb1 Binary files /dev/null and b/build/icon_512x512@2x.png differ diff --git a/package.json b/package.json index 24d13b1..1498116 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "Crimata", - "version": "0.9.9", + "version": "1.0.0-beta.0", "private": true, "description": "Cross-platform messenger application built with electron, vue3, and TS.", "author": { - "name": "Enrique Hernandez" + "name": "Andrew Gundersen" }, "scripts": { "build": "vue-cli-service electron:build", @@ -12,7 +12,7 @@ "postinstall": "electron-builder install-app-deps", "postuninstall": "electron-builder install-app-deps" }, - "main": "init.js", + "main": "background.js", "dependencies": { "@google-cloud/speech": "^4.2.0", "@types/animejs": "^3.1.2", @@ -27,8 +27,10 @@ "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", @@ -66,25 +68,8 @@ "spectron": "11.0.0", "typescript": "~3.9.3", "vue-cli-plugin-electron-builder": "~2.0.0-rc.6", - "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" - } - } - } - } + "vue-jest": "^5.0.0-0", + "webpack-node-externals": "^3.0.0" }, "gitHooks": { "pre-commit": "lint-staged" @@ -100,5 +85,24 @@ "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 581da0e..6ea1ac4 100644 --- a/src/account.ts +++ b/src/account.ts @@ -1,66 +1,79 @@ +import { app } from "electron"; +import { parseAuthRes } from "@/auth"; import { postAuth, postLogin, postLogout } from "@/api/account"; -import { endSession, launchSession } from "@/session"; -import { getToken, setToken, setProfile, getProfile, clearStore } from "./store"; -import { parseAuthRes } from "./auth"; -import { ipcEmit } from "@/composables/useEmitter"; +import { updateAppUI, launchSession, endSession } from "@/session"; +import { getToken, setToken, clearToken } from "./store"; +import { backgroundMitt, ipcEmit } from "@/composables/useEmitter"; -export const accountAuth = async (): Promise => { + +/* Either null or a crimataId */ +let account: string | null = null; + + +export const accountAuth = async (): Promise => { /* attempt to get a login token from the store */ const token = getToken(); - /* try to login with it, returns platform secret and new token on success */ - if (token) { - try { + try { + if (token) { + + // attempt to login with token const res = await postAuth(token); - const parsed = parseAuthRes(res); - setToken(parsed.token) - setProfile(parsed.profile); + // save jwt token and profile + setToken(parsed.token); + account = parsed.crimataId - return { - profile: parsed.profile, - token: parsed.token - }; + // launch session + launchSession(parsed.token); - } catch(e) { - console.log('[ACCOUNT]', e); - clearStore(); - throw(new Error('Failed to authenticate.')); + } else throw(new Error('Failed to authenticate (no token).')); + + } catch (e) { + console.log(e); + clearToken(); + + } finally { + + // push state changes to the frontend + updateAppState(); - } - } else { - throw(new Error('Unable to authenticate.')); } + }; -export const accountLogin: IpcHandlerCallback = async (payload) => { - const account = payload as AccountCredentials; +export const accountLogin = async (payload: any): Promise => { + + const creds = payload as AccountCredentials; + try { // attempt login with email password - const res = await postLogin(account.email, account.password); + const res = await postLogin(creds.email, creds.password); const parsed = parseAuthRes(res); // save jwt token and profile setToken(parsed.token); - setProfile(parsed.profile); + account = parsed.crimataId; // launch session launchSession(parsed.token); - // return profile to renderer - return parsed.profile; + // push state changes to the frontend + updateAppState(); + + return; } catch(e) { - clearStore(); + clearToken(); throw e; } -} +} export const accountLogout = async (): Promise => { @@ -68,8 +81,12 @@ export const accountLogout = async (): Promise => { // post logout to backend await postLogout(); - // remove key and crimataId - clearStore(); + // remove key and account + clearToken(); + account = null; + + // push account state to browser + ipcEmit("set-account", account); // kill crimata platform session endSession(); @@ -83,13 +100,24 @@ export const accountLogout = async (): Promise => { } -export const updateAppState = (): void => { +// logout when auth fails on platform end +backgroundMitt.on("close-auth-fail", async (_payload: any) => { - const profile = getProfile(); + await accountLogout(); - ipcEmit("set-profile", profile); +}); - // ipcEmit('messages') etc +// emits state of account, version, and UI +export const updateAppState = () => { + + ipcEmit("set-account", account); + + ipcEmit("set-version", app.getVersion()); + + updateAppUI(); } + + + diff --git a/src/audio.ts b/src/audio.ts deleted file mode 100644 index 7ae2562..0000000 --- a/src/audio.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* eslint @typescript-eslint/no-var-requires: "off" */ - -"use strict"; - -// where the audio goes -let buffer: ArrayBuffer[] = []; - -// place audio data in buffer -export 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 14ed36a..31c66e0 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,13 +1,5 @@ - export const parseAuthRes = (authRes: any) => { const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string; - const profile = authRes.data as Profile; - return { - token, - profile - } -}; - - - - + const crimataId = authRes.data as string; + return {token, crimataId} +}; \ No newline at end of file diff --git a/src/composables/useIpcMain.ts b/src/composables/useIpcMain.ts index ebfc4bc..4c3ba24 100644 --- a/src/composables/useIpcMain.ts +++ b/src/composables/useIpcMain.ts @@ -71,12 +71,10 @@ export class IpcListener implements IIpcListener { ipcMain.removeAllListeners(this.channel); } - private _onPost = (_e: IpcMainEvent, payload?: string | null): void => { + private _onPost = (_e: IpcMainEvent, payload: InputParam): void => { console.log(`[IPC] Post: ${this.channel}`); - - const params = payload ? JSON.parse(payload) : null; - this._listenerCallback(params); + this._listenerCallback(payload); } } diff --git a/src/composables/useMessageCanvas.ts b/src/composables/useMessageCanvas.ts deleted file mode 100644 index c85f7ed..0000000 --- a/src/composables/useMessageCanvas.ts +++ /dev/null @@ -1,36 +0,0 @@ - - - -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 d1cfc02..3b9f2c9 100644 --- a/src/composables/useWebsockets.ts +++ b/src/composables/useWebsockets.ts @@ -11,7 +11,13 @@ let _connectionCheckInterval: ReturnType; export default function useWebSockets( messageCallback: (message: string) => void, - connectionStatusCallback: (alive: boolean) => void, + connectionStatusCallback: (status: string) => void, + statusOptions?: { + openMessage: string; + pongMessage: string; + closeMessage: string; + pingErrorMessage: string; + }, ) { let socket: WebSocket; @@ -41,13 +47,15 @@ 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(false); + connectionStatusCallback(statusOptions ? statusOptions.pingErrorMessage : "Connection Lost"); setTimeout(() => connect(socketUrl, secret), _reconnectTimeout); } }); @@ -61,24 +69,28 @@ export default function useWebSockets( messageCallback(event.toString()) }); - socket.on("close", (event: WebSocket.CloseEvent) => { - connectionStatusCallback(false); + socket.on("close", (code: number, reason: string) => { + connectionStatusCallback(statusOptions ? statusOptions.closeMessage : "Connection Closed"); clearInterval(_connectionCheckInterval); - if (!event.wasClean) { - setTimeout(() => connect(socketUrl, secret), _reconnectTimeout); + if (code !== 1000 || reason !== 'session-logout') { + setTimeout(() => { + connect(socketUrl, secret), _reconnectTimeout + }); } }); - socket.on("pong", () => connectionStatusCallback(true)); + socket.on("pong", () => connectionStatusCallback(statusOptions ? statusOptions.pongMessage : "Pong")); + + socket.on('error', () => {}); } - const close = () => { + const close = (code: number, reason: string) => { if (socket) { - socket.close(); + socket.close(code, reason); } - } + }; return { connect, diff --git a/src/init.ts b/src/init.ts index 9a0fdf6..ad43998 100644 --- a/src/init.ts +++ b/src/init.ts @@ -26,6 +26,7 @@ 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 5529662..f35e283 100644 --- a/src/ipc/handlers.ts +++ b/src/ipc/handlers.ts @@ -1,14 +1,12 @@ "use strict"; -import { accountLogin, accountLogout, accountProfile } from "@/account"; +import { accountLogin, accountLogout } 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, @@ -19,10 +17,3 @@ 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 8ef5d45..f92bead 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 a360835..ca37a2d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,30 +10,51 @@ */ import initIpcMain from "@/ipc/index"; -import { accountAuth, updateAppState } from "./account"; -import { launchSession } from "./session"; +import { accountAuth } from "./account"; import createWindow from "./window"; -let authState: AuthState | null; +// 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'; 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 { - authState = await accountAuth() as AuthState; - } catch(e) { - console.log('AUTH:', e); - authState = null; - } finally { - if (authState) { - launchSession(authState.token as string); - } - updateAppState(); - } + /* try to authenticate with token */ + await accountAuth(); } diff --git a/src/profile.ts b/src/profile.ts new file mode 100644 index 0000000..0c5428d --- /dev/null +++ b/src/profile.ts @@ -0,0 +1,22 @@ +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 686591b..3e5cf13 100644 --- a/src/render/App.vue +++ b/src/render/App.vue @@ -1,15 +1,10 @@