Compare commits

..
Author SHA1 Message Date
riqo
8203a90de5 add initial state shape 2021-06-09 09:03:52 -05:00
52 changed files with 1054 additions and 1139 deletions

79
build/config.gypi Normal file
View file

@ -0,0 +1,79 @@
# Do not edit. File was generated by node-gyp's "configure" step
{
"target_defaults": {
"cflags": [],
"default_configuration": "Release",
"defines": [],
"include_dirs": [],
"libraries": []
},
"variables": {
"asan": 0,
"build_v8_with_gn": "false",
"coverage": "false",
"dcheck_always_on": 0,
"debug_nghttp2": "false",
"debug_node": "false",
"enable_lto": "false",
"enable_pgo_generate": "false",
"enable_pgo_use": "false",
"error_on_warn": "false",
"force_dynamic_crt": 0,
"host_arch": "x64",
"icu_data_in": "../../deps/icu-tmp/icudt67l.dat",
"icu_endianness": "l",
"icu_gyp_path": "tools/icu/icu-generic.gyp",
"icu_path": "deps/icu-small",
"icu_small": "false",
"icu_ver_major": "67",
"is_debug": 0,
"llvm_version": "0.0",
"napi_build_version": "6",
"node_byteorder": "little",
"node_debug_lib": "false",
"node_enable_d8": "false",
"node_install_npm": "true",
"node_module_version": 83,
"node_no_browser_globals": "false",
"node_prefix": "/",
"node_release_urlbase": "https://nodejs.org/download/release/",
"node_shared": "false",
"node_shared_brotli": "false",
"node_shared_cares": "false",
"node_shared_http_parser": "false",
"node_shared_libuv": "false",
"node_shared_nghttp2": "false",
"node_shared_openssl": "false",
"node_shared_zlib": "false",
"node_tag": "",
"node_target_type": "executable",
"node_use_bundled_v8": "true",
"node_use_dtrace": "true",
"node_use_etw": "false",
"node_use_node_code_cache": "true",
"node_use_node_snapshot": "true",
"node_use_openssl": "true",
"node_use_v8_platform": "true",
"node_with_ltcg": "false",
"node_without_node_options": "false",
"openssl_fips": "",
"openssl_is_fips": "false",
"shlib_suffix": "83.dylib",
"target_arch": "x64",
"v8_enable_31bit_smis_on_64bit_arch": 0,
"v8_enable_gdbjit": 0,
"v8_enable_i18n_support": 1,
"v8_enable_inspector": 1,
"v8_enable_pointer_compression": 0,
"v8_no_strict_aliasing": 1,
"v8_optimized_debug": 1,
"v8_promise_internal_field_count": 1,
"v8_random_seed": 0,
"v8_trace_maps": 0,
"v8_use_siphash": 1,
"want_separate_host_toolset": 0,
"xcode_version": "11.0",
"nodedir": "/Users/Enrique/Library/Caches/node-gyp/14.4.0",
"standalone_static_library": 1
}
}

View file

@ -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",
@ -24,6 +24,7 @@
"animejs": "^3.2.0",
"axios": "^0.21.1",
"core-js": "^3.6.5",
"dotenv": "^10.0.0",
"electron-is-dev": "^2.0.0",
"electron-store": "^8.0.0",
"electron-updater": "^4.3.8",
@ -72,9 +73,7 @@
"lintOnSave": false,
"pluginOptions": {
"electronBuilder": {
"mainProcessFile": "./src/init.ts",
"rendererProcessFile": "./src/render/main.ts",
"preload": "./src/render/preload.ts",
"preload": "src/preload.ts",
"builderOptions": {
"appId": "com.crimata.ElectronUpdaterApp",
"artifactName": "${productName}-${version}.${ext}",

View file

@ -11,7 +11,11 @@
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- <h1>Hello World!</h1>
We are using Node.js <span id="node-version"></span>, Chromium
<span id="chrome-version"></span>, and Electron
<span id="electron-version"></span>. -->
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View file

@ -1,95 +0,0 @@
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";
export const accountAuth = async (): Promise<Error | AuthState> => {
/* attempt to get a login token from the store */
const token = getToken();
/* try to login with it, returns platform secret and new token on success */
if (token) {
try {
const res = await postAuth(token);
const parsed = parseAuthRes(res);
setToken(parsed.token)
setProfile(parsed.profile);
return {
profile: parsed.profile,
token: parsed.token
};
} catch(e) {
console.log('[ACCOUNT]', e);
clearStore();
throw(new Error('Failed to authenticate.'));
}
} else {
throw(new Error('Unable to authenticate.'));
}
};
export const accountLogin: IpcHandlerCallback<AccountCredentials, Profile> = async (payload) => {
const account = payload as AccountCredentials;
try {
// attempt login with email password
const res = await postLogin(account.email, account.password);
const parsed = parseAuthRes(res);
// save jwt token and profile
setToken(parsed.token);
setProfile(parsed.profile);
// launch session
launchSession(parsed.token);
// return profile to renderer
return parsed.profile;
} catch(e) {
clearStore();
throw e;
}
}
export const accountLogout = async (): Promise<Error | void> => {
try {
// post logout to backend
await postLogout();
// remove key and crimataId
clearStore();
// kill crimata platform session
endSession();
return;
} catch(e) {
console.log('[ACCOUNT]', e);
return (new Error('Failed to logout. Please try again.'));
}
}
export const updateAppState = (): void => {
const profile = getProfile();
ipcEmit("set-profile", profile);
// ipcEmit('messages') etc
}

View file

@ -1,31 +1,23 @@
import useHttp from "@/composables/useHttp";
import { useHttp } from "@/composables/http";
import axios from "axios";
import {config} from "@/config";
const { post } = useHttp();
export const postAuth = async (token: string) => (
await axios({
url: config.BUSINESS_URL + config.BUSINESS_PREFIX + '/account/authenticate',
headers: {
Cookie: `crimataCookie=${token}`
},
method: 'POST',
})
);
export const postLogin = async (email: string, password: string) => (
export const submit = async (email: string, password: string) => (
await post('/account/login', { email, password })
);
export const postLogout =
async (): Promise<null | Error> => (await post('/account/logout'));
)
export const fetchAccount = async (email: string, token: string) => (
await axios({
url: "http://127.0.0.1:3000/api/account/profile",
headers: {
Cookie: `jwt=${token}`
},
method: 'GET',
data: {
email,
}
})
)

View file

@ -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<ArrayBuffer> = (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<Error | string> => (
// 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<Buffer> {
// 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;
// }
// }
// }

View file

@ -1,13 +0,0 @@
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
}
};

187
src/composables/audio.ts Normal file
View file

@ -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<Error | string> => (
// 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<Buffer> {
// 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;
// }
// }
// }

View file

@ -1,16 +1,15 @@
/* eslint-disable */
// Backend emitter
//
const EventEmitter = require('events');
class BackgroundMitt extends EventEmitter { }
export const backgroundMitt = new BackgroundMitt();
export const ipcEmit = <T>(channel: string, payload: T) => {
export default function ipcEmit (channel: string, payload: any) {
backgroundMitt.emit('ipc-renderer', {
channel,
payload
endpoint: channel,
message: payload
});
};
}

View file

@ -1,8 +1,10 @@
import axios, { AxiosRequestConfig } from 'axios';
import {config} from "@/config";
const baseURL = config.BUSINESS_URL + config.BUSINESS_PREFIX;
const preFix = '/api';
const baseURL = "http://127.0.0.1:3000" + preFix;
interface Request {
endpoint: string;
@ -10,6 +12,7 @@ interface Request {
config?: Record<string, any>;
}
const makeQuery = (reqQuery: Record<string, any>) => {
let result = '';
@ -22,7 +25,7 @@ const makeQuery = (reqQuery: Record<string, any>) => {
};
export default function useHttp() {
export const useHttp = () => {
const api = axios.create({
baseURL,

9
src/composables/json.ts Normal file
View file

@ -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.")
}
})
}

51
src/composables/store.ts Normal file
View file

@ -0,0 +1,51 @@
const Store = require('electron-store');
const schema = {
// should be separate, used to authenticate against business and platform
key: {
type: 'string',
},
profile: {
type:
},
messages: {
new: Message[],
saved: ViewMessages[]
},
};
export const store = new Store({
schema,
encryptionKey: "super user test"
});
export const emitInitialState = () => {
const profile = store.get(profile);
const messages = store.get(messages);
emit("initial-state", {
messages,
profile
});
}
/* add a message to state.messages */
export function addMessage(message: Message) {
// update state
// TODO: add logic to handle new vs saved
state.messages.new.push(message);
state.messages.saved.push(message);
saveState();
// emit new message
}

View file

@ -1,82 +0,0 @@
import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
export class IpcHandler<InputParam, ReturnType> implements IIpcHandler<InputParam, ReturnType> {
readonly channel: string;
readonly _handlerCallback: IpcHandlerCallback<InputParam, ReturnType>;
constructor(options: {
channel: string;
handlerCallback: IpcHandlerCallback<InputParam, ReturnType>;
}) {
this.channel = options.channel;
this._handlerCallback = options.handlerCallback;
}
handle() {
this.remove();
ipcMain.handle(this.channel, this._onInvoke);
}
remove() {
ipcMain.removeHandler(this.channel);
}
private _onInvoke = (_e: IpcMainInvokeEvent, payload?: string | null): Promise<Error | ReturnType> => {
return new Promise(async (resolve, reject) => {
console.log(`[IPC] Handle:${this.channel}`);
try {
const params = payload ? JSON.parse(payload) : null;
const res = await this._handlerCallback(params);
resolve(res as unknown as ReturnType);
} catch(e) {
console.log(`[IPC] Error:${this.channel}`);
reject(e);
}
});
}
}
export class IpcListener<InputParam> implements IIpcListener<InputParam> {
readonly channel: string;
readonly _listenerCallback: IpcListenerCallback<InputParam>;
constructor(options: {
channel: string;
listenerCallback: IpcListenerCallback<InputParam>;
}) {
this.channel = options.channel;
this._listenerCallback = options.listenerCallback;
}
listen() {
this.remove();
ipcMain.on(this.channel, this._onPost);
}
remove() {
ipcMain.removeAllListeners(this.channel);
}
private _onPost = (_e: IpcMainEvent, payload?: string | null): void => {
console.log(`[IPC] Post: ${this.channel}`);
const params = payload ? JSON.parse(payload) : null;
this._listenerCallback(params);
}
}

View file

@ -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);
}
}
}

View file

@ -1,13 +0,0 @@
import {config} from "@/config";
import fs from 'fs';
export const saveToJson = (fileName: string, data: any) => {
fs.writeFile(config.configPath + fileName, JSON.stringify(data), (err) => {
if (err) {
console.log("Error when saving to json.")
}
})
}

View file

@ -1,89 +0,0 @@
"use strict";
import WebSocket from 'ws';
const _connectionCheckTimeout = 4000;
const _reconnectTimeout = 1000;
let _connectionCheckInterval: ReturnType<typeof setTimeout>;
export default function useWebSockets(
messageCallback: (message: string) => void,
connectionStatusCallback: (alive: boolean) => void,
) {
let socket: WebSocket;
const send = async (data: Record<string, any>): Promise<boolean> => {
return new Promise((resolve, reject) => {
if (socket) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data));
resolve(true);
}
}
reject(false);
});
}
const connect = (socketUrl: string, secret: string) => {
// avoid setting multiple interval;
if (_connectionCheckInterval) clearInterval(_connectionCheckInterval);
/* create a new socket */
socket = new WebSocket(socketUrl);
/* add event listeners */
socket.on("open", () => {
socket.send(JSON.stringify({key: secret}));
// ping server
_connectionCheckInterval = setInterval(() => {
socket.ping(null, true, (e: Error) => {
if (e) {
socket.close();
connectionStatusCallback(false);
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
}
});
}, _connectionCheckTimeout);
});
socket.on("message", (event: WebSocket.MessageEvent) => {
console.log("message received", event);
messageCallback(event.toString())
});
socket.on("close", (event: WebSocket.CloseEvent) => {
connectionStatusCallback(false);
clearInterval(_connectionCheckInterval);
if (!event.wasClean) {
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
}
});
socket.on("pong", () => connectionStatusCallback(true));
}
const close = () => {
if (socket) {
socket.close();
}
}
return {
connect,
send,
close
};
}

View file

@ -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<string, any>): Promise<boolean> => {
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
};
}

View file

@ -1,17 +0,0 @@
import { app } from "electron";
const env = process.env;
const PLATFORM_PORT = env.PLATFORM_PORT || 8760;
const PLATFORM_IP = env.PLATFORM_IP || 'http://127.0.0.1';
const BUSINESS_PORT = env.BUSINESS_PORT || 3000;
const BUSINESS_IP = env.BUSINESS_IP || 'http://127.0.0.1';
export const config = {
PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`,
BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`,
BUSINESS_PREFIX: '/api',
configPath: app.getPath('userData')
}

View file

@ -8,7 +8,8 @@
import { app, protocol } from "electron";
import createWindow from "./window";
import main from "./main";
import { backgroundMitt } from '@/composables/useEmitter';
require('dotenv').config();
console.log('Starting Crimata electron app.');
@ -19,13 +20,6 @@ protocol.registerSchemesAsPrivileged([
const isDev = require('electron-is-dev');
let win: boolean;
// Listen for window creation.
backgroundMitt.on('window-active', (state: boolean) => {
win = state;
});
/* Start main process on ready */
app.on("ready", async () => {
await main();
@ -49,4 +43,4 @@ if (isDev) {
process.on("SIGTERM", () => {
app.quit();
});
}
}

126
src/ipc/account.ts Normal file
View file

@ -0,0 +1,126 @@
"use strict";
import { submit, fetchProfile, logout } from "../api/account";
import { ipcMain, IpcMainInvokeEvent } from "electron";
import { store } from "@/composables/store";
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
}
};
/**
* Get user profile from store and try to login with it.
*/
const onTokenLogin = async (
_event: IpcMainInvokeEvent,
_payload: null
): Promise<Profile | Error> => (
new Promise(async (resolve, reject) => {
console.log('[IPC]: user-profile');
// get jwt token and crimataId from store
const token = store.get('key');
const crimataId = store.get('crimataId');
// authenticate and fetch profile
try {
// attempt login with email token
const res = await fetchProfile(crimataId, token);
const parsed = parseAuthRes(res);
// return profile to renderer
resolve(parsed.profile);
} catch(e) {
reject(new Error('Failed to fetch profile.'));
}
})
)
const onLogin = async (
_event: IpcMainInvokeEvent,
payload: string
): Promise<Profile | Error> => (
new Promise(async (resolve, reject) => {
console.log('[IPC]: user-login');
const account = JSON.parse(payload);
if ( account.password && account.email ) {
try {
// attempt login with email password
const res = await submit(account.email, account.password);
const parsed = parseAuthRes(res);
// save jwt token and profile
store.set('key', parsed.token);
store.set('crimataId', parsed.profile.crimataId);
// init session
// return profile to renderer
resolve(parsed.profile);
} catch(e) {
console.log('[API]', e);
reject(new Error('Failed to authenticate'));
}
}
})
)
const onLogout = async (
_event: IpcMainInvokeEvent,
_payload: null
): Promise<void> => (
new Promise(async (resolve, reject) => {
console.log('[IPC]: user-logout');
try {
// post logout to backend
await logout();
// remove key and crimataId
store.delete('key');
store.delete('crimataId');
// TODO: kill crimata platform session
// endSession();
resolve();
} catch(e) {
reject(new Error('Failed to logout. Please try again.'));
}
})
)
export default function useAccountListeners(): void {
ipcMain.removeHandler("user-profile");
ipcMain.handle("user-profile", onProfile);
ipcMain.removeHandler("user-login");
ipcMain.handle("user-login", onLogin);
ipcMain.removeHandler("user-logout");
ipcMain.handle("user-logout", onLogout);
}

34
src/ipc/audio.ts Normal file
View file

@ -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<Error | ArrayBuffer[]> => {
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);
}

View file

@ -1,28 +0,0 @@
"use strict";
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,
handlerCallback: accountLogin
});
export const logoutHandler = new IpcHandler({
channel: LOGOUT_CHANNEL,
handlerCallback: accountLogout
});
export const getAudioHandler = new IpcHandler({
channel: GET_AUDIO_CHANNEL,
handlerCallback: flush
});

View file

@ -1,33 +1,17 @@
"use strict";
import * as handlers from "./handlers";
import * as listeners from "./listeners";
import useAccountListeners from "./account";
import useSessionListeners from "./session";
// import useAudioListeners from "./audio";
const ipcHandlers: IPCHandlers = {};
const ipcListeners: IPCListeners = {};
const _initHandlers = (): void => {
for (const [key, handler] of Object.entries(handlers)) {
if (!(key in ipcHandlers)) {
ipcHandlers[key] = handler;
handler.handle();
}
}
};
export default function useIpc(): void {
const _initListeners = (): void => {
for (const [key, listener] of Object.entries(listeners)) {
if (!(key in ipcListeners)) {
ipcListeners[key] = listener;
listener.listen();
}
}
};
useAccountListeners();
useSessionListeners();
// useAudioListeners();
export default function initIpcMain(): void {
_initHandlers();
_initListeners();
}

View file

@ -1,24 +0,0 @@
import { IpcListener } from "@/composables/useIpcMain"
import { sendMessage } from '@/session';
import { collect } from "@/audio";
import { updateAppState } from "@/account";
const CLIENT_MESSAGE_CHANNEL = "post-session-send"
const GET_AUDIO_CHANNEL = "post-audio-collect";
const APP_MOUNT_CHANNEL = "post-app-mount";
export const messageListener = new IpcListener<Message>({
channel: CLIENT_MESSAGE_CHANNEL,
listenerCallback: sendMessage
});
export const audioChunkListener = new IpcListener<ArrayBuffer>({
channel: GET_AUDIO_CHANNEL,
listenerCallback: collect
});
export const appMountListener = new IpcListener<null>({
channel: APP_MOUNT_CHANNEL,
listenerCallback: updateAppState
});

20
src/ipc/session.ts Normal file
View file

@ -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);
}

View file

@ -1,39 +1,72 @@
/**
* 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 initIpcMain from "@/ipc/index";
import { accountAuth, updateAppState } from "./account";
import { launchSession } from "./session";
import createWindow from "./window";
import { fetchAccount, submit } from "@/api/account";
import { launchSession, endSession } from "@/session";
import useIpc from "@/ipc/index";
import store from "@/composables/store";
let authState: AuthState | null;
/* user profile, signals whether user is logged in */
let auth: Profile | null = null;
export default async function main() {
/* initiate controls for frontend to use when needed */
initIpcMain();
/* launch browser window */
await createWindow();
/* authenticate the user */
export async function authenticate(email: string, password: string) {
/* attempt normal login */
try {
authState = await accountAuth() as AuthState;
} catch(e) {
console.log('AUTH:', e);
authState = null;
} finally {
if (authState) {
launchSession(authState.token as string);
}
updateAppState();
auth = await submit(email, password);
} catch (e) {
console.log(e);
}
/* launch if profile */
if (auth) {
launchSession(auth);
}
}
/* 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();
}

View file

@ -1,15 +1,14 @@
<template>
<!-- Only render when profile has been set -->
<main id="app" v-if="authComplete">
<main id="app" v-if="typeof profile !== 'undefined'">
<Header />
<!-- Main Components -->
<Messenger
v-if="profile"
v-if="profile"
:profile="profile"
/>
<!-- Main Components
-->
<Login v-else />
</main>
@ -21,17 +20,14 @@
<script lang="ts">
import { defineComponent, onMounted } from "vue";
import { IpcRendererEvent } from "electron";
import { defineComponent, onMounted, onUnmounted, Ref } from "vue";
import { invokeProfile } from "@/ipc/account";
import Splash from "@/render/components/splash.vue";
import Messenger from "@/render/components/messenger.vue";
import Login from "@/render/components/login.vue";
import Header from "@/render/components/header.vue";
import { profile, authComplete } from "@/render/composables/useProfile";
import { postAppMount } from "@/render/ipc";
export default defineComponent({
components: {
@ -43,10 +39,23 @@ export default defineComponent({
setup() {
onMounted(() => postAppMount());
const state: Ref;
onMounted(async () => {
console.log("[APP]:mounted.");
/* listen for auth related messages */
window.addEventListener("update-state", (event: any) => {
state.value = event.data;
});
});
onUnmounted(() => {
window.ipcRenderer.removeAllListeners("update-state");
});
return {
authComplete,
profile
}
}
@ -59,6 +68,7 @@ export default defineComponent({
html, body {
margin: 0;
padding: 0;
// Background color set in window.ts
}
#app {

View file

@ -10,7 +10,7 @@
<!-- message context -->
<span class="context">
<div :class="`${type}-icon`"/>
<div :class="`${type}-icon`">
{{ context }}
</span>
@ -22,24 +22,23 @@
import { defineComponent, ref, onMounted } from 'vue';
export default defineComponent({
name: "Bubble",
name: "MessageItem",
props: ["text", "context", "type", "position"],
setup() {
const seen = ref(false);
/* initialize seen state */
if (document.visibilityState === "visible") {
seen.value = true;
} else seen.value = false;
const seen = ref(() => {
if (document.visibilityState === "visible") {
return true;
} else return false;
});
onMounted(() => {
if(seen.value)
document.addEventListener("visibilitychange", () => {
seen.value = true
});
if (!seen)
document.addEventListener("visibilitychange", () => seen = true);
});

View file

@ -92,7 +92,7 @@ export function newMessage ({
audio=false,
context=false,
uid=uuidv4()
}) {
}): Message {
return {
text: text,
audio: audio,

View file

@ -1,7 +1,9 @@
import anime from "animejs";
import { useIpc } from '@/modules/ipc';
import { onMounted, onUnmounted, ref, Ref } from "vue";
import { postMessage } from "@/render/ipc";
import { postMessage } from "@/ipc/session";
import { newMessage, animateAudioInput } from "./helpers";
import { invokeReturnAudio, postAudioChunk } from "@/render/ipc";
import { invokeStopRecord, postAudioChunk } from "@/ipc/audio";
export default function useAudioInputController (typing: Ref) {
@ -26,7 +28,7 @@ export default function useAudioInputController (typing: Ref) {
// get audio and post new message to backend
mediaRecorder.addEventListener('stop', (_e: Event) => {
invokeReturnAudio().then((audio: ArrayBuffer[] | Error) => {
invokeStopRecord().then((audio: ArrayBuffer[] | Error) => {
console.log(audio);
// postMessage(newMessage({audio: audio}));
});

View file

@ -1,10 +1,10 @@
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
import { postMessage } from "@/render/ipc";
import { postMessage } from "@/ipc/session";
import { newMessage, animateTextInput } from "./helpers";
export default function useTextInputController(elementX: Ref) {
let textInput: HTMLInputElement | null;
const { side, show, hide, switchSide } = animateTextInput();
@ -37,10 +37,10 @@ export default function useTextInputController(elementX: Ref) {
// Send it to the backend for processing.
const message = newMessage({
text: false
text: textInput.value
});
// postMessage(message);
postMessage(message);
clearInput()
}

View file

@ -1,38 +1,143 @@
import { ref } from 'vue';
import useScroll from "@/render/composables/useScroll";
import invokeSavedMessages from "@/render/ipc";
import useScroll from "@/render/composables/scroll";
const messagesRef = ref();
const messages = ref(new Map());
/* seed the canvas with messages */
const seedCanvas = (messages: Message[]) => {
messagesRef.value = messages;
function getTimeStamp(): number {
const currentdate = new Date();
return currentdate.getTime();
}
const addMessage = (message: Message) => {
messagesRef.value.push(message);
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;
}
let target_message = messagesRef.value.filter((m: Message) => {
return m.uid = message.uid;
})[0];
const loadSavedMessages = async () => {
const messageData = await invokeSavedMessages();
messages.value = new Map(Object.entries(messageData));
}
if (target_message) {
target_message = message;
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 {
messagesRef,
seedCanvas,
addMessage,
updateMessage
};
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);
// }
// }

View file

@ -14,22 +14,14 @@
<script lang="ts">
// import { postNavBarExit, postNavBarMin } from "@/render/ipc";
import { defineComponent } from "vue";
export default defineComponent({
name: "Header",
setup() {
const postNavBarExit = () => {};
const postNavBarMin = () => {};
import { postNavBarExit, postNavBarMin } from "@/render/ipc";
setup() {
return {
postNavBarExit,
postNavBarMin
}
}
});
}
</script>
@ -86,4 +78,4 @@
.minimizeButton:active {
background-color: #c08e38;
}
</style>
</style>

View file

@ -6,16 +6,16 @@
:style="{ top: `${elementY}px`, left: `${elementX}px` }"
>
<div>{{ initials }}</div>
<!-- Recording animation on space bar -->
<span v-if="recording" class="play"></span>
<span v-if="recording" class="pause"></span>
<!-- Show text input on key-down -->
<input
id="textInput"
type="text"
<input
id="textInput"
type="text"
/>
<!-- Show suggestions menu on click -->
@ -27,19 +27,24 @@
<script lang="ts">
import { defineComponent } from "vue";
import draggify from "@/render/composables/useDraggify";
import draggify from "@/modules/draggify";
import TextInput from "@/components/textInput.vue";
import useTextInputController from
"@/render/components/controllers/inputItem.control.text";
import useAudioInputController from
"@/render/components/controllers/inputItem.control.audio";
import useTextInputController from
"@/components/controllers/inputItem.control.audio";
import useAudioInputController from
"@/components/controllers/inputItem.control.text";
export default defineComponent({
name: "InputItem",
props: ["initials"],
components: {
TextInput
},
setup() {
// Default values for position.

View file

@ -35,8 +35,11 @@
<script lang="ts">
import { defineComponent, ref } from "vue";
import { setProfile } from '@/render/composables/useProfile';
import { invokeLogin } from "@/render/ipc";
import { useIpc } from "@/modules/ipc";
import { authRequest } from '@/modules/message';
import { useProfile } from '@/modules/auth';
import { invokeLogin } from "@/ipcRend/account";
import { postInitSession } from "@/ipcRend/session";
export default defineComponent({
name: "Login",
@ -54,14 +57,12 @@ export default defineComponent({
const profile = await invokeLogin({
email: usr.value,
password: pwd.value
}) as Profile;
});
setProfile(profile)
/* emit event to app.vue */
window.postMessage(profile);
} catch(e) {
console.log(e);
}
} catch (e) console.log(e);
}

View file

@ -9,7 +9,7 @@
<div id="messenger">
<Bubble
v-for="message in messages"
:text="message.content.text"
:text="message.text"
:context="message.context"
:key="message[0]"
/>
@ -20,26 +20,45 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted } from "vue";
import Message from "@/render/components/message.vue";
import InputItem from "@/render/components/inputItem.vue";
import Settings from "@/render/components/settings.vue";
import Bubble from "@/render/components/bubble.vue";
import { profile } from "@/render/composables/useProfile";
import { messages } from "@/render/composables/useMessages";
import useMessages from "@/render/composables/messages";
export default defineComponent({
name: "Messenger",
props: ["state"],
components: {
Message,
InputItem,
Settings,
Bubble
Settings
},
setup() {
setup(props) {
// Handle messages in view.
const { messages, updateMessageView } = useMessages();
onMounted(() => {
/* populate the message view with existing messages */
updateMessageView(state.savedMessages, state.newMessages);
/* wait and listen for new messages to come in */
window.ipcRenderer.on("new-message", (_e: any, payload: any) => {
updateMessageView(payload.message);
});
});
onUnmounted(() => {
window.ipcRenderer.removeAllListeners("new-message");
});
return {
messages,
profile
messages
};
},

View file

@ -28,8 +28,10 @@
<script lang="ts">
import { defineComponent, ref } from "vue";
import { clearProfile } from "@/render/composables/useProfile"
import { invokeLogout } from "@/render/ipc";
import { useIpc } from "@/modules/ipc";
import { logoutRequest } from '@/modules/message';
import { useProfile } from "@/modules/auth"
import { invokeLogout } from "@/ipcRend/account";
export default defineComponent({
name: "Settings",
@ -37,6 +39,10 @@
setup() {
const toggleSettings = ref(false);
const { post, invoke } = useIpc();
const { clearProfile } = useProfile();
// Listen for escape key to close settings.
const onEscape = (e: any) => {
if(e.key === "Escape") {
@ -57,8 +63,8 @@
console.log("Submitting logout request.");
try {
await invokeLogout();
clearProfile();
await invokeLogout();
} catch(e) {
console.log('error')
}

View file

@ -0,0 +1,21 @@
import { ref } from "vue";
const profile = ref();
export const useProfile = () => {
const setProfile = (payload: Profile) => {
profile.value = payload;
}
const clearProfile = () => {
profile.value = null;
}
return {
setProfile,
clearProfile,
profile
}
}

View file

@ -0,0 +1,21 @@
export default function useIpc () {
const invoke = async (endpoint: string, payload: any) => {
try {
const res = await window.ipcRenderer.invoke(endpoint, payload);
return res;
} catch (e) {
throw e;
}
}
const post = (endpoint: string, payload: any) => {
window.ipcRenderer.send(endpoint, payload);
};
return {
invoke,
post
}
}

View file

@ -1,54 +0,0 @@
import { IpcRendererEvent } from "electron";
export class IpcRendererListener<InputParam> implements IIpcListener<InputParam> {
readonly channel: string;
readonly _listenerCallback: IpcListenerCallback<InputParam>;
constructor(options: {
channel: string;
listenerCallback: IpcListenerCallback<InputParam>;
}) {
this.channel = options.channel;
this._listenerCallback = options.listenerCallback;
}
listen() {
this.remove();
window.ipcRenderer.on(this.channel, this._onPost);
}
remove() {
window.ipcRenderer.removeAllListeners(this.channel);
}
private _onPost = (_e: IpcRendererEvent, payload: InputParam): void => {
console.log(`[IPC] Post: ${this.channel}`);
this._listenerCallback(payload);
}
}
export default function useIpcRenderer () {
const invoke = async (endpoint: string, payload: any) => {
try {
const res = await window.ipcRenderer.invoke(endpoint, payload);
return res;
} catch (e) {
throw e;
}
}
const post = (endpoint: string, payload: any) => {
window.ipcRenderer.send(endpoint, payload);
};
return {
invoke,
post,
}
}

View file

@ -1,33 +0,0 @@
// shared
import { ref, Ref } from "vue";
import useScroll from "@/render/composables/useScroll";
export const messages: Ref<Array<Message>> = ref([]);
export const setMessages: IpcListenerCallback<Array<Message>> = (payload) => {
messages.value = payload as Array<Message>;
}
export const addMessage: IpcListenerCallback<Message> = (payload) => {
messages.value.push(payload as Message);
}
export const updateMessage: IpcListenerCallback<Message> = (payload) => {
const message = payload as Message;
let targetMessage = messages.value.filter((m: Message) => {
return m.uid = message.uid;
})[0];
if (targetMessage) {
targetMessage = message;
}
}
export default {
messages,
setMessages,
addMessage,
updateMessage
};

View file

@ -1,27 +0,0 @@
// shared
import { ref } from "vue";
export const profile = ref();
export const authComplete = ref(false);
export const setProfile: IpcListenerCallback<Profile | null> = (payload) => {
payload ? profile.value = payload : clearProfile();
showRender();
};
export const clearProfile = () => {
profile.value = null;
};
export const showRender = () => {
authComplete.value = true;
};
export default {
setProfile,
clearProfile,
profile,
showRender,
authComplete,
};

View file

@ -1,44 +1,46 @@
import useIpc from "@/render/composables/useIpcRend";
import * as rendererListeners from "./listeners";
import useIpc from "@/render/composables/ipc";
const { post, invoke } = useIpc();
/**
*
*
* Account and auth related endpoints
*
*/
*
*/
export const invokeProfile = async (): Promise<Profile | Error> => (
await invoke('user-profile', null)
);
export const invokeLogin = async (
payload: LoginPayload
): Promise<Profile | Error> => (
await invoke('invoke-account-login', JSON.stringify(payload))
await invoke('user-login', JSON.stringify(payload))
);
export const invokeLogout = async (): Promise<void> => (
await invoke("invoke-account-logout", null)
await invoke("user-logout", null)
);
/**
*
*
* Audio endpoints
*
*
*/
export const postAudioChunk = (chunk: ArrayBuffer): void => (
post("post-audio-collect", chunk)
post("audio-chunk", chunk)
);
export const invokeReturnAudio = async (): Promise<ArrayBuffer[] | Error> => (
await invoke("invoke-audio-flush", null)
await invoke("get-audio", null)
);
/**
*
*
* Crimata Platform (session) endpoints
*
*
*/
export const invokeSession = async (cid: string): Promise<Profile | Error> => (
@ -46,35 +48,5 @@ export const invokeSession = async (cid: string): Promise<Profile | Error> => (
);
export const postMessage = (payload: Message): void => (
post('post-session-send', payload)
);
export const postAppMount = (): void => (
post('post-app-mount', null)
);
/**
*
* Ipc Renderer Listeners
*
*/
let ipcListeners: IPCListeners = {};
export const initIpcRendererListeners = () => {
for (const [key, listener] of Object.entries(rendererListeners)) {
if (!(key in ipcListeners)) {
ipcListeners[key] = listener;
listener.listen();
}
}
};
export const removeListeners = () => {
for (const [key, listener] of Object.entries(rendererListeners)) {
listener.remove();
}
ipcListeners = {};
};
post('client-message', payload)
);

View file

@ -1,32 +0,0 @@
import { IpcRendererListener } from "./composables/useIpcRend"
import { setProfile } from "./composables/useProfile";
import { setMessages, addMessage, updateMessage } from "./composables/useMessages";
const SET_PROFILE_CHANNEL = "set-profile";
const INIT_MESSAGES_CHANNEL = "init-messages";
const ADD_MESSAGE_CHANNEL = "add-message";
const UPDATE_MESSAGE_CHANNEL = "update-message";
export const setProfileListener = new IpcRendererListener({
channel: SET_PROFILE_CHANNEL,
listenerCallback: setProfile
});
export const initMessagesListener = new IpcRendererListener({
channel: INIT_MESSAGES_CHANNEL,
listenerCallback: setMessages
});
export const addMessagesListener = new IpcRendererListener({
channel: ADD_MESSAGE_CHANNEL,
listenerCallback: addMessage
});
export const updateMessagesListener = new IpcRendererListener({
channel: UPDATE_MESSAGE_CHANNEL,
listenerCallback: updateMessage
});

View file

@ -1,21 +0,0 @@
// src/main.ts
import App from "./App.vue";
import mitt from "mitt";
import { createApp } from "vue";
import { initIpcRendererListeners } from "./ipc"
// Handle ipcMain events.
initIpcRendererListeners();
// Handle events.
const emitter = mitt();
const app = createApp(App);
app.provide("mitt", emitter);
app.mount("#app");

View file

@ -2,100 +2,63 @@
// import useAudio from "@/audio";
import { ipcEmit } from "@/composables/useEmitter";
import useWebsockets from "./composables/useWebsockets";
import {config} from "@/config";
/* data structure of messages that's tied to the UI */
const uiState: any | null = null;
import useAudio from "@/audio";
import { loadState, saveState, emitState } from "@/state";
/* start and stop audio functionality */
// const { initAudio, closeAudio } = useAudio();
const isInitMessage = (message: any): boolean => {
return true;
};
const deauthenticate = (): void => {
console.log('deauthenticating')
};
const isAddMessage = (message: any): boolean => {
return true;
};
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);
const onMessageCallback = (payload: string) => {
/* push the message to the browser */
if (win) emit("new-message", message);
const message = JSON.parse(payload);
console.log(typeof message);
});
/* if the platform fails to authenticate, we must back down */
if (message === "CLOSE_AUTH_FAIL") {
deauthenticate();
return;
}
/* send a message to the platform */
export function sendMessage(message: Message) {
ipcEmit("add-message", message)
return
/* add the message to the state */
addMessage(message);
/* on init, platform sends state, used to init canvas */
if (isInitMessage(message)) {
ipcEmit("init-messages", message)
}
else if (isAddMessage(message)) {
ipcEmit("add-messages", message)
}
else {
ipcEmit("update-messages", message)
}
}
const onConnectionStatusCallback = (alive: boolean) => {
// console.log('[Session]: Connection Alive: ', alive);
// ipcEmit('connection-state', alive);
}
const { connect, send, close } = useWebsockets(
onMessageCallback,
onConnectionStatusCallback
);
/* send a message to the platform */
export function sendMessage<Message>(message: Message): void {
/* 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(platformKey: string) {
export function launchSession(profile: Profile) {
/* load any previously saved state for that user */
loadState(profile);
/* connect to the platform */
connect(config.PLATFORM_URL, platformKey);
connect(profile);
/* initialize the audio streams */
// initAudio();
}
/* finally we can push state to browser */
if (win) emitState();
}
export function endSession() {
// closeAudio();
closeAudioStreams();
close();
closeSocket();
}
state.clear();
}

29
src/state.ts Normal file
View file

@ -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);
}

View file

@ -1,31 +0,0 @@
const Store = require('electron-store');
const schema = {
token: {
type: 'string',
},
profile: {}
};
const store = new Store({
schema,
encryptionKey: "super user test"
});
export const getToken = (): string | undefined => (store.get("token"));
export const clearToken = (): void => (store.delete("token"));
export const setToken = (token: string): void => (store.set('token', token));
export const setProfile = (profile: Profile): Profile => (store.set('profile', profile));
export const getProfile = (): Profile => (store.get('profile'));
export const clearProfile = (): void => (store.delete('profile'));
export const clearStore = (): void => {
clearToken();
clearProfile();
}

View file

@ -1,14 +1,16 @@
interface Message {
text: boolean | string;
context: boolean | string;
audio: boolean | string;
type: 1 | 2 | 3;
time: number;
uid: string;
}
interface ViewMessage extends Message {
child: string;
seen: boolean;
newMessage: boolean;
}
interface WindowState {
@ -24,54 +26,12 @@ interface Profile {
initials: string;
}
interface State {
profile: Profile | null;
messages
}
interface LoginPayload {
email: string;
password: string;
}
interface AuthState {
profile: Profile | null;
token: string | null;
}
interface AccountCredentials {
email: string;
password: string;
}
/*
* Electron Ipc
*/
interface IpcHandlerCallback<I, O> {
(payload: I | null): Promise<Error | O>;
}
interface IpcListenerCallback<T> {
(payload: T | null): void;
}
interface IIpcHandler<I, O> {
handle(): void;
remove(): void;
readonly _handlerCallback: IpcHandlerCallback<I, O>;
}
interface IIpcListener<I> {
listen(): void;
remove(): void;
readonly _listenerCallback: IpcListenerCallback<I>;
}
interface IPCHandlers {
[handler: string]: IIpcHandler<any, any>;
}
interface IPCListeners {
[listener: string]: IIpcListener<any> | null;
}
interface IpcRendererEvent<T> {
channel: string;
payload: T | null;
}

View file

@ -1,14 +1,17 @@
"use strict";
import { BrowserWindow, ipcMain, app } from "electron";
import { BrowserWindow, ipcMain } from "electron";
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
import { backgroundMitt } from './composables/useEmitter';
import { saveToJson } from "./composables/useSaveToJSON";
import { backgroundMitt } from './coposables/emitter';
import { saveToJson } from "./coposables/json";
import * as path from "path";
import fs from 'fs';
import { config } from "@/config";
const { autoUpdater } = require('electron-updater');
interface IpcRendererPayload {
endpoint: string;
message: Message | null;
}
let win: BrowserWindow | null;
let winState: WindowState;
@ -16,8 +19,8 @@ const loadWinState = (fileName: string): WindowState => {
let state: WindowState;
try {
state = JSON.parse(fs.readFileSync(config.configPath + fileName).toString());
}
state = JSON.parse(fs.readFileSync(configPath + fileName).toString());
}
catch (error) {
state = {
@ -29,8 +32,8 @@ const loadWinState = (fileName: string): WindowState => {
}
return state
}
}
// Called when a NavBar button is pressed.
const onNavBar = (_event: any, action: string): void => {
@ -44,9 +47,11 @@ const onNavBar = (_event: any, action: string): void => {
}
// Util function to render message on ipc-renderer event.
const postToWindow = <T>(event: IpcRendererEvent<T>): void => {
const renderMessage = (payload: IpcRendererPayload): void => {
if (win) {
win.webContents.send(event.channel, event.payload);
win.webContents.send(payload.endpoint, {
message: payload.message
});
}
}
@ -81,7 +86,7 @@ const onWindowMount = (): void => {
// Gateway for messages to the frontend.
backgroundMitt.removeAllListeners("ipc-renderer")
backgroundMitt.on("ipc-renderer", postToWindow);
backgroundMitt.on("ipc-renderer", renderMessage);
}
@ -105,8 +110,8 @@ export default async function createWindow(): Promise<void> {
win = new BrowserWindow({
width: winState.width,
height: winState.height,
x: winState.x as number,
y: winState.y as number,
x: winState.x,
y: winState.y,
resizable: true,
backgroundColor: '#EBEBEB',
frame: false,