Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95e3dab1c2 | ||
|
|
2c510765c0 | ||
|
|
56196bcbcf | ||
|
|
c8411d573a | ||
|
|
a89e856661 | ||
|
|
ab487e4f5e | ||
|
|
a22b3c68e5 | ||
|
|
a6351af83c | ||
|
|
798b848c9b | ||
|
|
d963f7d8a8 | ||
|
|
0a3c9f71d8 | ||
|
|
d851db2fcc | ||
|
|
9e8641b4de | ||
|
|
628210dab5 | ||
|
|
25b03d501c | ||
|
|
0cfa2340be | ||
|
|
0079c72ea0 |
52 changed files with 1139 additions and 1019 deletions
|
|
@ -1,79 +0,0 @@
|
|||
# Do not edit. File was generated by node-gyp's "configure" step
|
||||
{
|
||||
"target_defaults": {
|
||||
"cflags": [],
|
||||
"default_configuration": "Release",
|
||||
"defines": [],
|
||||
"include_dirs": [],
|
||||
"libraries": []
|
||||
},
|
||||
"variables": {
|
||||
"asan": 0,
|
||||
"build_v8_with_gn": "false",
|
||||
"coverage": "false",
|
||||
"dcheck_always_on": 0,
|
||||
"debug_nghttp2": "false",
|
||||
"debug_node": "false",
|
||||
"enable_lto": "false",
|
||||
"enable_pgo_generate": "false",
|
||||
"enable_pgo_use": "false",
|
||||
"error_on_warn": "false",
|
||||
"force_dynamic_crt": 0,
|
||||
"host_arch": "x64",
|
||||
"icu_data_in": "../../deps/icu-tmp/icudt67l.dat",
|
||||
"icu_endianness": "l",
|
||||
"icu_gyp_path": "tools/icu/icu-generic.gyp",
|
||||
"icu_path": "deps/icu-small",
|
||||
"icu_small": "false",
|
||||
"icu_ver_major": "67",
|
||||
"is_debug": 0,
|
||||
"llvm_version": "0.0",
|
||||
"napi_build_version": "6",
|
||||
"node_byteorder": "little",
|
||||
"node_debug_lib": "false",
|
||||
"node_enable_d8": "false",
|
||||
"node_install_npm": "true",
|
||||
"node_module_version": 83,
|
||||
"node_no_browser_globals": "false",
|
||||
"node_prefix": "/",
|
||||
"node_release_urlbase": "https://nodejs.org/download/release/",
|
||||
"node_shared": "false",
|
||||
"node_shared_brotli": "false",
|
||||
"node_shared_cares": "false",
|
||||
"node_shared_http_parser": "false",
|
||||
"node_shared_libuv": "false",
|
||||
"node_shared_nghttp2": "false",
|
||||
"node_shared_openssl": "false",
|
||||
"node_shared_zlib": "false",
|
||||
"node_tag": "",
|
||||
"node_target_type": "executable",
|
||||
"node_use_bundled_v8": "true",
|
||||
"node_use_dtrace": "true",
|
||||
"node_use_etw": "false",
|
||||
"node_use_node_code_cache": "true",
|
||||
"node_use_node_snapshot": "true",
|
||||
"node_use_openssl": "true",
|
||||
"node_use_v8_platform": "true",
|
||||
"node_with_ltcg": "false",
|
||||
"node_without_node_options": "false",
|
||||
"openssl_fips": "",
|
||||
"openssl_is_fips": "false",
|
||||
"shlib_suffix": "83.dylib",
|
||||
"target_arch": "x64",
|
||||
"v8_enable_31bit_smis_on_64bit_arch": 0,
|
||||
"v8_enable_gdbjit": 0,
|
||||
"v8_enable_i18n_support": 1,
|
||||
"v8_enable_inspector": 1,
|
||||
"v8_enable_pointer_compression": 0,
|
||||
"v8_no_strict_aliasing": 1,
|
||||
"v8_optimized_debug": 1,
|
||||
"v8_promise_internal_field_count": 1,
|
||||
"v8_random_seed": 0,
|
||||
"v8_trace_maps": 0,
|
||||
"v8_use_siphash": 1,
|
||||
"want_separate_host_toolset": 0,
|
||||
"xcode_version": "11.0",
|
||||
"nodedir": "/Users/Enrique/Library/Caches/node-gyp/14.4.0",
|
||||
"standalone_static_library": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
@ -24,7 +24,6 @@
|
|||
"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",
|
||||
|
|
@ -73,7 +72,9 @@
|
|||
"lintOnSave": false,
|
||||
"pluginOptions": {
|
||||
"electronBuilder": {
|
||||
"preload": "src/preload.ts",
|
||||
"mainProcessFile": "./src/init.ts",
|
||||
"rendererProcessFile": "./src/render/main.ts",
|
||||
"preload": "./src/render/preload.ts",
|
||||
"builderOptions": {
|
||||
"appId": "com.crimata.ElectronUpdaterApp",
|
||||
"artifactName": "${productName}-${version}.${ext}",
|
||||
|
|
|
|||
|
|
@ -11,11 +11,7 @@
|
|||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<!-- <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>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
95
src/account.ts
Normal file
95
src/account.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -1,23 +1,31 @@
|
|||
|
||||
import { useHttp } from "@/composables/http";
|
||||
import useHttp from "@/composables/useHttp";
|
||||
import axios from "axios";
|
||||
import {config} from "@/config";
|
||||
|
||||
const { post } = useHttp();
|
||||
|
||||
export const submit = async (email: string, password: string) => (
|
||||
await post('/account/login', { email, password })
|
||||
)
|
||||
|
||||
export const fetchAccount = async (email: string, token: string) => (
|
||||
export const postAuth = async (token: string) => (
|
||||
await axios({
|
||||
url: "http://127.0.0.1:3000/api/account/profile",
|
||||
headers: {
|
||||
Cookie: `jwt=${token}`
|
||||
},
|
||||
method: 'GET',
|
||||
data: {
|
||||
email,
|
||||
}
|
||||
url: config.BUSINESS_URL + config.BUSINESS_PREFIX + '/account/authenticate',
|
||||
headers: {
|
||||
Cookie: `crimataCookie=${token}`
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
export const postLogin = async (email: string, password: string) => (
|
||||
await post('/account/login', { email, password })
|
||||
);
|
||||
|
||||
|
||||
export const postLogout =
|
||||
async (): Promise<null | Error> => (await post('/account/logout'));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
187
src/audio.ts
187
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<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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
13
src/auth.ts
Normal file
13
src/auth.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
export const parseAuthRes = (authRes: any) => {
|
||||
const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
|
||||
const profile = authRes.data as Profile;
|
||||
return {
|
||||
token,
|
||||
profile
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
/* eslint @typescript-eslint/no-var-requires: "off" */
|
||||
|
||||
"use strict";
|
||||
|
||||
// where the audio goes
|
||||
let buffer: ArrayBuffer[] = [];
|
||||
|
||||
// place audio data in buffer
|
||||
export function collect (chunk: ArrayBuffer) {
|
||||
buffer.push(chunk);
|
||||
}
|
||||
|
||||
// return audio and clear buffer
|
||||
export function flush () {
|
||||
const bufferCopy = buffer;
|
||||
buffer = [];
|
||||
return bufferCopy;
|
||||
}
|
||||
|
||||
// import { backgroundMitt } from '@/modules/emitter';
|
||||
// const portAudio = require('naudiodon');
|
||||
|
||||
// // Audio in and out stream objects.
|
||||
// let ai: typeof portAudio.AudioIO | boolean = false;
|
||||
// let ao: typeof portAudio.AudioIO | boolean = false;
|
||||
|
||||
// // Whether activly recording.
|
||||
// let record = false;
|
||||
|
||||
// const audioContainer = {
|
||||
// input: '',
|
||||
// }
|
||||
|
||||
// const audioOptions = {
|
||||
// channelCount: 1,
|
||||
// sampleFormat: 16,
|
||||
// sampleRate: 16000,
|
||||
// deviceId: -1,
|
||||
// closeOnError: false,
|
||||
// }
|
||||
|
||||
// export const toggleRecord = (): void => { record = !record };
|
||||
|
||||
|
||||
// export const fetchAudioInput = (): Promise<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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
export const saveToJson = (fileName: string, data: any) => {
|
||||
|
||||
fs.writeFile(configPath + fileName, JSON.stringify(data), (err) => {
|
||||
if (err) {
|
||||
console.log("Error when saving to json.")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
|
||||
const Store = require('electron-store');
|
||||
|
||||
const schema = {
|
||||
key: {
|
||||
type: 'string',
|
||||
},
|
||||
crimataId: {
|
||||
type: 'string'
|
||||
}
|
||||
};
|
||||
|
||||
export const store = new Store({
|
||||
schema,
|
||||
encryptionKey: "super user test"
|
||||
});
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
/* eslint-disable */
|
||||
|
||||
// Backend emitter
|
||||
|
||||
//
|
||||
const EventEmitter = require('events');
|
||||
class BackgroundMitt extends EventEmitter { }
|
||||
|
||||
export const backgroundMitt = new BackgroundMitt();
|
||||
|
||||
export default function ipcEmit (channel: string, payload: any) {
|
||||
export const ipcEmit = <T>(channel: string, payload: T) => {
|
||||
backgroundMitt.emit('ipc-renderer', {
|
||||
endpoint: channel,
|
||||
message: payload
|
||||
channel,
|
||||
payload
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import {config} from "@/config";
|
||||
|
||||
const preFix = '/api';
|
||||
|
||||
const baseURL = "http://127.0.0.1:3000" + preFix;
|
||||
|
||||
const baseURL = config.BUSINESS_URL + config.BUSINESS_PREFIX;
|
||||
|
||||
interface Request {
|
||||
endpoint: string;
|
||||
|
|
@ -12,7 +10,6 @@ interface Request {
|
|||
config?: Record<string, any>;
|
||||
}
|
||||
|
||||
|
||||
const makeQuery = (reqQuery: Record<string, any>) => {
|
||||
|
||||
let result = '';
|
||||
|
|
@ -25,7 +22,7 @@ const makeQuery = (reqQuery: Record<string, any>) => {
|
|||
};
|
||||
|
||||
|
||||
export const useHttp = () => {
|
||||
export default function useHttp() {
|
||||
|
||||
const api = axios.create({
|
||||
baseURL,
|
||||
82
src/composables/useIpcMain.ts
Normal file
82
src/composables/useIpcMain.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
36
src/composables/useMessageCanvas.ts
Normal file
36
src/composables/useMessageCanvas.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
13
src/composables/useSaveToJSON.ts
Normal file
13
src/composables/useSaveToJSON.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
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.")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
89
src/composables/useWebsockets.ts
Normal file
89
src/composables/useWebsockets.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
|
||||
"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
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
|
||||
"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
|
||||
};
|
||||
|
||||
}
|
||||
17
src/config.ts
Normal file
17
src/config.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
import { app } from "electron";
|
||||
|
||||
const env = process.env;
|
||||
|
||||
const PLATFORM_PORT = env.PLATFORM_PORT || 8760;
|
||||
const PLATFORM_IP = env.PLATFORM_IP || 'http://127.0.0.1';
|
||||
|
||||
const BUSINESS_PORT = env.BUSINESS_PORT || 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')
|
||||
}
|
||||
12
src/init.ts
12
src/init.ts
|
|
@ -8,8 +8,7 @@
|
|||
import { app, protocol } from "electron";
|
||||
import createWindow from "./window";
|
||||
import main from "./main";
|
||||
|
||||
require('dotenv').config();
|
||||
import { backgroundMitt } from '@/composables/useEmitter';
|
||||
|
||||
console.log('Starting Crimata electron app.');
|
||||
|
||||
|
|
@ -20,6 +19,13 @@ protocol.registerSchemesAsPrivileged([
|
|||
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
let win: boolean;
|
||||
|
||||
// Listen for window creation.
|
||||
backgroundMitt.on('window-active', (state: boolean) => {
|
||||
win = state;
|
||||
});
|
||||
|
||||
/* Start main process on ready */
|
||||
app.on("ready", async () => {
|
||||
await main();
|
||||
|
|
@ -43,4 +49,4 @@ if (isDev) {
|
|||
process.on("SIGTERM", () => {
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
|
||||
"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);
|
||||
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
|
||||
"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);
|
||||
|
||||
}
|
||||
28
src/ipc/handlers.ts
Normal file
28
src/ipc/handlers.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
"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
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -1,17 +1,33 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
import useAccountListeners from "./account";
|
||||
import useSessionListeners from "./session";
|
||||
// import useAudioListeners from "./audio";
|
||||
import * as handlers from "./handlers";
|
||||
import * as listeners from "./listeners";
|
||||
|
||||
const ipcHandlers: IPCHandlers = {};
|
||||
const ipcListeners: IPCListeners = {};
|
||||
|
||||
export default function useIpc(): void {
|
||||
const _initHandlers = (): void => {
|
||||
for (const [key, handler] of Object.entries(handlers)) {
|
||||
if (!(key in ipcHandlers)) {
|
||||
ipcHandlers[key] = handler;
|
||||
handler.handle();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useAccountListeners();
|
||||
|
||||
useSessionListeners();
|
||||
|
||||
// useAudioListeners();
|
||||
const _initListeners = (): void => {
|
||||
for (const [key, listener] of Object.entries(listeners)) {
|
||||
if (!(key in ipcListeners)) {
|
||||
ipcListeners[key] = listener;
|
||||
listener.listen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default function initIpcMain(): void {
|
||||
_initHandlers();
|
||||
_initListeners();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
24
src/ipc/listeners.ts
Normal file
24
src/ipc/listeners.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
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
|
||||
});
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
|
||||
"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);
|
||||
}
|
||||
77
src/main.ts
77
src/main.ts
|
|
@ -1,72 +1,39 @@
|
|||
/**
|
||||
* 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 { fetchAccount, submit } from "@/api/account";
|
||||
import { launchSession, endSession } from "@/session";
|
||||
import useIpc from "@/ipc/index";
|
||||
import store from "@/composables/store";
|
||||
import initIpcMain from "@/ipc/index";
|
||||
import { accountAuth, updateAppState } from "./account";
|
||||
import { launchSession } from "./session";
|
||||
import createWindow from "./window";
|
||||
|
||||
/* user profile, signals whether user is logged in */
|
||||
let auth: Profile | null = null;
|
||||
|
||||
/* authenticate the user */
|
||||
export async function authenticate(email: string, password: string) {
|
||||
|
||||
/* attempt normal login */
|
||||
try {
|
||||
auth = await submit(email, password);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
/* 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();
|
||||
|
||||
}
|
||||
let authState: AuthState | null;
|
||||
|
||||
export default async function main() {
|
||||
|
||||
/* launch browser window */
|
||||
// await createWindow();
|
||||
/* initiate controls for frontend to use when needed */
|
||||
initIpcMain();
|
||||
|
||||
/* attempt key-based authentication with business api */
|
||||
const token = store.get('key', null);
|
||||
const crimataId = store.get('crimataId', null);
|
||||
/* launch browser window */
|
||||
await createWindow();
|
||||
|
||||
try {
|
||||
const res = await fetchAccount(crimataId, token);
|
||||
auth = parseAuthRes(res);
|
||||
} catch (e) {
|
||||
console.log('[MAIN]', e);
|
||||
authState = await accountAuth() as AuthState;
|
||||
} catch(e) {
|
||||
console.log('AUTH:', e);
|
||||
authState = null;
|
||||
} finally {
|
||||
if (authState) {
|
||||
launchSession(authState.token as string);
|
||||
}
|
||||
updateAppState();
|
||||
}
|
||||
|
||||
/* connect to Crimata, or listen for manual login req */
|
||||
if (auth) {
|
||||
launchSession(auth);
|
||||
}
|
||||
|
||||
/* initiate controls for frontend to use when needed */
|
||||
useIpc();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
<template>
|
||||
<!-- Only render when profile has been set -->
|
||||
<main id="app" v-if="typeof profile !== 'undefined'">
|
||||
<main id="app" v-if="authComplete">
|
||||
|
||||
<Header />
|
||||
|
||||
<!-- Main Components -->
|
||||
<Messenger
|
||||
v-if="profile"
|
||||
:profile="profile"
|
||||
v-if="profile"
|
||||
/>
|
||||
|
||||
<!-- Main Components
|
||||
-->
|
||||
<Login v-else />
|
||||
|
||||
</main>
|
||||
|
|
@ -20,14 +21,17 @@
|
|||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, onMounted, onUnmounted, Ref } from "vue";
|
||||
import { invokeProfile } from "@/ipc/account";
|
||||
import { defineComponent, onMounted } from "vue";
|
||||
import { IpcRendererEvent } from "electron";
|
||||
|
||||
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: {
|
||||
|
|
@ -39,23 +43,10 @@ export default defineComponent({
|
|||
|
||||
setup() {
|
||||
|
||||
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");
|
||||
});
|
||||
onMounted(() => postAppMount());
|
||||
|
||||
return {
|
||||
authComplete,
|
||||
profile
|
||||
}
|
||||
}
|
||||
|
|
@ -68,7 +59,6 @@ export default defineComponent({
|
|||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
// Background color set in window.ts
|
||||
}
|
||||
|
||||
#app {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<!-- message context -->
|
||||
<span class="context">
|
||||
<div :class="`${type}-icon`">
|
||||
<div :class="`${type}-icon`"/>
|
||||
{{ context }}
|
||||
</span>
|
||||
|
||||
|
|
@ -22,23 +22,24 @@
|
|||
import { defineComponent, ref, onMounted } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageItem",
|
||||
name: "Bubble",
|
||||
|
||||
props: ["text", "context", "type", "position"],
|
||||
|
||||
setup() {
|
||||
|
||||
const seen = ref(false);
|
||||
/* initialize seen state */
|
||||
const seen = ref(() => {
|
||||
if (document.visibilityState === "visible") {
|
||||
return true;
|
||||
} else return false;
|
||||
});
|
||||
if (document.visibilityState === "visible") {
|
||||
seen.value = true;
|
||||
} else seen.value = false;
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
if (!seen)
|
||||
document.addEventListener("visibilitychange", () => seen = true);
|
||||
if(seen.value)
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
seen.value = true
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export function newMessage ({
|
|||
audio=false,
|
||||
context=false,
|
||||
uid=uuidv4()
|
||||
}): Message {
|
||||
}) {
|
||||
return {
|
||||
text: text,
|
||||
audio: audio,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import anime from "animejs";
|
||||
import { useIpc } from '@/modules/ipc';
|
||||
import { onMounted, onUnmounted, ref, Ref } from "vue";
|
||||
import { postMessage } from "@/ipc/session";
|
||||
import { postMessage } from "@/render/ipc";
|
||||
import { newMessage, animateAudioInput } from "./helpers";
|
||||
import { invokeStopRecord, postAudioChunk } from "@/ipc/audio";
|
||||
import { invokeReturnAudio, postAudioChunk } from "@/render/ipc";
|
||||
|
||||
export default function useAudioInputController (typing: Ref) {
|
||||
|
||||
|
|
@ -28,7 +26,7 @@ export default function useAudioInputController (typing: Ref) {
|
|||
|
||||
// get audio and post new message to backend
|
||||
mediaRecorder.addEventListener('stop', (_e: Event) => {
|
||||
invokeStopRecord().then((audio: ArrayBuffer[] | Error) => {
|
||||
invokeReturnAudio().then((audio: ArrayBuffer[] | Error) => {
|
||||
console.log(audio);
|
||||
// postMessage(newMessage({audio: audio}));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import { postMessage } from "@/ipc/session";
|
||||
import { postMessage } from "@/render/ipc";
|
||||
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: textInput.value
|
||||
text: false
|
||||
});
|
||||
|
||||
postMessage(message);
|
||||
// postMessage(message);
|
||||
|
||||
clearInput()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,143 +1,38 @@
|
|||
import { ref } from 'vue';
|
||||
import invokeSavedMessages from "@/render/ipc";
|
||||
import useScroll from "@/render/composables/scroll";
|
||||
import useScroll from "@/render/composables/useScroll";
|
||||
|
||||
const messages = ref(new Map());
|
||||
const messagesRef = ref();
|
||||
|
||||
function getTimeStamp(): number {
|
||||
const currentdate = new Date();
|
||||
return currentdate.getTime();
|
||||
/* seed the canvas with messages */
|
||||
const seedCanvas = (messages: Message[]) => {
|
||||
messagesRef.value = messages;
|
||||
}
|
||||
|
||||
const newViewMessage = (message: Message): ViewMessage => {
|
||||
return {
|
||||
text: message.text,
|
||||
context: message.context,
|
||||
audio: message.audio,
|
||||
from: message.from,
|
||||
uid: message.uid,
|
||||
time: getTimeStamp(),
|
||||
isChild: "none",
|
||||
seen: false,
|
||||
newMessage: false
|
||||
};
|
||||
}
|
||||
|
||||
const addMessage = (message: Message, newMessage=false) => {
|
||||
const viewMessage = newViewMessage(message);
|
||||
if (newMessage) viewMessage.newMessage = true;
|
||||
messages.value.set(viewMessage.uid, viewMessage);
|
||||
const addMessage = (message: Message) => {
|
||||
messagesRef.value.push(message);
|
||||
}
|
||||
|
||||
const updateMessage = (message: Message) => {
|
||||
const viewMessage = messages.value.get(message.uid);
|
||||
viewMessage.context = message.context;
|
||||
viewMessage.text = message.text;
|
||||
}
|
||||
|
||||
const loadSavedMessages = async () => {
|
||||
const messageData = await invokeSavedMessages();
|
||||
messages.value = new Map(Object.entries(messageData));
|
||||
}
|
||||
let target_message = messagesRef.value.filter((m: Message) => {
|
||||
return m.uid = message.uid;
|
||||
})[0];
|
||||
|
||||
const saveMessages = () => {
|
||||
const messageData = Object.fromEntries(messages.value);
|
||||
// must save to json.
|
||||
}
|
||||
|
||||
const pruneMessages = (limit=200) => {
|
||||
if (messages.value.size >= limit) {
|
||||
const oldest = Array.from(messages.value.keys()).shift();
|
||||
messages.value.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
const updateGrouping = () => {
|
||||
|
||||
const isSimmilar = (messageA: ViewMessage, messageB: ViewMessage) => {
|
||||
if ((Math.abs(messageA.time - messageB.time) < 20000) && (messageA.from == messageB.from) && (messageA.context == messageB.context)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
if (target_message) {
|
||||
target_message = message;
|
||||
}
|
||||
|
||||
const refs = Array.from(messages.value.keys())
|
||||
|
||||
// Get the last three messages.
|
||||
const first = messages.value.get(refs[refs.length - 1])
|
||||
const second = messages.value.get(refs[refs.length - 2])
|
||||
const third = messages.value.get(refs[refs.length - 3])
|
||||
|
||||
// If messages are simmilar, update the classes.
|
||||
if ((first) && (second)) {
|
||||
if (isSimmilar(first, second)) {
|
||||
first.isChild = "last" // i.e. last in group.
|
||||
second.isChild = "first"
|
||||
|
||||
if (third) {
|
||||
if ((third.isChild == "first") || (third.isChild == "middle")) {
|
||||
second.isChild = "middle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function useMessages() {
|
||||
|
||||
const { updateScrollRef, adjustScroll } = useScroll("messenger");
|
||||
|
||||
/* Given new message object, update the view accordingly */
|
||||
const updateMessageView = (newMessages: Message[]) => {
|
||||
|
||||
const bottom = updateScrollRef(); // see if the user is scrolled down
|
||||
|
||||
// take each message and apply view
|
||||
newMessages.forEach((message: Message) => {
|
||||
|
||||
// add or update message depending
|
||||
if (messages.value.has(message.uid)) {
|
||||
updateMessage(message);
|
||||
} else addMessage(message);
|
||||
|
||||
pruneMessages(); // pop off old messages from view
|
||||
updateGrouping(); // group like message together
|
||||
|
||||
if (bottom) adjustScroll(); // only scroll if user was at bottom
|
||||
|
||||
saveMessages();
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
updateMessageView,
|
||||
loadSavedMessages
|
||||
}
|
||||
messagesRef,
|
||||
seedCanvas,
|
||||
addMessage,
|
||||
updateMessage
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
// // Seed message view with message history.
|
||||
// const prepMessageView = async (newMessages: Message[]) => {
|
||||
// console.log("MSGR:Prepping messenger view.")
|
||||
|
||||
// // Load and render saved messages and immediately scroll to bottom.
|
||||
// await loadSavedMessages();
|
||||
// setTimeout(setScroll.bind(false), 10);
|
||||
|
||||
// // Render new messages, then wait 1s to scroll.
|
||||
// if (newMessages.length) {
|
||||
|
||||
// console.log("MSGR:Adding new messages")
|
||||
|
||||
// newMessages.forEach(message => {
|
||||
// addMessage(message, true);
|
||||
// })
|
||||
|
||||
// setTimeout(setScroll.bind(true), 1000);
|
||||
|
||||
// }
|
||||
// }
|
||||
|
|
@ -14,14 +14,22 @@
|
|||
|
||||
<script lang="ts">
|
||||
|
||||
import { postNavBarExit, postNavBarMin } from "@/render/ipc";
|
||||
|
||||
setup() {
|
||||
// import { postNavBarExit, postNavBarMin } from "@/render/ipc";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Header",
|
||||
setup() {
|
||||
const postNavBarExit = () => {};
|
||||
const postNavBarMin = () => {};
|
||||
return {
|
||||
postNavBarExit,
|
||||
postNavBarMin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
|
@ -78,4 +86,4 @@
|
|||
.minimizeButton:active {
|
||||
background-color: #c08e38;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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,24 +27,19 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { defineComponent } from "vue";
|
||||
import draggify from "@/modules/draggify";
|
||||
import draggify from "@/render/composables/useDraggify";
|
||||
|
||||
import TextInput from "@/components/textInput.vue";
|
||||
|
||||
import useTextInputController from
|
||||
"@/components/controllers/inputItem.control.audio";
|
||||
import useAudioInputController from
|
||||
"@/components/controllers/inputItem.control.text";
|
||||
import useTextInputController from
|
||||
"@/render/components/controllers/inputItem.control.text";
|
||||
import useAudioInputController from
|
||||
"@/render/components/controllers/inputItem.control.audio";
|
||||
|
||||
export default defineComponent({
|
||||
name: "InputItem",
|
||||
|
||||
props: ["initials"],
|
||||
|
||||
components: {
|
||||
TextInput
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
||||
// Default values for position.
|
||||
|
|
|
|||
|
|
@ -35,11 +35,8 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { defineComponent, ref } from "vue";
|
||||
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";
|
||||
import { setProfile } from '@/render/composables/useProfile';
|
||||
import { invokeLogin } from "@/render/ipc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Login",
|
||||
|
|
@ -57,12 +54,14 @@ export default defineComponent({
|
|||
const profile = await invokeLogin({
|
||||
email: usr.value,
|
||||
password: pwd.value
|
||||
});
|
||||
}) as Profile;
|
||||
|
||||
/* emit event to app.vue */
|
||||
window.postMessage(profile);
|
||||
setProfile(profile)
|
||||
|
||||
} catch (e) console.log(e);
|
||||
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<div id="messenger">
|
||||
<Bubble
|
||||
v-for="message in messages"
|
||||
:text="message.text"
|
||||
:text="message.content.text"
|
||||
:context="message.context"
|
||||
:key="message[0]"
|
||||
/>
|
||||
|
|
@ -20,45 +20,26 @@
|
|||
<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 useMessages from "@/render/composables/messages";
|
||||
import Bubble from "@/render/components/bubble.vue";
|
||||
|
||||
import { profile } from "@/render/composables/useProfile";
|
||||
import { messages } from "@/render/composables/useMessages";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Messenger",
|
||||
|
||||
props: ["state"],
|
||||
|
||||
components: {
|
||||
Message,
|
||||
InputItem,
|
||||
Settings
|
||||
Settings,
|
||||
Bubble
|
||||
},
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
setup() {
|
||||
return {
|
||||
messages
|
||||
messages,
|
||||
profile
|
||||
};
|
||||
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,10 +28,8 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { useIpc } from "@/modules/ipc";
|
||||
import { logoutRequest } from '@/modules/message';
|
||||
import { useProfile } from "@/modules/auth"
|
||||
import { invokeLogout } from "@/ipcRend/account";
|
||||
import { clearProfile } from "@/render/composables/useProfile"
|
||||
import { invokeLogout } from "@/render/ipc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Settings",
|
||||
|
|
@ -39,10 +37,6 @@
|
|||
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") {
|
||||
|
|
@ -63,8 +57,8 @@
|
|||
console.log("Submitting logout request.");
|
||||
|
||||
try {
|
||||
clearProfile();
|
||||
await invokeLogout();
|
||||
clearProfile();
|
||||
} catch(e) {
|
||||
console.log('error')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
|
||||
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
|
||||
}
|
||||
}
|
||||
54
src/render/composables/useIpcRend.ts
Normal file
54
src/render/composables/useIpcRend.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
|
||||
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,
|
||||
}
|
||||
}
|
||||
33
src/render/composables/useMessages.ts
Normal file
33
src/render/composables/useMessages.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// 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
|
||||
};
|
||||
27
src/render/composables/useProfile.ts
Normal file
27
src/render/composables/useProfile.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// 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,
|
||||
};
|
||||
|
|
@ -1,46 +1,44 @@
|
|||
|
||||
import useIpc from "@/render/composables/ipc";
|
||||
import useIpc from "@/render/composables/useIpcRend";
|
||||
import * as rendererListeners from "./listeners";
|
||||
|
||||
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('user-login', JSON.stringify(payload))
|
||||
await invoke('invoke-account-login', JSON.stringify(payload))
|
||||
);
|
||||
|
||||
export const invokeLogout = async (): Promise<void> => (
|
||||
await invoke("user-logout", null)
|
||||
await invoke("invoke-account-logout", null)
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Audio endpoints
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
export const postAudioChunk = (chunk: ArrayBuffer): void => (
|
||||
post("audio-chunk", chunk)
|
||||
post("post-audio-collect", chunk)
|
||||
);
|
||||
|
||||
export const invokeReturnAudio = async (): Promise<ArrayBuffer[] | Error> => (
|
||||
await invoke("get-audio", null)
|
||||
await invoke("invoke-audio-flush", null)
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Crimata Platform (session) endpoints
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
export const invokeSession = async (cid: string): Promise<Profile | Error> => (
|
||||
|
|
@ -48,5 +46,35 @@ export const invokeSession = async (cid: string): Promise<Profile | Error> => (
|
|||
);
|
||||
|
||||
export const postMessage = (payload: Message): void => (
|
||||
post('client-message', payload)
|
||||
);
|
||||
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 = {};
|
||||
};
|
||||
|
||||
|
|
|
|||
32
src/render/listeners.ts
Normal file
32
src/render/listeners.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
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
|
||||
});
|
||||
|
||||
21
src/render/main.ts
Normal file
21
src/render/main.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
// 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");
|
||||
|
|
@ -2,63 +2,100 @@
|
|||
|
||||
|
||||
|
||||
import useAudio from "@/audio";
|
||||
import { loadState, saveState, emitState } from "@/state";
|
||||
// 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;
|
||||
|
||||
/* start and stop audio functionality */
|
||||
const { initAudio, closeAudio } = useAudio();
|
||||
// const { initAudio, closeAudio } = useAudio();
|
||||
|
||||
const isInitMessage = (message: any): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const deauthenticate = (): void => {
|
||||
console.log('deauthenticating')
|
||||
};
|
||||
|
||||
const isAddMessage = (message: any): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Controls for interfacing with the platform.
|
||||
* Takes an onMessage callback which we define below.
|
||||
*/
|
||||
const { connect, send, close } = usePlatform((message: Message) => {
|
||||
|
||||
/* add the message to the state */
|
||||
addMessage(message);
|
||||
|
||||
/* push the message to the browser */
|
||||
if (win) emit("new-message", message);
|
||||
const onMessageCallback = (payload: string) => {
|
||||
|
||||
});
|
||||
const message = JSON.parse(payload);
|
||||
console.log(typeof message);
|
||||
|
||||
/* send a message to the platform */
|
||||
export function sendMessage(message: Message) {
|
||||
/* if the platform fails to authenticate, we must back down */
|
||||
if (message === "CLOSE_AUTH_FAIL") {
|
||||
deauthenticate();
|
||||
return;
|
||||
}
|
||||
|
||||
/* add the message to the state */
|
||||
addMessage(message);
|
||||
ipcEmit("add-message", message)
|
||||
return
|
||||
|
||||
/* push the message to the browser */
|
||||
if (win) emit("new-message", 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 {
|
||||
|
||||
/* socket send */
|
||||
send(message);
|
||||
|
||||
}
|
||||
|
||||
/* launch a new session (the main process for authenticated users) */
|
||||
export function launchSession(profile: Profile) {
|
||||
|
||||
/* load any previously saved state for that user */
|
||||
loadState(profile);
|
||||
/* launch a new session (the main process for authenticated users) */
|
||||
export function launchSession(platformKey: string) {
|
||||
|
||||
/* connect to the platform */
|
||||
connect(profile);
|
||||
connect(config.PLATFORM_URL, platformKey);
|
||||
|
||||
/* initialize the audio streams */
|
||||
// initAudio();
|
||||
|
||||
/* finally we can push state to browser */
|
||||
if (win) emitState();
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function endSession() {
|
||||
|
||||
closeAudioStreams();
|
||||
// closeAudio();
|
||||
|
||||
closeSocket();
|
||||
close();
|
||||
|
||||
state.clear();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
src/state.ts
29
src/state.ts
|
|
@ -1,29 +0,0 @@
|
|||
const Store = require('electron-store');
|
||||
|
||||
/* simple data persistance */
|
||||
const store = new Store;
|
||||
|
||||
/* state of the session (e.g. profile and messages for now) */
|
||||
let state: State | null = null;
|
||||
|
||||
/* load saved state in electron store for given user */
|
||||
export function loadState(profile: Profile) {
|
||||
state = store.get("state", null);
|
||||
}
|
||||
|
||||
/* add a message to state.messages */
|
||||
export function addMessage(message: Message) {
|
||||
if (state) {
|
||||
state.messages.push(message);
|
||||
saveState();
|
||||
}
|
||||
}
|
||||
|
||||
export function saveState() {
|
||||
store.set("state", state);
|
||||
}
|
||||
|
||||
export function emitState() {
|
||||
emit("update-state", state);
|
||||
}
|
||||
|
||||
31
src/store.ts
Normal file
31
src/store.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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();
|
||||
}
|
||||
|
||||
56
src/types.ts
56
src/types.ts
|
|
@ -1,16 +1,14 @@
|
|||
|
||||
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 {
|
||||
|
|
@ -26,12 +24,54 @@ 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
"use strict";
|
||||
|
||||
import { BrowserWindow, ipcMain } from "electron";
|
||||
import { BrowserWindow, ipcMain, app } from "electron";
|
||||
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
|
||||
import { backgroundMitt } from './coposables/emitter';
|
||||
import { saveToJson } from "./coposables/json";
|
||||
import { backgroundMitt } from './composables/useEmitter';
|
||||
import { saveToJson } from "./composables/useSaveToJSON";
|
||||
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;
|
||||
|
||||
|
|
@ -19,8 +16,8 @@ const loadWinState = (fileName: string): WindowState => {
|
|||
let state: WindowState;
|
||||
|
||||
try {
|
||||
state = JSON.parse(fs.readFileSync(configPath + fileName).toString());
|
||||
}
|
||||
state = JSON.parse(fs.readFileSync(config.configPath + fileName).toString());
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
state = {
|
||||
|
|
@ -32,8 +29,8 @@ const loadWinState = (fileName: string): WindowState => {
|
|||
}
|
||||
|
||||
return state
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Called when a NavBar button is pressed.
|
||||
const onNavBar = (_event: any, action: string): void => {
|
||||
|
|
@ -47,11 +44,9 @@ const onNavBar = (_event: any, action: string): void => {
|
|||
}
|
||||
|
||||
// Util function to render message on ipc-renderer event.
|
||||
const renderMessage = (payload: IpcRendererPayload): void => {
|
||||
const postToWindow = <T>(event: IpcRendererEvent<T>): void => {
|
||||
if (win) {
|
||||
win.webContents.send(payload.endpoint, {
|
||||
message: payload.message
|
||||
});
|
||||
win.webContents.send(event.channel, event.payload);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +81,7 @@ const onWindowMount = (): void => {
|
|||
|
||||
// Gateway for messages to the frontend.
|
||||
backgroundMitt.removeAllListeners("ipc-renderer")
|
||||
backgroundMitt.on("ipc-renderer", renderMessage);
|
||||
backgroundMitt.on("ipc-renderer", postToWindow);
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -110,8 +105,8 @@ export default async function createWindow(): Promise<void> {
|
|||
win = new BrowserWindow({
|
||||
width: winState.width,
|
||||
height: winState.height,
|
||||
x: winState.x,
|
||||
y: winState.y,
|
||||
x: winState.x as number,
|
||||
y: winState.y as number,
|
||||
resizable: true,
|
||||
backgroundColor: '#EBEBEB',
|
||||
frame: false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue